From 3ef52297f1ca56942d2846a44412b10e5c74db03 Mon Sep 17 00:00:00 2001 From: vincbeck Date: Tue, 22 Sep 2026 10:53:31 -0400 Subject: [PATCH] Allow a Job to be associated with several teams A Job's team scope was a single nullable column, which cannot describe a Dag processor that parses bundles belonging to more than one team. Scoping such a processor to no team at all loses the information that its work is relevant to each of those teams, so team coverage in the health endpoint and the team column in the UI cannot be derived from it. Raised in review of #73295. --- .../logging-monitoring/check-health.rst | 4 +- airflow-core/docs/migrations-ref.rst | 5 +- .../src/airflow/api/common/airflow_health.py | 10 +- .../api_fastapi/core_api/datamodels/job.py | 2 +- .../core_api/datamodels/monitor.py | 2 +- .../core_api/openapi/_private_ui.yaml | 10 +- .../openapi/v2-rest-api-generated.yaml | 22 +- .../airflow/cli/commands/triggerer_command.py | 2 +- airflow-core/src/airflow/jobs/job.py | 23 +- .../src/airflow/jobs/triggerer_job_runner.py | 5 +- ...37_3_4_0_add_job_team_association_table.py | 81 +++ airflow-core/src/airflow/models/team.py | 24 +- .../ui/openapi-gen/requests/schemas.gen.ts | 34 +- .../ui/openapi-gen/requests/types.gen.ts | 4 +- .../pages/Dashboard/Health/Health.test.tsx | 9 +- .../ui/src/pages/Dashboard/Health/Health.tsx | 2 +- .../Dashboard/Health/HealthInstances.tsx | 14 +- airflow-core/src/airflow/utils/db.py | 2 +- .../unit/api/common/test_airflow_health.py | 28 +- .../core_api/routes/public/test_job.py | 26 +- .../core_api/routes/public/test_monitor.py | 6 +- .../routes/public/test_task_instances.py | 2 +- .../cli/commands/test_triggerer_command.py | 14 +- airflow-core/tests/unit/jobs/test_base_job.py | 67 ++ .../tests/unit/jobs/test_triggerer_job.py | 2 +- .../tests/unit/utils/test_db_cleanup.py | 1 + .../airflowctl/api/datamodels/generated.py | 4 +- .../src/tests_common/test_utils/db.py | 7 + uv.lock | 632 +++++++++--------- 29 files changed, 629 insertions(+), 415 deletions(-) create mode 100644 airflow-core/src/airflow/migrations/versions/0137_3_4_0_add_job_team_association_table.py diff --git a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst index 878f949686741..c259a68d3b3ba 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst @@ -64,7 +64,7 @@ including per-instance details when multiple schedulers, triggerers, or Dag proc { "hostname": "triggerer-1.example.com", "latest_triggerer_heartbeat": "2018-12-26T17:16:12+00:00", - "team_name": "team-a" + "team_names": ["team-a"] } ] }, @@ -130,7 +130,7 @@ including per-instance details when multiple schedulers, triggerers, or Dag proc * ``hostname``: host where the component is running * the corresponding ``latest_*_heartbeat`` for that instance - * ``team_name`` (triggerer only): team the triggerer is scoped to, or ``null`` when unscoped + * ``team_names`` (triggerer only): teams the triggerer is scoped to, empty when unscoped * ``bundle_names`` (Dag processor only): Dag bundles that processor is configured to parse, or ``null`` when unset * For HA deployments, prefer ``detailed_status`` and ``instances`` when you need to see every scheduler, diff --git a/airflow-core/docs/migrations-ref.rst b/airflow-core/docs/migrations-ref.rst index 009cb049e481d..942f3e6ad864a 100644 --- a/airflow-core/docs/migrations-ref.rst +++ b/airflow-core/docs/migrations-ref.rst @@ -39,7 +39,10 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | Revision ID | Revises ID | Airflow Version | Description | +=========================+==================+===================+==============================================================+ -| ``3b7a91c5df20`` (head) | ``5182d0596ee2`` | ``3.4.0`` | Fold task_map into xcom.mapped_length. | +| ``c9f4b3e7a218`` (head) | ``3b7a91c5df20`` | ``3.4.0`` | Replace the ``job.team_name`` column with a ``job_team`` | +| | | | association table. | ++-------------------------+------------------+-------------------+--------------------------------------------------------------+ +| ``3b7a91c5df20`` | ``5182d0596ee2`` | ``3.4.0`` | Fold task_map into xcom.mapped_length. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | ``5182d0596ee2`` | ``b6a9c2e7d410`` | ``3.4.0`` | Widen revoked_token.jti to store external-issuer token | | | | | identifiers. | diff --git a/airflow-core/src/airflow/api/common/airflow_health.py b/airflow-core/src/airflow/api/common/airflow_health.py index def85190e4401..b073ede5f8a7b 100644 --- a/airflow-core/src/airflow/api/common/airflow_health.py +++ b/airflow-core/src/airflow/api/common/airflow_health.py @@ -95,7 +95,7 @@ def _legacy_status(jobs: list[Job]) -> HealthStatus: def _triggerer_instance_health(job: Job) -> dict[str, Any]: return { **_job_instance_health(job, "latest_triggerer_heartbeat"), - "team_name": job.team_name, + "team_names": job.team_names, } @@ -168,7 +168,13 @@ def _triggerer_detailed_status(jobs: list[Job]) -> DetailedHealthStatus: if not expected: return _liveness_status(jobs) - return _coverage_status(expected, {job.team_name for job in jobs if job.is_alive()}) + covered: set[str | None] = set() + for job in jobs: + if job.is_alive(): + # An unscoped triggerer has no ``team_names``, it covers the team-less + # scope, represented by ``None`` in both ``expected`` and ``covered``. + covered |= set(job.team_names) or {None} + return _coverage_status(expected, covered) def get_airflow_health() -> dict[str, Any]: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py index 8e316d7b2e381..9164ad4e03af2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py @@ -37,7 +37,7 @@ class JobResponse(BaseModel): executor_class: str | None hostname: str | None unixname: str | None - team_name: str | None = None + team_names: list[str] = Field(default_factory=list) bundle_names: list[str] | None = None dag_display_name: str | None = Field( validation_alias=AliasPath("dag_model", "dag_display_name"), default=None diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py index 442a5e9f44aa3..4af62ae387b63 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py @@ -40,7 +40,7 @@ class TriggererInstanceInfoResponse(BaseModel): hostname: str | None latest_triggerer_heartbeat: str | None - team_name: str | None + team_names: list[str] class DagProcessorInstanceInfoResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index 7af3e1262f4cb..24938a8f7658a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -4107,11 +4107,11 @@ components: - type: string - type: 'null' title: Unixname - team_name: - anyOf: - - type: string - - type: 'null' - title: Team Name + team_names: + items: + type: string + type: array + title: Team Names bundle_names: anyOf: - items: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 732ca53e13898..49694cd5d035c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -15389,11 +15389,11 @@ components: - type: string - type: 'null' title: Unixname - team_name: - anyOf: - - type: string - - type: 'null' - title: Team Name + team_names: + items: + type: string + type: array + title: Team Names bundle_names: anyOf: - items: @@ -17211,16 +17211,16 @@ components: - type: string - type: 'null' title: Latest Triggerer Heartbeat - team_name: - anyOf: - - type: string - - type: 'null' - title: Team Name + team_names: + items: + type: string + type: array + title: Team Names type: object required: - hostname - latest_triggerer_heartbeat - - team_name + - team_names title: TriggererInstanceInfoResponse description: Triggerer instance info serializer for responses. UpdateHITLDetailPayload: diff --git a/airflow-core/src/airflow/cli/commands/triggerer_command.py b/airflow-core/src/airflow/cli/commands/triggerer_command.py index 87200ff57beeb..3f881e35c510b 100644 --- a/airflow-core/src/airflow/cli/commands/triggerer_command.py +++ b/airflow-core/src/airflow/cli/commands/triggerer_command.py @@ -62,7 +62,7 @@ def triggerer_run( set_component_mp_start_method("triggerer") with _serve_logs(skip_serve_logs): triggerer_job_runner = TriggererJobRunner( - job=Job(heartrate=triggerer_heartrate, team_name=team_name), + job=Job(heartrate=triggerer_heartrate, team_names=[team_name] if team_name else []), capacity=capacity, queues=queues, ) diff --git a/airflow-core/src/airflow/jobs/job.py b/airflow-core/src/airflow/jobs/job.py index fa59a440abc5c..d19dff203f47c 100644 --- a/airflow-core/src/airflow/jobs/job.py +++ b/airflow-core/src/airflow/jobs/job.py @@ -17,14 +17,14 @@ # under the License. from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from datetime import datetime from enum import Enum from functools import cached_property, lru_cache from time import sleep from typing import TYPE_CHECKING, NoReturn -from sqlalchemy import ForeignKey, Index, Integer, String, case, select +from sqlalchemy import Index, Integer, String, case, select from sqlalchemy.exc import OperationalError from sqlalchemy.orm import Mapped, backref, foreign, mapped_column, relationship from sqlalchemy.orm.session import make_transient @@ -35,6 +35,7 @@ from airflow.exceptions import AirflowException from airflow.listeners.listener import get_listener_manager from airflow.models.base import ID_LEN, Base +from airflow.models.team import JobTeam from airflow.utils.helpers import convert_camel_to_snake from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.net import get_hostname @@ -101,9 +102,6 @@ class Job(Base, LoggingMixin): executor_class: Mapped[str | None] = mapped_column(String(500)) hostname: Mapped[str | None] = mapped_column(String(500)) unixname: Mapped[str | None] = mapped_column(String(1000)) - team_name: Mapped[str | None] = mapped_column( - String(50), ForeignKey("team.name", ondelete="SET NULL"), nullable=True - ) bundle_names: Mapped[list[str] | None] = mapped_column(ExtendedJSON, nullable=True) __table_args__ = ( @@ -137,6 +135,21 @@ class Job(Base, LoggingMixin): Only makes sense for SchedulerJob. """ + job_teams = relationship( + "JobTeam", + cascade="all, delete-orphan", + lazy="selectin", + ) + + @property + def team_names(self) -> list[str]: + """Names of the teams whose workloads this job serves, empty when it is not team-scoped.""" + return sorted(job_team.team_name for job_team in self.job_teams) + + @team_names.setter + def team_names(self, names: Iterable[str]) -> None: + self.job_teams = [JobTeam(team_name=name) for name in dict.fromkeys(names)] + def __init__(self, heartrate=None, **kwargs): # Save init parameters as DB fields self.heartbeat_failed = False diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py b/airflow-core/src/airflow/jobs/triggerer_job_runner.py index 243b8f154d77c..ede83fe6c393a 100644 --- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py +++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py @@ -276,7 +276,10 @@ def _execute(self) -> int | None: capacity=self.capacity, logger=log, queues=self.queues, - team_name=self.job.team_name, + # A triggerer is started with at most one ``--team-name`` for now, so while a Job can + # serve several teams this one never does, and the runner stays scoped to a single team. + # If we later let a triggerer take several ``--team-name`` values, this must fan out. + team_name=next(iter(self.job.team_names), None), ) # Run the main DB comms loop in this process self.trigger_runner.run() diff --git a/airflow-core/src/airflow/migrations/versions/0137_3_4_0_add_job_team_association_table.py b/airflow-core/src/airflow/migrations/versions/0137_3_4_0_add_job_team_association_table.py new file mode 100644 index 0000000000000..49af0e10b256e --- /dev/null +++ b/airflow-core/src/airflow/migrations/versions/0137_3_4_0_add_job_team_association_table.py @@ -0,0 +1,81 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Replace the ``job.team_name`` column with a ``job_team`` association table. + +A single job can serve more than one team -- a Dag processor parsing bundles that belong to +several teams is relevant to each of them -- so the association is many-to-many. + +Revision ID: c9f4b3e7a218 +Revises: 3b7a91c5df20 +Create Date: 2026-09-22 10:00:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c9f4b3e7a218" +down_revision = "3b7a91c5df20" +branch_labels = None +depends_on = None +airflow_version = "3.4.0" + + +def upgrade(): + """Replace ``job.team_name`` with the ``job_team`` association table.""" + # The column goes before the table is created: on SQLite, dropping a column rebuilds the + # whole ``job`` table, which a ``job_team`` foreign key pointing at it would not survive. + with op.batch_alter_table("job", schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f("job_team_name_fkey"), type_="foreignkey") + batch_op.drop_column("team_name") + + op.create_table( + "job_team", + sa.Column("job_id", sa.Integer(), nullable=False), + sa.Column("team_name", sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint("job_id", "team_name", name=op.f("job_team_pkey")), + sa.ForeignKeyConstraint( + columns=("job_id",), + refcolumns=["job.id"], + name="job_team_job_id_fkey", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + columns=("team_name",), + refcolumns=["team.name"], + name="job_team_team_name_fkey", + ondelete="CASCADE", + ), + sa.Index("idx_job_team_team_name", "team_name", unique=False), + ) + + +def downgrade(): + """Restore the ``job.team_name`` column.""" + op.drop_table("job_team") + + with op.batch_alter_table("job", schema=None) as batch_op: + batch_op.add_column(sa.Column("team_name", sa.String(length=50), nullable=True)) + batch_op.create_foreign_key( + batch_op.f("job_team_name_fkey"), "team", ["team_name"], ["name"], ondelete="SET NULL" + ) diff --git a/airflow-core/src/airflow/models/team.py b/airflow-core/src/airflow/models/team.py index fab0e27240976..9045e97e0da65 100644 --- a/airflow-core/src/airflow/models/team.py +++ b/airflow-core/src/airflow/models/team.py @@ -20,7 +20,7 @@ import re from typing import TYPE_CHECKING -from sqlalchemy import Column, ForeignKey, Index, String, Table, select +from sqlalchemy import Column, ForeignKey, Index, Integer, String, Table, select from sqlalchemy.orm import Mapped, mapped_column, relationship from airflow.models.base import Base, StringID @@ -64,6 +64,28 @@ def find_invalid_team_names(names: Iterable[str]) -> list[str]: ) +class JobTeam(Base): + """ + Association between a Job and a team whose workloads that Job serves. + + Mapped as a class rather than a bare association table so that a Job can be given its + teams before it is persisted: writing link rows never needs to load, and therefore can + never accidentally insert, a :class:`Team`. + """ + + __tablename__ = "job_team" + + job_id: Mapped[int] = mapped_column(Integer, ForeignKey("job.id", ondelete="CASCADE"), primary_key=True) + team_name: Mapped[str] = mapped_column( + String(50), ForeignKey("team.name", ondelete="CASCADE"), primary_key=True + ) + + __table_args__ = (Index("idx_job_team_team_name", "team_name"),) + + def __repr__(self): + return f"JobTeam(job_id={self.job_id}, team_name={self.team_name})" + + class Team(Base): """ Contains the list of teams defined in the environment. diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index b9a58b37fd6a3..71d21c125c53e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -6122,16 +6122,12 @@ export const $JobResponse = { ], title: 'Unixname' }, - team_name: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - title: 'Team Name' + team_names: { + items: { + type: 'string' + }, + type: 'array', + title: 'Team Names' }, bundle_names: { anyOf: [ @@ -8946,20 +8942,16 @@ export const $TriggererInstanceInfoResponse = { ], title: 'Latest Triggerer Heartbeat' }, - team_name: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - title: 'Team Name' + team_names: { + items: { + type: 'string' + }, + type: 'array', + title: 'Team Names' } }, type: 'object', - required: ['hostname', 'latest_triggerer_heartbeat', 'team_name'], + required: ['hostname', 'latest_triggerer_heartbeat', 'team_names'], title: 'TriggererInstanceInfoResponse', description: 'Triggerer instance info serializer for responses.' } as const; diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 5972b95c45879..776009c9afc25 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1639,7 +1639,7 @@ export type JobResponse = { executor_class: string | null; hostname: string | null; unixname: string | null; - team_name?: string | null; + team_names?: Array<(string)>; bundle_names?: Array<(string)> | null; dag_display_name?: string | null; }; @@ -2239,7 +2239,7 @@ export type TriggererInfoResponse = { export type TriggererInstanceInfoResponse = { hostname: string | null; latest_triggerer_heartbeat: string | null; - team_name: string | null; + team_names: Array<(string)>; }; /** diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.test.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.test.tsx index c4c8fda259901..961222ae93228 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.test.tsx @@ -240,7 +240,7 @@ describe("Health", () => { expect(screen.getByText("health.instances.unknownHostname")).toBeInTheDocument(); }); - it("shows the owning team of each triggerer only when multi-team is enabled", async () => { + it("shows every owning team of each triggerer only when multi-team is enabled", async () => { mockConfig.multi_team = true; renderHealth( health({ @@ -250,7 +250,7 @@ describe("Health", () => { { hostname: "triggerer-1.example.com", latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", - team_name: "team-a", + team_names: ["team-a", "team-b"], }, ], latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", @@ -263,6 +263,7 @@ describe("Health", () => { expect(screen.getByText("health.instances.team")).toBeInTheDocument(); expect(screen.getByRole("link", { name: "team-a" })).toHaveAttribute("href", "/dags?teams=team-a"); + expect(screen.getByRole("link", { name: "team-b" })).toHaveAttribute("href", "/dags?teams=team-b"); }); it("hides the team column when multi-team is disabled", async () => { @@ -274,7 +275,7 @@ describe("Health", () => { { hostname: "triggerer-1.example.com", latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", - team_name: "team-a", + team_names: ["team-a"], }, ], latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", @@ -299,7 +300,7 @@ describe("Health", () => { { hostname: "triggerer-1.example.com", latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", - team_name: null, + team_names: [], }, ], latest_triggerer_heartbeat: "2026-09-11T10:00:00Z", diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx index 91b68c949bfc5..aef443c9f7c37 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx @@ -44,7 +44,7 @@ const triggererInstances = (instances?: Array | n instances?.map((instance): HealthInstance => ({ hostname: instance.hostname, latestHeartbeat: instance.latest_triggerer_heartbeat, - teamName: instance.team_name, + teamNames: instance.team_names, })); const dagProcessorInstances = (instances?: Array | null) => diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/HealthInstances.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/HealthInstances.tsx index 537b23b799ab5..afbea263a9844 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/HealthInstances.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/HealthInstances.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Table, Text } from "@chakra-ui/react"; +import { HStack, Table, Text } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { TeamName } from "src/components/TeamName"; @@ -26,7 +26,7 @@ import { useConfig } from "src/queries/useConfig"; /** * One live component replica, normalised across the three per-component instance payloads so the - * table does not have to know which ``latest_*_heartbeat`` field the endpoint used. ``teamName`` + * table does not have to know which ``latest_*_heartbeat`` field the endpoint used. ``teamNames`` * and ``bundleNames`` are left undefined for components that do not report them at all. There is no * per-instance status: the endpoint only lists replicas that are running, and how healthy the set of * them is together is what the component badge shows. @@ -35,7 +35,7 @@ export type HealthInstance = { readonly bundleNames?: Array | null; readonly hostname: string | null; readonly latestHeartbeat: string | null; - readonly teamName?: string | null; + readonly teamNames?: Array | null; }; type Props = { @@ -47,7 +47,7 @@ export const HealthInstances = ({ instances }: Props) => { const isMultiTeam = Boolean(useConfig("multi_team")); // Both columns only apply to one component each, and even there every instance may report // nothing — an all-empty column is noise, so it is dropped rather than rendered blank. - const showTeam = isMultiTeam && instances.some((instance) => Boolean(instance.teamName)); + const showTeam = isMultiTeam && instances.some((instance) => Boolean(instance.teamNames?.length)); const showBundles = instances.some((instance) => Boolean(instance.bundleNames?.length)); return ( @@ -77,7 +77,11 @@ export const HealthInstances = ({ instances }: Props) => { {showTeam ? ( - + + {instance.teamNames?.map((teamName) => ( + + ))} + ) : undefined} {showBundles ? {instance.bundleNames?.join(", ")} : undefined} diff --git a/airflow-core/src/airflow/utils/db.py b/airflow-core/src/airflow/utils/db.py index a116050d1ba7d..a76937b6b1d2d 100644 --- a/airflow-core/src/airflow/utils/db.py +++ b/airflow-core/src/airflow/utils/db.py @@ -117,7 +117,7 @@ class MappedClassProtocol(Protocol): "3.1.8": "509b94a1042d", "3.2.0": "1d6611b6ab7c", "3.3.0": "d2f4e1b3c5a7", - "3.4.0": "3b7a91c5df20", + "3.4.0": "c9f4b3e7a218", } # Prefix used to identify tables holding data moved during migration. diff --git a/airflow-core/tests/unit/api/common/test_airflow_health.py b/airflow-core/tests/unit/api/common/test_airflow_health.py index cb10bcb79acda..9affe9faab660 100644 --- a/airflow-core/tests/unit/api/common/test_airflow_health.py +++ b/airflow-core/tests/unit/api/common/test_airflow_health.py @@ -57,14 +57,14 @@ def _mock_job( hostname: str, heartbeat: datetime, alive: bool, - team_name: str | None = None, + team_names: list[str] | None = None, bundle_names: list[str] | None = None, ) -> MagicMock: job = MagicMock(spec=Job) job.hostname = hostname job.latest_heartbeat = heartbeat job.is_alive = MagicMock(return_value=alive) - job.team_name = team_name + job.team_names = team_names or [] job.bundle_names = bundle_names return job @@ -86,7 +86,7 @@ def _create_job( heartbeat: datetime, state: JobState = JobState.RUNNING, end_date: datetime | None = None, - team_name: str | None = None, + team_names: list[str] | None = None, bundle_names: list[str] | None = None, ) -> Job: job = Job( @@ -94,7 +94,7 @@ def _create_job( latest_heartbeat=heartbeat, hostname=hostname, end_date=end_date, - team_name=team_name, + team_names=team_names or [], bundle_names=bundle_names, ) if runner_class is TriggererJobRunner: @@ -118,13 +118,13 @@ def _create_job( hostname="triggerer-alive", heartbeat=datetime(2024, 2, 1), alive=True, - team_name="team-a", + team_names=["team-a"], ) STALE_TRIGGERER_JOB_MOCK = _mock_job( hostname="triggerer-stale", heartbeat=datetime(2024, 1, 1), alive=False, - team_name="team-b", + team_names=["team-b"], ) ALIVE_DAG_PROCESSOR_JOB_MOCK = _mock_job( @@ -230,7 +230,7 @@ def test_get_airflow_health_all_stale_jobs(mock_get_jobs_health): @patch("airflow.api.common.airflow_health.get_jobs_health") -def test_get_airflow_health_mixed_triggerers_include_team_name(mock_get_jobs_health): +def test_get_airflow_health_mixed_triggerers_include_team_names(mock_get_jobs_health): mock_get_jobs_health.side_effect = [[], [ALIVE_TRIGGERER_JOB_MOCK, STALE_TRIGGERER_JOB_MOCK], []] health_status = get_airflow_health() @@ -241,7 +241,7 @@ def test_get_airflow_health_mixed_triggerers_include_team_name(mock_get_jobs_hea { "hostname": ALIVE_TRIGGERER_JOB_MOCK.hostname, "latest_triggerer_heartbeat": ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(), - "team_name": ALIVE_TRIGGERER_JOB_MOCK.team_name, + "team_names": ALIVE_TRIGGERER_JOB_MOCK.team_names, }, ] assert ( @@ -267,7 +267,7 @@ def test_get_airflow_health_triggerer_and_dag_processor_healthy(mock_get_jobs_he { "hostname": ALIVE_TRIGGERER_JOB_MOCK.hostname, "latest_triggerer_heartbeat": ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(), - "team_name": ALIVE_TRIGGERER_JOB_MOCK.team_name, + "team_names": ALIVE_TRIGGERER_JOB_MOCK.team_names, } ], }, @@ -300,7 +300,7 @@ def _triggerer(*, alive: bool, team_name: str | None) -> MagicMock: hostname=f"triggerer-{team_name}-{'alive' if alive else 'stale'}", heartbeat=datetime(2024, 2, 1), alive=alive, - team_name=team_name, + team_names=[team_name] if team_name else [], ) @@ -598,7 +598,7 @@ def test_get_airflow_health_all_stale_jobs(self, *, session): assert health_status["scheduler"]["latest_scheduler_heartbeat"] == first.isoformat() @provide_session - def test_get_airflow_health_mixed_triggerers_include_team_name(self, testing_team, *, session): + def test_get_airflow_health_mixed_triggerers_include_team_names(self, testing_team, *, session): alive_heartbeat = timezone.utcnow() stale_heartbeat = timezone.utcnow() - STALE_HEARTBEAT_AGE alive = _create_job( @@ -606,14 +606,14 @@ def test_get_airflow_health_mixed_triggerers_include_team_name(self, testing_tea TriggererJobRunner, hostname="triggerer-alive", heartbeat=alive_heartbeat, - team_name=testing_team.name, + team_names=[testing_team.name], ) _create_job( session, TriggererJobRunner, hostname="triggerer-stale", heartbeat=stale_heartbeat, - team_name=testing_team.name, + team_names=[testing_team.name], ) session.commit() @@ -625,7 +625,7 @@ def test_get_airflow_health_mixed_triggerers_include_team_name(self, testing_tea { "hostname": alive.hostname, "latest_triggerer_heartbeat": alive_heartbeat.isoformat(), - "team_name": testing_team.name, + "team_names": [testing_team.name], }, ] assert health_status["triggerer"]["latest_triggerer_heartbeat"] == alive_heartbeat.isoformat() diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py index cb4c725449241..f969d48b30562 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py @@ -19,10 +19,12 @@ from typing import TYPE_CHECKING import pytest +from sqlalchemy import delete from sqlalchemy.orm import Session from airflow.jobs.job import Job, JobState from airflow.jobs.scheduler_job_runner import SchedulerJobRunner +from airflow.models.team import JobTeam, Team from airflow.utils.session import NEW_SESSION, provide_session from airflow.utils.state import State @@ -144,7 +146,7 @@ def test_get_jobs( ): # setup testcase at runtime based on the `testcase` parameter self.setup(testcase) - with assert_queries_count(2): + with assert_queries_count(3): response = test_client.get("/jobs", params=query_params) assert response.status_code == expected_status_code if expected_status_code != 200: @@ -167,7 +169,7 @@ def test_get_jobs( "latest_heartbeat": from_datetime_to_zulu(matched[0].latest_heartbeat), "executor_class": None, "hostname": matched[0].hostname, - "team_name": None, + "team_names": [], "unixname": matched[0].unixname, } assert resp_job == expected_job @@ -189,12 +191,24 @@ def test_get_jobs_filters_by_dag_id(self, test_client, session: Session): assert response_json["total_entries"] == 1 assert response_json["jobs"][0]["dag_id"] == "target_dag" - def test_get_jobs_includes_team_name_and_bundle_names(self, test_client, session: Session, testing_team): + @pytest.fixture + def extra_team(self, session: Session): + team = Team(name="other-team") + session.add(team) + session.commit() + yield team + session.execute(delete(JobTeam).where(JobTeam.team_name == team.name)) + session.delete(team) + session.commit() + + def test_get_jobs_includes_team_names_and_bundle_names( + self, test_client, session: Session, testing_team, extra_team + ): clear_db_jobs() job = Job( state=JobState.RUNNING, - job_type="TriggererJob", - team_name=testing_team.name, + job_type="DagProcessorJob", + team_names=[extra_team.name, testing_team.name], bundle_names=["bundle-a", "bundle-b"], ) session.add(job) @@ -205,7 +219,7 @@ def test_get_jobs_includes_team_name_and_bundle_names(self, test_client, session assert response.status_code == 200 response_json = response.json() assert response_json["total_entries"] == 1 - assert response_json["jobs"][0]["team_name"] == testing_team.name + assert response_json["jobs"][0]["team_names"] == sorted([testing_team.name, extra_team.name]) assert response_json["jobs"][0]["bundle_names"] == ["bundle-a", "bundle-b"] def test_should_raises_401_unauthenticated(self, unauthenticated_test_client): diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py index e696ec43811b3..e1325de1d9c24 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py @@ -137,7 +137,7 @@ def test_health_with_dag_processor(self, mock_get_airflow_health, test_client): assert body["triggerer"]["status"] == HEALTHY @mock.patch("airflow.api_fastapi.core_api.routes.public.monitor.get_airflow_health") - def test_health_includes_instance_team_name_and_bundle_names(self, mock_get_airflow_health, test_client): + def test_health_includes_instance_team_names_and_bundle_names(self, mock_get_airflow_health, test_client): mock_get_airflow_health.return_value = { "metadatabase": {"status": HEALTHY}, "scheduler": { @@ -161,7 +161,7 @@ def test_health_includes_instance_team_name_and_bundle_names(self, mock_get_airf "status": HEALTHY, "hostname": "triggerer-1", "latest_triggerer_heartbeat": "2024-11-23T11:09:15.815483+00:00", - "team_name": "team-a", + "team_names": ["team-a"], } ], }, @@ -184,7 +184,7 @@ def test_health_includes_instance_team_name_and_bundle_names(self, mock_get_airf assert response.status_code == 200 body = response.json() - assert body["triggerer"]["instances"][0]["team_name"] == "team-a" + assert body["triggerer"]["instances"][0]["team_names"] == ["team-a"] assert body["dag_processor"]["instances"][0]["bundle_names"] == ["bundle-a"] assert body["scheduler"]["instances"][0]["hostname"] == "scheduler-1" diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py index 53f5bc85aa3e0..7d14b4a7c0515 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py @@ -421,7 +421,7 @@ def test_should_respond_200_with_task_state_in_deferred(self, test_client, sessi "end_date": None, "job_type": "TriggererJob", "state": "running", - "team_name": None, + "team_names": [], "unixname": getuser(), }, "team_name": None, diff --git a/airflow-core/tests/unit/cli/commands/test_triggerer_command.py b/airflow-core/tests/unit/cli/commands/test_triggerer_command.py index f88dc90ea5ccb..449ed61626c53 100644 --- a/airflow-core/tests/unit/cli/commands/test_triggerer_command.py +++ b/airflow-core/tests/unit/cli/commands/test_triggerer_command.py @@ -52,7 +52,7 @@ def test_capacity_argument( mock_serve.return_value.__enter__.assert_called_once() mock_serve.return_value.__exit__.assert_called_once() mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, capacity=42, queues=None) - assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is None + assert mock_triggerer_job_runner.call_args.kwargs["job"].team_names == [] @conf_vars({("triggerer", "queues_enabled"): "True"}) @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner") @@ -67,7 +67,7 @@ def test_queues_argument(self, mock_serve, mock_triggerer_job_runner): mock_triggerer_job_runner.assert_called_once_with( job=mock.ANY, capacity=4, queues=set(["my_queue", "other_queue"]) ) - assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is None + assert mock_triggerer_job_runner.call_args.kwargs["job"].team_names == [] @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner") @mock.patch("airflow.cli.commands.triggerer_command.run_job") @@ -120,20 +120,20 @@ def test_team_name_rejected_when_team_does_not_exist(self, mock_get_team): @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner") @mock.patch("airflow.cli.commands.triggerer_command._serve_logs") def test_team_name_passed_through(self, mock_serve, mock_triggerer_job_runner, mock_get_team): - """--team-name should be set on the Job when valid""" + """--team-name should be the Job's only team when valid""" mock_triggerer_job_runner.return_value.job_type = "TriggererJob" args = self.parser.parse_args(["triggerer", "--team-name", "team_a"]) triggerer_command.triggerer(args) mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, capacity=mock.ANY, queues=None) - assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name == "team_a" + assert mock_triggerer_job_runner.call_args.kwargs["job"].team_names == ["team_a"] @conf_vars({("core", "multi_team"): "False"}) @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner") @mock.patch("airflow.cli.commands.triggerer_command._serve_logs") - def test_no_team_name_passes_none(self, mock_serve, mock_triggerer_job_runner): - """Without --team-name, Job.team_name is None""" + def test_no_team_name_leaves_job_unscoped(self, mock_serve, mock_triggerer_job_runner): + """Without --team-name, Job.team_names is empty""" mock_triggerer_job_runner.return_value.job_type = "TriggererJob" args = self.parser.parse_args(["triggerer"]) triggerer_command.triggerer(args) mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, capacity=mock.ANY, queues=None) - assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is None + assert mock_triggerer_job_runner.call_args.kwargs["job"].team_names == [] diff --git a/airflow-core/tests/unit/jobs/test_base_job.py b/airflow-core/tests/unit/jobs/test_base_job.py index 211e4128d01de..eda85fa270b42 100644 --- a/airflow-core/tests/unit/jobs/test_base_job.py +++ b/airflow-core/tests/unit/jobs/test_base_job.py @@ -23,10 +23,12 @@ from unittest.mock import ANY, Mock, patch import pytest +from sqlalchemy import delete, func, select from sqlalchemy.exc import OperationalError from airflow._shared.timezones import timezone from airflow.jobs.job import Job, health_check_threshold, most_recent_job, perform_heartbeat, run_job +from airflow.models.team import JobTeam, Team from airflow.utils.session import create_session from airflow.utils.state import State @@ -278,3 +280,68 @@ def test_health_check_threshold(self, job_type, config_section, config_key, thre def test_health_check_threshold_unknown_job_uses_heartrate_fallback(self): assert health_check_threshold("UnknownJob", 30) == 30 * 2.1 + + +class TestJobTeams: + @pytest.fixture + def teams(self, session): + names = ["team-b", "team-a"] + session.add_all(Team(name=name) for name in names) + session.commit() + yield names + session.execute(delete(JobTeam)) + session.execute(delete(Team).where(Team.name.in_(names))) + session.commit() + + def test_no_teams_by_default(self, session): + job = Job() + session.add(job) + session.commit() + + assert job.team_names == [] + + def test_multiple_teams_round_trip_sorted(self, session, teams): + job = Job(team_names=teams) + session.add(job) + session.commit() + session.expunge(job) + + assert session.get(Job, job.id).team_names == ["team-a", "team-b"] + + def test_repeated_team_name_is_stored_once(self, session, teams): + job = Job(team_names=["team-a", "team-a"]) + session.add(job) + session.commit() + + assert job.team_names == ["team-a"] + assert session.scalar(select(func.count()).select_from(JobTeam)) == 1 + + def test_reassigning_teams_replaces_the_previous_ones(self, session, teams): + job = Job(team_names=["team-a"]) + session.add(job) + session.commit() + + job.team_names = ["team-b"] + session.commit() + + assert job.team_names == ["team-b"] + assert session.scalars(select(JobTeam.team_name)).all() == ["team-b"] + + def test_teams_survive_the_heartbeat_merge(self, session, teams): + """``prepare_for_execution`` detaches the Job, so the heartbeat merge must not duplicate links.""" + job = Job(team_names=teams) + job.prepare_for_execution() + job.heartbeat(heartbeat_callback=Mock()) + + assert session.scalar(select(func.count()).select_from(JobTeam)) == 2 + assert session.get(Job, job.id).team_names == ["team-a", "team-b"] + + def test_deleting_a_job_removes_its_team_links(self, session, teams): + job = Job(team_names=teams) + session.add(job) + session.commit() + + session.delete(job) + session.commit() + + assert session.scalar(select(func.count()).select_from(JobTeam)) == 0 diff --git a/airflow-core/tests/unit/jobs/test_triggerer_job.py b/airflow-core/tests/unit/jobs/test_triggerer_job.py index 79b1755cd4fd9..7b4a45e9ea722 100644 --- a/airflow-core/tests/unit/jobs/test_triggerer_job.py +++ b/airflow-core/tests/unit/jobs/test_triggerer_job.py @@ -272,7 +272,7 @@ def test_execute_passes_job_team_name_to_supervisor(mock_supervisor_start, team_ mock_supervisor._exit_code = 0 mock_supervisor_start.return_value = mock_supervisor - job = Job(team_name=team_name) + job = Job(team_names=[team_name] if team_name else []) job_runner = TriggererJobRunner(job) with ( patch.object(job_runner, "register_signals"), diff --git a/airflow-core/tests/unit/utils/test_db_cleanup.py b/airflow-core/tests/unit/utils/test_db_cleanup.py index c7104d954d43a..375461b6848ac 100644 --- a/airflow-core/tests/unit/utils/test_db_cleanup.py +++ b/airflow-core/tests/unit/utils/test_db_cleanup.py @@ -1097,6 +1097,7 @@ def test_no_models_missing(self): "deadline_alert", # cascade from serialized_dag, which cascades from dag_version "hitl_detail", # cascade from task_instance "hitl_detail_history", # cascade from task_instance_history + "job_team", # cascade from job "task_inlet_asset_reference", # cascade from dag } diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 72cecf0550959..8938ad8d0534b 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -951,7 +951,7 @@ class JobResponse(BaseModel): executor_class: Annotated[str | None, Field(title="Executor Class")] hostname: Annotated[str | None, Field(title="Hostname")] unixname: Annotated[str | None, Field(title="Unixname")] - team_name: Annotated[str | None, Field(title="Team Name")] = None + team_names: Annotated[list[str] | None, Field(title="Team Names")] = None bundle_names: Annotated[list[str] | None, Field(title="Bundle Names")] = None dag_display_name: Annotated[str | None, Field(title="Dag Display Name")] = None @@ -1363,7 +1363,7 @@ class TriggererInstanceInfoResponse(BaseModel): hostname: Annotated[str | None, Field(title="Hostname")] latest_triggerer_heartbeat: Annotated[str | None, Field(title="Latest Triggerer Heartbeat")] - team_name: Annotated[str | None, Field(title="Team Name")] + team_names: Annotated[list[str], Field(title="Team Names")] class UpdateHITLDetailPayload(BaseModel): diff --git a/devel-common/src/tests_common/test_utils/db.py b/devel-common/src/tests_common/test_utils/db.py index 1525625375780..bfa3b067fc880 100644 --- a/devel-common/src/tests_common/test_utils/db.py +++ b/devel-common/src/tests_common/test_utils/db.py @@ -69,6 +69,7 @@ AIRFLOW_V_3_1_PLUS, AIRFLOW_V_3_2_PLUS, AIRFLOW_V_3_3_PLUS, + AIRFLOW_V_3_4_PLUS, ) log = logging.getLogger(__name__) @@ -470,6 +471,12 @@ def clear_db_logs(): @_retry_db def clear_db_jobs(): with create_session() as session: + if AIRFLOW_V_3_4_PLUS: + from airflow.models.team import JobTeam + + # SQLite does not enforce the ON DELETE CASCADE, and it reuses job ids, so leftover + # job_team rows would silently re-attach to the next job created. + session.execute(delete(JobTeam)) session.execute(delete(Job)) diff --git a/uv.lock b/uv.lock index 1e0292121f234..51fc8c66ab9db 100644 --- a/uv.lock +++ b/uv.lock @@ -29,148 +29,148 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P4D" [options.exclude-newer-package] -apache-airflow = false -apache-airflow-breeze = false -apache-airflow-core = false -apache-airflow-ctl = false -apache-airflow-ctl-tests = false -apache-airflow-dev = false -apache-airflow-devel-common = false -apache-airflow-docker-stack = false +apache-airflow-providers-informatica = false +apache-airflow-providers-clickhousedb = false +apache-airflow-providers-amazon = false +apache-airflow-providers-elasticsearch = false +apache-airflow-providers-duckdb = false apache-airflow-docker-tests = false -apache-airflow-e2e-tests = false -apache-airflow-helm-chart = false -apache-airflow-kubernetes-tests = false -apache-airflow-mypy = false +apache-airflow-providers-microsoft-winrm = false +typesafe-sdk = "2026-09-19T00:00:00Z" apache-airflow-providers = false -apache-airflow-providers-airbyte = false -apache-airflow-providers-akeyless = false -apache-airflow-providers-alibaba = false -apache-airflow-providers-amazon = false -apache-airflow-providers-anthropic = false -apache-airflow-providers-apache-arrow = false -apache-airflow-providers-apache-beam = false -apache-airflow-providers-apache-cassandra = false +apache-airflow-providers-fab = false +apache-airflow-providers-openlineage = false +apache-airflow-providers-sftp = false +apache-airflow-e2e-tests = false +apache-airflow-shared-logging = false +apache-airflow-providers-common-dataquality = false apache-airflow-providers-apache-drill = false -apache-airflow-providers-apache-druid = false -apache-airflow-providers-apache-flink = false -apache-airflow-providers-apache-hdfs = false -apache-airflow-providers-apache-hive = false -apache-airflow-providers-apache-iceberg = false -apache-airflow-providers-apache-impala = false -apache-airflow-providers-apache-kafka = false -apache-airflow-providers-apache-kylin = false -apache-airflow-providers-apache-livy = false -apache-airflow-providers-apache-pig = false -apache-airflow-providers-apache-pinot = false -apache-airflow-providers-apache-spark = false -apache-airflow-providers-apache-tinkerpop = false -apache-airflow-providers-apprise = false +apache-airflow-providers-pgvector = false +apache-airflow-providers-imap = false +apache-airflow-providers-qdrant = false +apache-airflow-providers-edge3 = false +apache-airflow-providers-neo4j = false +apache-airflow-providers-discord = false +apache-airflow-providers-opensearch = false +apache-airflow-providers-samba = false apache-airflow-providers-arangodb = false -apache-airflow-providers-asana = false -apache-airflow-providers-atlassian-jira = false +apache-airflow-providers-apache-spark = false +apache-airflow-providers-ftp = false +apache-airflow-providers-jenkins = false +apache-airflow-shared-listeners = false +apache-airflow-shared-providers-discovery = false +apache-airflow-providers-telegram = false apache-airflow-providers-celery = false -apache-airflow-providers-clickhousedb = false -apache-airflow-providers-cloudant = false -apache-airflow-providers-cncf-kubernetes = false -apache-airflow-providers-cohere = false +apache-airflow-providers-docker = false +apache-airflow-providers-sendgrid = false apache-airflow-providers-common-ai = false -apache-airflow-providers-common-compat = false -apache-airflow-providers-common-dataquality = false -apache-airflow-providers-common-io = false -apache-airflow-providers-common-messaging = false -apache-airflow-providers-common-sql = false -apache-airflow-providers-databricks = false -apache-airflow-providers-datadog = false +apache-airflow = false +apache-airflow-shared-observability = false +apache-airflow-dev = false apache-airflow-providers-dbt-cloud = false -apache-airflow-providers-dingding = false -apache-airflow-providers-discord = false -apache-airflow-providers-docker = false -apache-airflow-providers-duckdb = false -apache-airflow-providers-edge3 = false -apache-airflow-providers-elasticsearch = false -apache-airflow-providers-exasol = false -apache-airflow-providers-fab = false -apache-airflow-providers-facebook = false -apache-airflow-providers-ftp = false -apache-airflow-providers-git = false -apache-airflow-providers-github = false -apache-airflow-providers-google = false -apache-airflow-providers-grpc = false -apache-airflow-providers-hashicorp = false -apache-airflow-providers-http = false -apache-airflow-providers-ibm-db2 = false -apache-airflow-providers-ibm-mq = false -apache-airflow-providers-imap = false -apache-airflow-providers-influxdb = false -apache-airflow-providers-informatica = false -apache-airflow-providers-jdbc = false -apache-airflow-providers-jenkins = false -apache-airflow-providers-keycloak = false -apache-airflow-providers-microsoft-azure = false -apache-airflow-providers-microsoft-mssql = false -apache-airflow-providers-microsoft-psrp = false -apache-airflow-providers-microsoft-winrm = false -apache-airflow-providers-mongo = false -apache-airflow-providers-mysql = false -apache-airflow-providers-neo4j = false -apache-airflow-providers-odbc = false -apache-airflow-providers-openai = false apache-airflow-providers-openfaas = false -apache-airflow-providers-openlineage = false -apache-airflow-providers-opensearch = false -apache-airflow-providers-opsgenie = false +apache-airflow-devel-common = false +apache-airflow-providers-apache-cassandra = false +apache-airflow-providers-asana = false apache-airflow-providers-oracle = false -apache-airflow-providers-pagerduty = false -apache-airflow-providers-papermill = false -apache-airflow-providers-pgvector = false +apache-airflow-providers-mysql = false +apache-airflow-providers-alibaba = false +apache-airflow-providers-microsoft-mssql = false +apache-airflow-providers-teradata = false +apache-airflow-providers-jdbc = false +apache-airflow-helm-chart = false +apache-airflow-providers-anthropic = false +apache-airflow-providers-common-io = false +apache-airflow-providers-cohere = false apache-airflow-providers-pinecone = false -apache-airflow-providers-postgres = false -apache-airflow-providers-presto = false -apache-airflow-providers-qdrant = false +apache-airflow-providers-segment = false apache-airflow-providers-redis = false +apache-airflow-shared-dagnode = false +apache-airflow-providers-apache-pinot = false +apache-airflow-providers-weaviate = false +google-cloud-aiplatform = "2026-08-13T00:00:00Z" +apache-airflow-providers-akeyless = false apache-airflow-providers-salesforce = false -apache-airflow-providers-samba = false -apache-airflow-providers-segment = false -apache-airflow-providers-sendgrid = false -apache-airflow-providers-sftp = false -apache-airflow-providers-singularity = false -apache-airflow-providers-slack = false -apache-airflow-providers-smtp = false -apache-airflow-providers-snowflake = false -apache-airflow-providers-sqlite = false apache-airflow-providers-ssh = false -apache-airflow-providers-standard = false -apache-airflow-providers-tableau = false -apache-airflow-providers-telegram = false -apache-airflow-providers-teradata = false -apache-airflow-providers-trino = false +apache-airflow-providers-papermill = false +apache-airflow-providers-google = false +pydantic-ai-skills = "2026-07-16T00:00:00Z" +apache-airflow-providers-apache-beam = false +apache-airflow-providers-microsoft-psrp = false apache-airflow-providers-vertica = false +apache-airflow-providers-apache-hdfs = false +apache-airflow-shared-template-rendering = false +apache-airflow-mypy = false +apache-airflow-providers-http = false +apache-airflow-providers-slack = false apache-airflow-providers-vespa = false -apache-airflow-providers-weaviate = false +apache-airflow-providers-databricks = false +apache-airflow-shared-state = false +apache-airflow-docker-stack = false +apache-airflow-providers-sqlite = false +apache-airflow-providers-ibm-mq = false +apache-airflow-shared-module-loading = false apache-airflow-providers-yandex = false -apache-airflow-providers-ydb = false -apache-airflow-providers-zendesk = false +apache-airflow-shared-serialization = false apache-airflow-scripts = false +apache-airflow-providers-exasol = false +apache-airflow-providers-mongo = false +apache-airflow-providers-apache-arrow = false +apache-airflow-providers-apprise = false +apache-airflow-providers-apache-impala = false +apache-airflow-ctl = false +apache-airflow-providers-github = false +apache-airflow-providers-snowflake = false +apache-airflow-providers-zendesk = false +apache-airflow-providers-presto = false +apache-airflow-providers-airbyte = false +apache-airflow-providers-apache-hive = false +apache-airflow-kubernetes-tests = false +apache-airflow-providers-grpc = false +apache-airflow-providers-apache-druid = false +apache-airflow-providers-apache-flink = false +apache-airflow-providers-cncf-kubernetes = false +apache-airflow-providers-apache-pig = false +apache-airflow-providers-apache-tinkerpop = false +apache-airflow-shared-timezones = false +apache-airflow-providers-apache-iceberg = false +apache-airflow-breeze = false +apache-airflow-providers-opsgenie = false +apache-airflow-providers-apache-livy = false +apache-airflow-core = false +apache-airflow-providers-hashicorp = false +apache-airflow-providers-pagerduty = false +apache-airflow-providers-datadog = false +apache-airflow-providers-apache-kafka = false +apache-airflow-providers-influxdb = false +apache-airflow-providers-keycloak = false +apache-airflow-providers-trino = false +apache-airflow-providers-common-messaging = false +apache-airflow-providers-standard = false +apache-airflow-providers-singularity = false +apache-airflow-providers-common-compat = false +apache-airflow-ctl-tests = false +apache-airflow-providers-tableau = false +apache-airflow-providers-ibm-db2 = false +apache-airflow-providers-common-sql = false apache-airflow-shared-configuration = false -apache-airflow-shared-dagnode = false -apache-airflow-shared-listeners = false -apache-airflow-shared-logging = false -apache-airflow-shared-module-loading = false -apache-airflow-shared-observability = false +apache-airflow-providers-facebook = false +apache-airflow-providers-ydb = false +apache-airflow-providers-microsoft-azure = false apache-airflow-shared-plugins-manager = false -apache-airflow-shared-providers-discovery = false apache-airflow-shared-secrets-backend = false apache-airflow-shared-secrets-masker = false -apache-airflow-shared-serialization = false -apache-airflow-shared-state = false -apache-airflow-shared-template-rendering = false -apache-airflow-shared-timezones = false +apache-airflow-providers-git = false apache-airflow-task-sdk = false +apache-airflow-providers-atlassian-jira = false +apache-airflow-providers-odbc = false +apache-airflow-providers-postgres = false +apache-airflow-providers-openai = false apache-airflow-task-sdk-integration-tests = false -google-cloud-aiplatform = "2026-08-13T00:00:00Z" -pydantic-ai-skills = "2026-07-16T00:00:00Z" -typesafe-sdk = "2026-09-19T00:00:00Z" +apache-airflow-providers-smtp = false +apache-airflow-providers-dingding = false +apache-airflow-providers-cloudant = false +apache-airflow-providers-apache-kylin = false [manifest] members = [ @@ -523,7 +523,7 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "caio", version = "0.9.25", source = { registry = "https://pypi.org/simple" } }, + { name = "caio", version = "0.9.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } wheels = [ @@ -555,7 +555,7 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "caio", version = "0.12.4", source = { registry = "https://pypi.org/simple" } }, + { name = "caio", version = "0.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/31/edb06aabd8f8f0b56d659f30800795f40b93cba96be946ce179f6931e3a5/aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41", size = 21600, upload-time = "2026-08-04T22:59:27.171Z" } wheels = [ @@ -722,7 +722,7 @@ name = "aiohttp-cors" version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, + { name = "aiohttp", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } wheels = [ @@ -11014,8 +11014,8 @@ name = "cassandra-driver" version = "3.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "geomet" }, + { name = "deprecated", marker = "python_full_version < '3.14'" }, + { name = "geomet", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/ed/4e16210e194660f107929ee494f1cf18557252655067d9b39029c241be2d/cassandra_driver-3.30.1.tar.gz", hash = "sha256:0c6a3e1428f7c6a9aa6c944b9c47a37cd2cdbeb5b5a82d42c33afdd56f14e398", size = 289233, upload-time = "2026-07-06T20:14:31.37Z" } wheels = [ @@ -11737,7 +11737,7 @@ name = "colorful" version = "0.5.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.15' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/31/109ef4bedeb32b4202e02ddb133162457adc4eb890a9ed9c05c9dd126ed0/colorful-0.5.8.tar.gz", hash = "sha256:bb16502b198be2f1c42ba3c52c703d5f651d826076817185f0294c1a549a7445", size = 209361, upload-time = "2025-10-29T11:53:21.663Z" } wheels = [ @@ -13302,7 +13302,7 @@ name = "geomet" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, + { name = "click", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/8c/dde022aa6747b114f6b14a7392871275dea8867e2bd26cddb80cc6d66620/geomet-1.1.0.tar.gz", hash = "sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605", size = 28732, upload-time = "2023-11-14T15:43:36.764Z" } wheels = [ @@ -13485,10 +13485,10 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "fasteners" }, - { name = "httplib2" }, - { name = "oauth2client" }, - { name = "six" }, + { name = "fasteners", marker = "python_full_version < '3.13'" }, + { name = "httplib2", marker = "python_full_version < '3.13'" }, + { name = "oauth2client", marker = "python_full_version < '3.13'" }, + { name = "six", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/da/aefc4cf4c168b5d875344cd9dddc77e3a2d11986b630251af5ce47dd2843/google-apitools-0.5.31.tar.gz", hash = "sha256:4af0dd6dd4582810690251f0b57a97c1873dadfda54c5bc195844c8907624170", size = 173463, upload-time = "2020-05-14T17:25:19.126Z" } @@ -13509,10 +13509,10 @@ resolution-markers = [ "(python_full_version == '3.13.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "fasteners" }, - { name = "httplib2" }, - { name = "oauth2client" }, - { name = "six" }, + { name = "fasteners", marker = "python_full_version >= '3.13'" }, + { name = "httplib2", marker = "python_full_version >= '3.13'" }, + { name = "oauth2client", marker = "python_full_version >= '3.13'" }, + { name = "six", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/e4/15fbddfffa968e10c602a30b7ca4703f3e41d2dbdef011e98eee85575133/google-apitools-0.5.35.tar.gz", hash = "sha256:911bc3698686c74187414c610ae30bd6e3c0a7404178fc6479ead6c420d2dd94", size = 531707, upload-time = "2025-09-24T20:22:51.11Z" } wheels = [ @@ -14838,7 +14838,7 @@ name = "gssapi" version = "1.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decorator" }, + { name = "decorator", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6b/fb/9492eb37b5786369b7e96fff2cf4040713acb33b96124813e058cbbd4356/gssapi-1.12.0.tar.gz", hash = "sha256:0b48f17296f869db89221c3bde463fdff35152a4cb99ae4aaa42b0b64333fc6c", size = 95321, upload-time = "2026-08-26T03:00:50.067Z" } wheels = [ @@ -15018,8 +15018,8 @@ name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ @@ -15666,17 +15666,17 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "exceptiongroup" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -15708,17 +15708,17 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/32/99451b1283ec5d92ad77073f12e1c667dc10775384d8f15c2914207149dd/ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529", size = 4539289, upload-time = "2026-09-01T08:29:32.6Z" } wheels = [ @@ -15730,7 +15730,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -16729,18 +16729,18 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "click", marker = "python_full_version < '3.14'" }, + { name = "fastuuid", marker = "python_full_version < '3.14'" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.14'" }, + { name = "jinja2", marker = "python_full_version < '3.14'" }, + { name = "jsonschema", marker = "python_full_version < '3.14'" }, + { name = "openai", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "python-dotenv", marker = "python_full_version < '3.14'" }, + { name = "tiktoken", marker = "python_full_version < '3.14'" }, + { name = "tokenizers", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/59/364fa090acef716effd88e6432d3442982b1f965b2d291f1528db0e6aabb/litellm-1.85.7.tar.gz", hash = "sha256:461cc5d5a6c7d5086b67f06f1d5a00c40d6fb0240099764cb040dc4a28452938", size = 15363830, upload-time = "2026-06-24T04:54:55.115Z" } wheels = [ @@ -16760,19 +16760,19 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, + { name = "aiohttp", marker = "python_full_version >= '3.14'" }, + { name = "click", marker = "python_full_version >= '3.14'" }, + { name = "fastuuid", marker = "python_full_version >= '3.14'" }, + { name = "httpx", marker = "python_full_version >= '3.14'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.14'" }, + { name = "jinja2", marker = "python_full_version >= '3.14'" }, + { name = "jsonschema", marker = "python_full_version >= '3.14'" }, + { name = "openai", marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.14'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.14'" }, + { name = "tiktoken", marker = "python_full_version >= '3.14'" }, + { name = "tokenizers", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/fe/d03be8a6be6914fabacef29457a2b3d02128c52c448cc6f3019b8e63488b/litellm-1.96.2.tar.gz", hash = "sha256:80d477ae092b05ce023b5084542cb3cb75999b52b1dd2e4d834fb348effc9399", size = 17682558, upload-time = "2026-08-11T21:12:24.117Z" } wheels = [ @@ -18883,9 +18883,9 @@ name = "opencensus" version = "0.11.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "google-api-core" }, - { name = "opencensus-context" }, - { name = "six" }, + { name = "google-api-core", marker = "python_full_version < '3.15'" }, + { name = "opencensus-context", marker = "python_full_version < '3.15'" }, + { name = "six", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/a7/a46dcffa1b63084f9f17fe3c8cb20724c4c8f91009fd0b2cfdb27d5d2b35/opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2", size = 64966, upload-time = "2024-01-03T18:04:07.085Z" } wheels = [ @@ -19400,10 +19400,10 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -19458,9 +19458,9 @@ wheels = [ [package.optional-dependencies] sql-other = [ - { name = "adbc-driver-postgresql" }, - { name = "adbc-driver-sqlite" }, - { name = "sqlalchemy" }, + { name = "adbc-driver-postgresql", marker = "python_full_version < '3.11'" }, + { name = "adbc-driver-sqlite", marker = "python_full_version < '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version < '3.11'" }, ] [[package]] @@ -19488,9 +19488,9 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } wheels = [ @@ -19539,9 +19539,9 @@ wheels = [ [package.optional-dependencies] sql-other = [ - { name = "adbc-driver-postgresql" }, - { name = "adbc-driver-sqlite" }, - { name = "sqlalchemy" }, + { name = "adbc-driver-postgresql", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "adbc-driver-sqlite", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] [[package]] @@ -22141,9 +22141,9 @@ name = "python3-saml" version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "isodate" }, - { name = "lxml" }, - { name = "xmlsec" }, + { name = "isodate", marker = "python_full_version < '3.13'" }, + { name = "lxml", marker = "python_full_version < '3.13'" }, + { name = "xmlsec", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/98/6e0268c3a9893af3d4c5cf670183e0314cd6b5cb034a612d6a7cc5060df8/python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133", size = 83468, upload-time = "2023-10-09T10:37:43.128Z" } wheels = [ @@ -22462,14 +22462,14 @@ name = "ray" version = "2.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "filelock" }, - { name = "jsonschema" }, - { name = "msgpack" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "requests" }, + { name = "click", marker = "python_full_version < '3.15'" }, + { name = "filelock", marker = "python_full_version < '3.15'" }, + { name = "jsonschema", marker = "python_full_version < '3.15'" }, + { name = "msgpack", marker = "python_full_version < '3.15'" }, + { name = "packaging", marker = "python_full_version < '3.15'" }, + { name = "protobuf", marker = "python_full_version < '3.15'" }, + { name = "pyyaml", marker = "python_full_version < '3.15'" }, + { name = "requests", marker = "python_full_version < '3.15'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/2f/41/760f9a076085d043d9fbb8f840436e1d21e47682c976d3620efbf02a3b9e/ray-2.58.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a2235a5a17e865be053e8aff6d9e9e36d1edb45ad8162f57f7cdfd0202a5a397", size = 67217855, upload-time = "2026-08-23T04:45:46.075Z" }, @@ -22496,20 +22496,20 @@ wheels = [ [package.optional-dependencies] default = [ - { name = "aiohttp" }, - { name = "aiohttp-cors" }, - { name = "colorful" }, - { name = "grpcio" }, - { name = "opencensus" }, - { name = "opentelemetry-exporter-prometheus" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "prometheus-client" }, - { name = "py-spy" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "smart-open" }, - { name = "virtualenv" }, + { name = "aiohttp", marker = "python_full_version < '3.15'" }, + { name = "aiohttp-cors", marker = "python_full_version < '3.15'" }, + { name = "colorful", marker = "python_full_version < '3.15'" }, + { name = "grpcio", marker = "python_full_version < '3.15'" }, + { name = "opencensus", marker = "python_full_version < '3.15'" }, + { name = "opentelemetry-exporter-prometheus", marker = "python_full_version < '3.15'" }, + { name = "opentelemetry-proto", marker = "python_full_version < '3.15'" }, + { name = "opentelemetry-sdk", marker = "python_full_version < '3.15'" }, + { name = "prometheus-client", marker = "python_full_version < '3.15'" }, + { name = "py-spy", marker = "python_full_version < '3.15'" }, + { name = "pydantic", marker = "python_full_version < '3.15'" }, + { name = "requests", marker = "python_full_version < '3.15'" }, + { name = "smart-open", marker = "python_full_version < '3.15'" }, + { name = "virtualenv", marker = "python_full_version < '3.15'" }, ] [[package]] @@ -23307,10 +23307,10 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680, upload-time = "2024-09-11T15:50:10.957Z" } wheels = [ @@ -23361,12 +23361,12 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "threadpoolctl" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } wheels = [ @@ -23423,7 +23423,7 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -23485,7 +23485,7 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -23572,7 +23572,7 @@ resolution-markers = [ "(python_full_version == '3.12.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } wheels = [ @@ -23678,8 +23678,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jeepney", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -23878,7 +23878,7 @@ name = "smart-open" version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wrapt" }, + { name = "wrapt", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/53/9c513747547fd595d5c143259129ea8b9c3ea2f6b7bb9dcea2b1966ded3c/smart_open-8.0.1.tar.gz", hash = "sha256:18b1c4496003c6902be17c15f032b5c319f307c89c6ae9e6b028b508bed8b2cf", size = 61882, upload-time = "2026-07-15T13:56:10.492Z" } wheels = [ @@ -23992,15 +23992,15 @@ name = "snowflake-snowpark-python" version = "1.55.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cloudpickle" }, - { name = "protobuf" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "setuptools" }, - { name = "snowflake-connector-python" }, - { name = "typing-extensions" }, - { name = "tzlocal" }, - { name = "wheel" }, + { name = "cloudpickle", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "python-dateutil", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "setuptools", marker = "python_full_version < '3.14'" }, + { name = "snowflake-connector-python", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "tzlocal", marker = "python_full_version < '3.14'" }, + { name = "wheel", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/35/84d01dd46679fd2324fca363e59599f52af32e5ee9b9fa68da17926f6690/snowflake_snowpark_python-1.55.0.tar.gz", hash = "sha256:2accc4e50f44635b8c9d5ed52f26a4d9885d00f17fcc16eee532e8d9d274c627", size = 1799269, upload-time = "2026-09-10T18:57:10.805Z" } wheels = [ @@ -24047,23 +24047,23 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, - { name = "tomli" }, + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -24081,23 +24081,23 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -24125,23 +24125,23 @@ resolution-markers = [ "(python_full_version == '3.12.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -24207,12 +24207,12 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "colorama" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "watchfiles" }, - { name = "websockets" }, + { name = "colorama", marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "starlette", marker = "python_full_version < '3.11'" }, + { name = "uvicorn", marker = "python_full_version < '3.11'" }, + { name = "watchfiles", marker = "python_full_version < '3.11'" }, + { name = "websockets", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } wheels = [ @@ -24244,13 +24244,13 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "colorama" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "watchfiles" }, - { name = "websockets" }, + { name = "starlette", marker = "python_full_version >= '3.11'" }, + { name = "uvicorn", marker = "python_full_version >= '3.11'" }, + { name = "watchfiles", marker = "python_full_version >= '3.11'" }, + { name = "websockets", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } wheels = [ @@ -24280,7 +24280,7 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'arm64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } wheels = [ @@ -24312,7 +24312,7 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", ] dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } @@ -26390,7 +26390,7 @@ name = "xmlsec" version = "1.3.17" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lxml" }, + { name = "lxml", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/14/538b75379e6ab8f688f14d8663e2ab138d9c778bac4999d155b5f33c71c1/xmlsec-1.3.17.tar.gz", hash = "sha256:f3fac9ae679f66585925cc00c5f6839ae36c1d03157619571dee18acc05b9c01", size = 115637, upload-time = "2025-11-11T16:20:46.019Z" } wheels = [