This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 02dff07c1ff Allow a Job to be associated with several teams (#73561)
02dff07c1ff is described below
commit 02dff07c1ffcae606b08dcf271f29e1cb16bef07
Author: Vincent <[email protected]>
AuthorDate: Thu Sep 24 11:53:12 2026 -0400
Allow a Job to be associated with several teams (#73561)
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.
---
.../logging-monitoring/check-health.rst | 4 +-
airflow-core/docs/migrations-ref.rst | 5 +-
.../src/airflow/api/common/airflow_health.py | 10 +-
.../airflow/api_fastapi/core_api/datamodels/job.py | 2 +-
.../api_fastapi/core_api/datamodels/monitor.py | 2 +-
.../api_fastapi/core_api/openapi/_private_ui.yaml | 10 +-
.../core_api/openapi/v2-rest-api-generated.yaml | 22 +-
.../src/airflow/cli/commands/triggerer_command.py | 2 +-
airflow-core/src/airflow/jobs/job.py | 23 +-
.../src/airflow/jobs/triggerer_job_runner.py | 5 +-
.../0137_3_4_0_add_job_team_association_table.py | 81 +++
airflow-core/src/airflow/models/team.py | 24 +-
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 34 +-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 4 +-
.../ui/src/pages/Dashboard/Health/Health.test.tsx | 9 +-
.../ui/src/pages/Dashboard/Health/Health.tsx | 2 +-
.../src/pages/Dashboard/Health/HealthInstances.tsx | 14 +-
airflow-core/src/airflow/utils/db.py | 2 +-
.../tests/unit/api/common/test_airflow_health.py | 28 +-
.../api_fastapi/core_api/routes/public/test_job.py | 26 +-
.../core_api/routes/public/test_monitor.py | 6 +-
.../core_api/routes/public/test_task_instances.py | 2 +-
.../unit/cli/commands/test_triggerer_command.py | 14 +-
airflow-core/tests/unit/jobs/test_base_job.py | 67 +++
airflow-core/tests/unit/jobs/test_triggerer_job.py | 2 +-
airflow-core/tests/unit/utils/test_db_cleanup.py | 1 +
.../src/airflowctl/api/datamodels/generated.py | 4 +-
devel-common/src/tests_common/test_utils/db.py | 7 +
uv.lock | 632 ++++++++++-----------
29 files changed, 629 insertions(+), 415 deletions(-)
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 878f9496867..c259a68d3b3 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 009cb049e48..942f3e6ad86 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 def85190e44..b073ede5f8a 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 8e316d7b2e3..9164ad4e03a 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 442a5e9f44a..4af62ae387b 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 7af3e1262f4..24938a8f765 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 732ca53e138..49694cd5d03 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 87200ff57be..3f881e35c51 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 fa59a440abc..d19dff203f4 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.configuration import conf
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 243b8f154d7..ede83fe6c39 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 @@ class TriggererJobRunner(BaseJobRunner, LoggingMixin):
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 00000000000..49af0e10b25
--- /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 fab0e272409..9045e97e0da 100644
--- a/airflow-core/src/airflow/models/team.py
+++ b/airflow-core/src/airflow/models/team.py
@@ -20,7 +20,7 @@ from __future__ import annotations
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 @@ dag_bundle_team_association_table = Table(
)
+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 b9a58b37fd6..71d21c125c5 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 5972b95c458..776009c9afc 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 c4c8fda2599..961222ae932 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 91b68c949bf..aef443c9f7c 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<TriggererInstanceInfoResponse> | 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<DagProcessorInstanceInfoResponse> | 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 537b23b799a..afbea263a98 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<string> | null;
readonly hostname: string | null;
readonly latestHeartbeat: string | null;
- readonly teamName?: string | null;
+ readonly teamNames?: Array<string> | 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) => {
</Table.Cell>
{showTeam ? (
<Table.Cell>
- <TeamName teamName={instance.teamName} />
+ <HStack gap={2} wrap="wrap">
+ {instance.teamNames?.map((teamName) => (
+ <TeamName key={teamName} teamName={teamName} />
+ ))}
+ </HStack>
</Table.Cell>
) : undefined}
{showBundles ? <Table.Cell>{instance.bundleNames?.join(",
")}</Table.Cell> : undefined}
diff --git a/airflow-core/src/airflow/utils/db.py
b/airflow-core/src/airflow/utils/db.py
index a116050d1ba..a76937b6b1d 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,7 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
"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 cb10bcb79ac..9affe9faab6 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 @@ ALIVE_TRIGGERER_JOB_MOCK = _mock_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 @@ class TestAirflowHealthFromDb:
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 @@ class TestAirflowHealthFromDb:
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 @@ class TestAirflowHealthFromDb:
{
"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 cb4c7254492..f969d48b305 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 __future__ import annotations
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 @@ class TestGetJobs(TestJobEndpoint):
):
# 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 @@ class TestGetJobs(TestJobEndpoint):
"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 @@ class TestGetJobs(TestJobEndpoint):
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 @@ class TestGetJobs(TestJobEndpoint):
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 e696ec43811..e1325de1d9c 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 @@ class TestGetHealth(TestMonitorEndpoint):
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 @@ class TestGetHealth(TestMonitorEndpoint):
"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 @@ class TestGetHealth(TestMonitorEndpoint):
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 53f5bc85aa3..7d14b4a7c05 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 @@ class TestGetTaskInstance(TestTaskInstanceEndpoint):
"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 f88dc90ea5c..449ed61626c 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 @@ class TestTriggererCommand:
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 @@ class TestTriggererCommand:
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 @@ class TestTriggererCommand:
@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 211e4128d01..eda85fa270b 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 @@ import sys
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 @@ class TestJob:
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 79b1755cd4f..7b4a45e9ea7 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 c7104d954d4..375461b6848 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 @@ class TestDBCleanup:
"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 72cecf05509..8938ad8d053 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 15256253757..bfa3b067fc8 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 @@ from tests_common.test_utils.version_compat import (
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 1e0292121f2..51fc8c66ab9 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 = [