This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 f806187dc85 UI: Add team column and filter to the deadlines list
(#72060)
f806187dc85 is described below
commit f806187dc85343d82fa4ab477ae5e7c71c1c50f1
Author: Vincent <[email protected]>
AuthorDate: Mon Aug 31 16:25:01 2026 -0400
UI: Add team column and filter to the deadlines list (#72060)
* UI: Add team column and filter to the deadlines list
When multi-team mode is enabled, operators watching deadlines across teams
need
to see which team owns each deadline and to scope the list to a single team
when
triaging that team's misses.
The column and the filter render only when the `multi_team` configuration is
enabled, and no team data is loaded when it is off, so single-team
deployments
are unaffected.
* Make deadlines UI tests recover from leftover team rows
attach_dag_to_team commits its bundle and team before yielding, so a test
run
interrupted inside that context manager leaves the rows behind. On a
persistent
database every later run then failed with a duplicate-key error until the
rows
were deleted by hand.
---
.../api_fastapi/core_api/datamodels/ui/deadline.py | 1 +
.../api_fastapi/core_api/openapi/_private_ui.yaml | 13 ++++
.../api_fastapi/core_api/routes/ui/deadlines.py | 10 ++-
.../src/airflow/ui/openapi-gen/queries/common.ts | 5 +-
.../ui/openapi-gen/queries/ensureQueryData.ts | 6 +-
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 6 +-
.../src/airflow/ui/openapi-gen/queries/queries.ts | 6 +-
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 6 +-
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 11 +++
.../ui/openapi-gen/requests/services.gen.ts | 4 +-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 2 +
.../src/airflow/ui/src/pages/Deadlines/index.tsx | 23 +++++-
.../core_api/routes/public/test_dag_run.py | 71 ++-----------------
.../core_api/routes/public/test_task_instances.py | 82 +++-------------------
.../core_api/routes/ui/test_deadlines.py | 63 +++++++++++++++--
devel-common/src/tests_common/test_utils/team.py | 68 ++++++++++++++++++
16 files changed, 215 insertions(+), 162 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py
index 9c09176fea4..53b583d1f40 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py
@@ -38,6 +38,7 @@ class DeadlineResponse(BaseModel):
dag_run_id: str = Field(validation_alias=AliasPath("dagrun", "run_id"))
alert_id: UUID | None = Field(validation_alias="deadline_alert_id",
default=None)
alert_name: str | None =
Field(validation_alias=AliasPath("deadline_alert", "name"), default=None)
+ team_name: str | None = Field(validation_alias=AliasPath("dagrun",
"team_name"), default=None)
class DeadlineCollectionResponse(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 25b50d9e825..6ca0e3c24b4 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
@@ -1142,6 +1142,14 @@ paths:
format: date-time
- type: 'null'
title: Last Updated At Lt
+ - name: teams
+ in: query
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
+ title: Teams
responses:
'200':
description: Successful Response
@@ -3368,6 +3376,11 @@ components:
- type: string
- type: 'null'
title: Alert Name
+ team_name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Team Name
type: object
required:
- id
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py
index 4127e98104e..6a239540a75 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/deadlines.py
@@ -25,14 +25,17 @@ from sqlalchemy.orm import contains_eager, noload
from airflow.api_fastapi.auth.managers.models.resource_details import
DagAccessEntity
from airflow.api_fastapi.common.db.common import SessionDep, paginated_select
+from airflow.api_fastapi.common.db.dags import eager_load_teams
from airflow.api_fastapi.common.parameters import (
FilterParam,
QueryLimit,
QueryOffset,
RangeFilter,
SortParam,
+ _DagIdTeamsFilter,
datetime_range_filter_factory,
filter_param_factory,
+ teams_filter_factory,
)
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.datamodels.ui.deadline import (
@@ -93,6 +96,7 @@ def get_deadlines(
last_updated_at: Annotated[
RangeFilter, Depends(datetime_range_filter_factory("last_updated_at",
Deadline))
],
+ teams: Annotated[_DagIdTeamsFilter,
Depends(teams_filter_factory(DagRun.dag_id))],
) -> DeadlineCollectionResponse:
"""
Get deadlines for a Dag run.
@@ -105,7 +109,9 @@ def get_deadlines(
.join(Deadline.dagrun)
.outerjoin(Deadline.deadline_alert)
.options(
- contains_eager(Deadline.dagrun).options(noload(DagRun.deadlines)),
+ contains_eager(Deadline.dagrun).options(
+ noload(DagRun.deadlines), *eager_load_teams(DagRun.dag_model)
+ ),
contains_eager(Deadline.deadline_alert),
noload(Deadline.callback),
)
@@ -123,7 +129,7 @@ def get_deadlines(
deadlines_select, total_entries = paginated_select(
statement=query,
- filters=[readable_dag_runs_filter, missed, deadline_time,
last_updated_at],
+ filters=[readable_dag_runs_filter, missed, deadline_time,
last_updated_at, teams],
order_by=order_by,
offset=offset,
limit=limit,
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index e5c078e4a11..206e6c29c87 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -986,7 +986,7 @@ export const UseDashboardServiceDagStatsKeyFn = (queryKey?:
Array<unknown>) => [
export type DeadlinesServiceGetDeadlinesDefaultResponse =
Awaited<ReturnType<typeof DeadlinesService.getDeadlines>>;
export type DeadlinesServiceGetDeadlinesQueryResult<TData =
DeadlinesServiceGetDeadlinesDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useDeadlinesServiceGetDeadlinesKey =
"DeadlinesServiceGetDeadlines";
-export const UseDeadlinesServiceGetDeadlinesKeyFn = ({ dagId, dagRunId,
deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit,
missed, offset, orderBy }: {
+export const UseDeadlinesServiceGetDeadlinesKeyFn = ({ dagId, dagRunId,
deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit,
missed, offset, orderBy, teams }: {
dagId: string;
dagRunId: string;
deadlineTimeGt?: string;
@@ -1001,7 +1001,8 @@ export const UseDeadlinesServiceGetDeadlinesKeyFn = ({
dagId, dagRunId, deadline
missed?: boolean;
offset?: number;
orderBy?: string[];
-}, queryKey?: Array<unknown>) => [useDeadlinesServiceGetDeadlinesKey,
...(queryKey ?? [{ dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy }])];
+ teams?: string[];
+}, queryKey?: Array<unknown>) => [useDeadlinesServiceGetDeadlinesKey,
...(queryKey ?? [{ dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy, teams }])];
export type DeadlinesServiceGetDagDeadlineAlertsDefaultResponse =
Awaited<ReturnType<typeof DeadlinesService.getDagDeadlineAlerts>>;
export type DeadlinesServiceGetDagDeadlineAlertsQueryResult<TData =
DeadlinesServiceGetDagDeadlineAlertsDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useDeadlinesServiceGetDagDeadlineAlertsKey =
"DeadlinesServiceGetDagDeadlineAlerts";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 2da84b8596d..bd88502f1e1 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1912,10 +1912,11 @@ export const ensureUseDashboardServiceDagStatsData =
(queryClient: QueryClient)
* @param data.lastUpdatedAtGt
* @param data.lastUpdatedAtLte
* @param data.lastUpdatedAtLt
+* @param data.teams
* @returns DeadlineCollectionResponse Successful Response
* @throws ApiError
*/
-export const ensureUseDeadlinesServiceGetDeadlinesData = (queryClient:
QueryClient, { dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy }: {
+export const ensureUseDeadlinesServiceGetDeadlinesData = (queryClient:
QueryClient, { dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy, teams }: {
dagId: string;
dagRunId: string;
deadlineTimeGt?: string;
@@ -1930,7 +1931,8 @@ export const ensureUseDeadlinesServiceGetDeadlinesData =
(queryClient: QueryClie
missed?: boolean;
offset?: number;
orderBy?: string[];
-}) => queryClient.ensureQueryData({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }), queryFn: () => DeadlinesService.getDeadlines({ dagId, dagRunId,
deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit,
misse [...]
+ teams?: string[];
+}) => queryClient.ensureQueryData({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }), queryFn: () => DeadlinesService.getDeadlines({ dagId,
dagRunId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit
[...]
/**
* Get Dag Deadline Alerts
* Get all deadline alerts defined on a Dag.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 917cc830f04..8d28a3aa001 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1912,10 +1912,11 @@ export const prefetchUseDashboardServiceDagStats =
(queryClient: QueryClient) =>
* @param data.lastUpdatedAtGt
* @param data.lastUpdatedAtLte
* @param data.lastUpdatedAtLt
+* @param data.teams
* @returns DeadlineCollectionResponse Successful Response
* @throws ApiError
*/
-export const prefetchUseDeadlinesServiceGetDeadlines = (queryClient:
QueryClient, { dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy }: {
+export const prefetchUseDeadlinesServiceGetDeadlines = (queryClient:
QueryClient, { dagId, dagRunId, deadlineTimeGt, deadlineTimeGte,
deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt, lastUpdatedAtGte,
lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset, orderBy, teams }: {
dagId: string;
dagRunId: string;
deadlineTimeGt?: string;
@@ -1930,7 +1931,8 @@ export const prefetchUseDeadlinesServiceGetDeadlines =
(queryClient: QueryClient
missed?: boolean;
offset?: number;
orderBy?: string[];
-}) => queryClient.prefetchQuery({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }), queryFn: () => DeadlinesService.getDeadlines({ dagId, dagRunId,
deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit,
missed, [...]
+ teams?: string[];
+}) => queryClient.prefetchQuery({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }), queryFn: () => DeadlinesService.getDeadlines({ dagId,
dagRunId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte,
lastUpdatedAtGt, lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit,
[...]
/**
* Get Dag Deadline Alerts
* Get all deadline alerts defined on a Dag.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 3ea1aede3b3..9c7971fb2d6 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1912,10 +1912,11 @@ export const useDashboardServiceDagStats = <TData =
Common.DashboardServiceDagSt
* @param data.lastUpdatedAtGt
* @param data.lastUpdatedAtLte
* @param data.lastUpdatedAtLt
+* @param data.teams
* @returns DeadlineCollectionResponse Successful Response
* @throws ApiError
*/
-export const useDeadlinesServiceGetDeadlines = <TData =
Common.DeadlinesServiceGetDeadlinesDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }: {
+export const useDeadlinesServiceGetDeadlines = <TData =
Common.DeadlinesServiceGetDeadlinesDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }: {
dagId: string;
dagRunId: string;
deadlineTimeGt?: string;
@@ -1930,7 +1931,8 @@ export const useDeadlinesServiceGetDeadlines = <TData =
Common.DeadlinesServiceG
missed?: boolean;
offset?: number;
orderBy?: string[];
-}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }, queryKey), queryFn: () => DeadlinesService.getDeadlines({ dagId,
dagRunId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, [...]
+ teams?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }, queryKey), queryFn: () => DeadlinesService.getDeadlines({
dagId, dagRunId, deadlineTimeGt, deadlineTimeGte, deadline [...]
/**
* Get Dag Deadline Alerts
* Get all deadline alerts defined on a Dag.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index d9aa328bcde..6fd74cd8483 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1912,10 +1912,11 @@ export const useDashboardServiceDagStatsSuspense =
<TData = Common.DashboardServ
* @param data.lastUpdatedAtGt
* @param data.lastUpdatedAtLte
* @param data.lastUpdatedAtLt
+* @param data.teams
* @returns DeadlineCollectionResponse Successful Response
* @throws ApiError
*/
-export const useDeadlinesServiceGetDeadlinesSuspense = <TData =
Common.DeadlinesServiceGetDeadlinesDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }: {
+export const useDeadlinesServiceGetDeadlinesSuspense = <TData =
Common.DeadlinesServiceGetDeadlinesDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }: {
dagId: string;
dagRunId: string;
deadlineTimeGt?: string;
@@ -1930,7 +1931,8 @@ export const useDeadlinesServiceGetDeadlinesSuspense =
<TData = Common.Deadlines
missed?: boolean;
offset?: number;
orderBy?: string[];
-}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy }, queryKey), queryFn: () => DeadlinesService.getDeadlines({ dagId,
dagRunId, deadlineTimeGt, deadlineTimeGte, deadlin [...]
+ teams?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseDeadlinesServiceGetDeadlinesKeyFn({ dagId, dagRunId, deadlineTimeGt,
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, lastUpdatedAtGt,
lastUpdatedAtGte, lastUpdatedAtLt, lastUpdatedAtLte, limit, missed, offset,
orderBy, teams }, queryKey), queryFn: () => DeadlinesService.getDeadlines({
dagId, dagRunId, deadlineTimeGt, deadlineTimeGte, [...]
/**
* Get Dag Deadline Alerts
* Get all deadline alerts defined on a Dag.
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 09e4248333c..3198d4162e7 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
@@ -10205,6 +10205,17 @@ export const $DeadlineResponse = {
}
],
title: 'Alert Name'
+ },
+ team_name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Team Name'
}
},
type: 'object',
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index e90b107ca6f..f5a53a28d22 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -4893,6 +4893,7 @@ export class DeadlinesService {
* @param data.lastUpdatedAtGt
* @param data.lastUpdatedAtLte
* @param data.lastUpdatedAtLt
+ * @param data.teams
* @returns DeadlineCollectionResponse Successful Response
* @throws ApiError
*/
@@ -4916,7 +4917,8 @@ export class DeadlinesService {
last_updated_at_gte: data.lastUpdatedAtGte,
last_updated_at_gt: data.lastUpdatedAtGt,
last_updated_at_lte: data.lastUpdatedAtLte,
- last_updated_at_lt: data.lastUpdatedAtLt
+ last_updated_at_lt: data.lastUpdatedAtLt,
+ teams: data.teams
},
errors: {
400: 'Bad Request',
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 16810c2219d..ae6b2c2074b 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
@@ -2541,6 +2541,7 @@ export type DeadlineResponse = {
dag_run_id: string;
alert_id?: string | null;
alert_name?: string | null;
+ team_name?: string | null;
};
/**
@@ -4740,6 +4741,7 @@ export type GetDeadlinesData = {
* Attributes to order by, multi criteria sort is supported. Prefix with
`-` for descending order. Supported attributes: `id, deadline_time, created_at,
last_updated_at, missed, dag_id, dag_run_id, alert_name`
*/
orderBy?: Array<(string)>;
+ teams?: Array<(string)>;
};
export type GetDeadlinesResponse = DeadlineCollectionResponse;
diff --git a/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
index d01d7b33ff6..8f4c7946e1d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
@@ -28,14 +28,16 @@ import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import { FilterBar } from "src/components/FilterBar";
+import { TeamName } from "src/components/TeamName";
import Time from "src/components/Time";
import { TruncatedText } from "src/components/TruncatedText";
import { SearchParamsKeys } from "src/constants/searchParams";
+import { useConfig } from "src/queries/useConfig";
import { useDocumentTitle, useFiltersHandler, type FilterableSearchParamsKeys
} from "src/utils";
type DeadlineRow = { row: { original: DeadlineResponse } };
-const createColumns = (translate: TFunction):
Array<ColumnDef<DeadlineResponse>> => [
+const createColumns = (translate: TFunction, multiTeam: boolean):
Array<ColumnDef<DeadlineResponse>> => [
{
accessorKey: "dag_id",
cell: ({ row: { original } }: DeadlineRow) => (
@@ -47,6 +49,16 @@ const createColumns = (translate: TFunction):
Array<ColumnDef<DeadlineResponse>>
),
header: translate("common:dagId"),
},
+ ...(multiTeam
+ ? [
+ {
+ accessorKey: "team_name",
+ cell: ({ row: { original } }: DeadlineRow) => <TeamName
teamName={original.team_name} />,
+ enableSorting: false,
+ header: translate("common:dagDetails.team"),
+ },
+ ]
+ : []),
{
accessorKey: "dag_run_id",
cell: ({ row: { original } }: DeadlineRow) => (
@@ -100,15 +112,18 @@ const deadlinesFilterKeys:
Array<FilterableSearchParamsKeys> = [
export const Deadlines = () => {
const { t: translate } = useTranslation(["browse", "common"]);
+ const multiTeamEnabled = Boolean(useConfig("multi_team"));
useDocumentTitle(translate("common:browse.deadlines"));
const { setTableURLState, tableURLState } = useTableURLState();
const [searchParams] = useSearchParams();
- const { filterConfigs, handleFiltersChange, initialValues } =
useFiltersHandler(deadlinesFilterKeys);
+ const { filterConfigs, handleFiltersChange, initialValues } =
useFiltersHandler(
+ multiTeamEnabled ? [...deadlinesFilterKeys, SearchParamsKeys.TEAMS] :
deadlinesFilterKeys,
+ );
- const columns = createColumns(translate);
+ const columns = createColumns(translate, multiTeamEnabled);
const { pagination, sorting } = tableURLState;
const [sort] = sorting;
@@ -118,6 +133,7 @@ export const Deadlines = () => {
const filteredMissed = searchParams.get(SearchParamsKeys.MISSED);
const deadlineTimeGte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_GTE);
const deadlineTimeLte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_LTE);
+ const teams = searchParams.getAll(SearchParamsKeys.TEAMS);
const missedFilter = filteredMissed === "true" ? true : filteredMissed ===
"false" ? false : undefined;
@@ -130,6 +146,7 @@ export const Deadlines = () => {
missed: missedFilter,
offset: pagination.pageIndex * pagination.pageSize,
orderBy,
+ teams: teams.length > 0 ? teams : undefined,
});
return (
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
index c2a8528afa6..8edfcb27816 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
@@ -25,7 +25,7 @@ from unittest import mock
import pytest
import time_machine
from fastapi.testclient import TestClient
-from sqlalchemy import delete, func, select, update
+from sqlalchemy import func, select, update
from airflow import plugins_manager
from airflow._shared.module_loading import qualname
@@ -64,6 +64,7 @@ from tests_common.test_utils.db import (
)
from tests_common.test_utils.format_datetime import from_datetime_to_zulu,
from_datetime_to_zulu_without_ms
from tests_common.test_utils.taskinstance import run_task_instance
+from tests_common.test_utils.team import attach_dag_to_team
from unit.listeners.class_listener import ClassBasedListener
if TYPE_CHECKING:
@@ -334,35 +335,6 @@ def get_dag_run_dict(run: DagRun):
}
-def _attach_dag_to_team(session, dag_id: str, *, bundle_name: str, team_name:
str) -> str:
- """
- Associate a Dag with a team via a team-scoped bundle for multi-team tests.
-
- Returns the Dag's original bundle name so the caller can restore it during
cleanup
- (``DagModel.bundle_name`` is a foreign key with no ``ON DELETE`` action).
- """
- original_bundle_name =
session.scalar(select(DagModel.bundle_name).where(DagModel.dag_id == dag_id))
- bundle = DagBundleModel(name=bundle_name)
- bundle.teams.append(Team(name=team_name))
- session.add(bundle)
- session.flush()
- session.execute(update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=bundle_name))
- session.commit()
- return original_bundle_name
-
-
-def _detach_dag_from_team(
- session, dag_id: str, *, bundle_name: str, team_name: str,
original_bundle_name: str
-) -> None:
- """Undo :func:`_attach_dag_to_team`, restoring the Dag's original
bundle."""
- session.execute(
- update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=original_bundle_name)
- )
- session.execute(delete(DagBundleModel).where(DagBundleModel.name ==
bundle_name))
- session.execute(delete(Team).where(Team.name == team_name))
- session.commit()
-
-
class TestGetDagRun:
@pytest.mark.parametrize(
("dag_id", "run_id", "state", "run_type", "triggered_by",
"dag_run_note"),
@@ -416,21 +388,10 @@ class TestGetDagRun:
@conf_vars({("core", "multi_team"): "True"})
@pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
def test_get_dag_run_includes_team_name(self, test_client, session):
- original_bundle_name = _attach_dag_to_team(
- session, DAG1_ID, bundle_name="team-bundle-run",
team_name="team-run"
- )
- try:
+ with attach_dag_to_team(session, DAG1_ID,
bundle_name="team-bundle-run", team_name="team-run"):
response =
test_client.get(f"/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}")
assert response.status_code == 200
assert response.json()["team_name"] == "team-run"
- finally:
- _detach_dag_from_team(
- session,
- DAG1_ID,
- bundle_name="team-bundle-run",
- team_name="team-run",
- original_bundle_name=original_bundle_name,
- )
def test_get_dag_run_not_found(self, test_client):
response = test_client.get(f"/dags/{DAG1_ID}/dagRuns/invalid")
@@ -489,31 +450,17 @@ class TestGetDagRuns:
@conf_vars({("core", "multi_team"): "True"})
@pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
def test_get_dag_runs_includes_team_name(self, test_client, session):
- original_bundle_name = _attach_dag_to_team(
- session, DAG1_ID, bundle_name="team-bundle-runs",
team_name="team-runs"
- )
- try:
+ with attach_dag_to_team(session, DAG1_ID,
bundle_name="team-bundle-runs", team_name="team-runs"):
response = test_client.get(f"/dags/{DAG1_ID}/dagRuns")
assert response.status_code == 200
body = response.json()
assert body["dag_runs"]
assert all(run["team_name"] == "team-runs" for run in
body["dag_runs"])
- finally:
- _detach_dag_from_team(
- session,
- DAG1_ID,
- bundle_name="team-bundle-runs",
- team_name="team-runs",
- original_bundle_name=original_bundle_name,
- )
@conf_vars({("core", "multi_team"): "True"})
@pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
def test_get_dag_runs_filtered_by_team(self, test_client, session):
- original_bundle_name = _attach_dag_to_team(
- session, DAG1_ID, bundle_name="team-bundle-filter",
team_name="team-filter"
- )
- try:
+ with attach_dag_to_team(session, DAG1_ID,
bundle_name="team-bundle-filter", team_name="team-filter"):
response = test_client.get("/dags/~/dagRuns", params={"teams":
["team-filter"]})
assert response.status_code == 200
body = response.json()
@@ -524,14 +471,6 @@ class TestGetDagRuns:
response = test_client.get("/dags/~/dagRuns", params={"teams":
["nonexistent-team"]})
assert response.status_code == 200
assert response.json()["total_entries"] == 0
- finally:
- _detach_dag_from_team(
- session,
- DAG1_ID,
- bundle_name="team-bundle-filter",
- team_name="team-filter",
- original_bundle_name=original_bundle_name,
- )
def test_invalid_order_by_raises_400(self, test_client):
response = test_client.get("/dags/test_dag1/dagRuns?order_by=invalid")
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 3784a57f062..4e7e4142015 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
@@ -65,6 +65,7 @@ from tests_common.test_utils.db import (
from tests_common.test_utils.logs import check_last_log
from tests_common.test_utils.mock_operators import MockOperator
from tests_common.test_utils.taskinstance import create_task_instance
+from tests_common.test_utils.team import attach_dag_to_team
pytestmark = pytest.mark.db_test
@@ -76,35 +77,6 @@ DEFAULT_DATETIME_1 =
dt.datetime.fromisoformat(DEFAULT_DATETIME_STR_1)
DEFAULT_DATETIME_2 = dt.datetime.fromisoformat(DEFAULT_DATETIME_STR_2)
-def _attach_dag_to_team(session, dag_id: str, *, bundle_name: str, team_name:
str) -> str:
- """
- Associate a Dag with a team via a team-scoped bundle for multi-team tests.
-
- Returns the Dag's original bundle name so the caller can restore it during
cleanup
- (``DagModel.bundle_name`` is a foreign key with no ``ON DELETE`` action).
- """
- original_bundle_name =
session.scalar(select(DagModel.bundle_name).where(DagModel.dag_id == dag_id))
- bundle = DagBundleModel(name=bundle_name)
- bundle.teams.append(Team(name=team_name))
- session.add(bundle)
- session.flush()
- session.execute(update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=bundle_name))
- session.commit()
- return original_bundle_name
-
-
-def _detach_dag_from_team(
- session, dag_id: str, *, bundle_name: str, team_name: str,
original_bundle_name: str
-) -> None:
- """Undo :func:`_attach_dag_to_team`, restoring the Dag's original
bundle."""
- session.execute(
- update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=original_bundle_name)
- )
- session.execute(delete(DagBundleModel).where(DagBundleModel.name ==
bundle_name))
- session.execute(delete(Team).where(Team.name == team_name))
- session.commit()
-
-
class TestTaskInstanceEndpoint:
@staticmethod
def clear_db():
@@ -277,23 +249,14 @@ class TestGetTaskInstance(TestTaskInstanceEndpoint):
@conf_vars({("core", "multi_team"): "True"})
def test_should_include_team_name(self, test_client, session):
self.create_task_instances(session)
- original_bundle_name = _attach_dag_to_team(
+ with attach_dag_to_team(
session, "example_python_operator", bundle_name="team-bundle-ti",
team_name="team-ti"
- )
- try:
+ ):
response = test_client.get(
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances/print_the_context"
)
assert response.status_code == 200
assert response.json()["team_name"] == "team-ti"
- finally:
- _detach_dag_from_team(
- session,
- "example_python_operator",
- bundle_name="team-bundle-ti",
- team_name="team-ti",
- original_bundle_name=original_bundle_name,
- )
def test_should_respond_200_with_decorator(self, test_client, session):
self.create_task_instances(session, "example_python_decorator")
@@ -727,26 +690,17 @@ class TestGetMappedTaskInstance(TestTaskInstanceEndpoint):
@conf_vars({("core", "multi_team"): "True"})
def test_should_include_team_name(self, test_client, session):
self.create_task_instances(session)
- original_bundle_name = _attach_dag_to_team(
+ with attach_dag_to_team(
session,
"example_python_operator",
bundle_name="team-bundle-mapped-ti",
team_name="team-mapped-ti",
- )
- try:
+ ):
response = test_client.get(
"/dags/example_python_operator/dagRuns/TEST_DAG_RUN_ID/taskInstances/print_the_context/-1",
)
assert response.status_code == 200
assert response.json()["team_name"] == "team-mapped-ti"
- finally:
- _detach_dag_from_team(
- session,
- "example_python_operator",
- bundle_name="team-bundle-mapped-ti",
- team_name="team-mapped-ti",
- original_bundle_name=original_bundle_name,
- )
class TestGetMappedTaskInstances:
@@ -2344,34 +2298,24 @@ class TestGetTaskInstances(TestTaskInstanceEndpoint):
@conf_vars({("core", "multi_team"): "True"})
def test_should_include_team_name(self, test_client, session):
self.create_task_instances(session)
- original_bundle_name = _attach_dag_to_team(
+ with attach_dag_to_team(
session, "example_python_operator", bundle_name="team-bundle-tis",
team_name="team-tis"
- )
- try:
+ ):
response =
test_client.get(f"/dags/{'example_python_operator'}/dagRuns/~/taskInstances")
assert response.status_code == 200
body = response.json()
assert body["task_instances"]
assert all(ti["team_name"] == "team-tis" for ti in
body["task_instances"])
- finally:
- _detach_dag_from_team(
- session,
- "example_python_operator",
- bundle_name="team-bundle-tis",
- team_name="team-tis",
- original_bundle_name=original_bundle_name,
- )
@conf_vars({("core", "multi_team"): "True"})
def test_should_filter_by_team(self, test_client, session):
self.create_task_instances(session)
- original_bundle_name = _attach_dag_to_team(
+ with attach_dag_to_team(
session,
"example_python_operator",
bundle_name="team-bundle-tis-filter",
team_name="team-tis-filter",
- )
- try:
+ ):
response = test_client.get(
"/dags/~/dagRuns/~/taskInstances", params={"teams":
["team-tis-filter"]}
)
@@ -2386,14 +2330,6 @@ class TestGetTaskInstances(TestTaskInstanceEndpoint):
)
assert response.status_code == 200
assert response.json()["total_entries"] == 0
- finally:
- _detach_dag_from_team(
- session,
- "example_python_operator",
- bundle_name="team-bundle-tis-filter",
- team_name="team-tis-filter",
- original_bundle_name=original_bundle_name,
- )
class TestGetTaskDependencies(TestTaskInstanceEndpoint):
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py
index 369729118c6..dd10f3ded99 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py
@@ -37,13 +37,17 @@ from airflow.utils.state import DagRunState
from airflow.utils.types import DagRunTriggeredByType, DagRunType
from tests_common.test_utils.asserts import assert_queries_count
+from tests_common.test_utils.config import conf_vars
from tests_common.test_utils.db import (
+ clear_db_dag_bundles,
clear_db_dags,
clear_db_deadline,
clear_db_deadline_alert,
clear_db_runs,
clear_db_serialized_dags,
+ clear_db_teams,
)
+from tests_common.test_utils.team import attach_dag_to_team
pytestmark = pytest.mark.db_test
@@ -72,13 +76,21 @@ def _cb() -> AsyncCallback:
return AsyncCallback(_CALLBACK_PATH)
[email protected](autouse=True)
-def setup(dag_maker, session):
+def _clear_db():
clear_db_deadline()
clear_db_deadline_alert()
clear_db_runs()
clear_db_dags()
clear_db_serialized_dags()
+ # attach_dag_to_team commits its bundle and team before yielding, so a run
interrupted
+ # inside that context manager leaves rows behind that would collide on the
next run.
+ clear_db_dag_bundles()
+ clear_db_teams()
+
+
[email protected](autouse=True)
+def setup(dag_maker, session):
+ _clear_db()
with dag_maker(DAG_ID, serialized=True, session=session):
EmptyOperator(task_id="task")
@@ -226,11 +238,7 @@ def setup(dag_maker, session):
dag_maker.sync_dagbag_to_db()
session.commit()
yield
- clear_db_deadline()
- clear_db_deadline_alert()
- clear_db_runs()
- clear_db_dags()
- clear_db_serialized_dags()
+ _clear_db()
class TestGetDagRunDeadlines:
@@ -456,6 +464,47 @@ class TestGetDeadlines:
unlinked = [dl for dl in deadlines if dl["alert_name"] is None]
assert all(dl["alert_id"] is None for dl in unlinked)
+ @pytest.mark.parametrize(
+ ("multi_team", "expected_team_name", "expected_queries"),
+ [
+ pytest.param("True", "team-deadlines", 4, id="multi_team_enabled"),
+ pytest.param("False", None, 3, id="multi_team_disabled"),
+ ],
+ )
+ def test_team_name_only_exposed_in_multi_team_mode(
+ self, test_client, session, multi_team, expected_team_name,
expected_queries
+ ):
+ with (
+ conf_vars({("core", "multi_team"): multi_team}),
+ attach_dag_to_team(
+ session, DAG_ID, bundle_name="team-bundle-deadlines",
team_name="team-deadlines"
+ ),
+ ):
+ with assert_queries_count(expected_queries):
+ response = test_client.get("/dags/~/dagRuns/~/deadlines")
+ assert response.status_code == 200
+ teams_by_dag = {dl["dag_id"]: dl["team_name"] for dl in
response.json()["deadlines"]}
+ assert teams_by_dag == {DAG_ID: expected_team_name, DAG_ID_2: None}
+
+ @conf_vars({("core", "multi_team"): "True"})
+ def test_filter_by_team(self, test_client, session):
+ with attach_dag_to_team(
+ session, DAG_ID, bundle_name="team-bundle-deadline-filter",
team_name="team-deadline-filter"
+ ):
+ with assert_queries_count(4):
+ response = test_client.get(
+ "/dags/~/dagRuns/~/deadlines", params={"teams":
["team-deadline-filter"]}
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_entries"] == 7
+ assert {dl["dag_id"] for dl in data["deadlines"]} == {DAG_ID}
+
+ # A team with no Dags returns nothing.
+ response = test_client.get("/dags/~/dagRuns/~/deadlines",
params={"teams": ["unknown-team"]})
+ assert response.status_code == 200
+ assert response.json()["total_entries"] == 0
+
def test_filter_nonexistent_dag_returns_empty(self, test_client):
"""Filtering by a dag_id that doesn't exist returns an empty list."""
response = test_client.get("/dags/nonexistent_dag/dagRuns/~/deadlines")
diff --git a/devel-common/src/tests_common/test_utils/team.py
b/devel-common/src/tests_common/test_utils/team.py
new file mode 100644
index 00000000000..9328bca698b
--- /dev/null
+++ b/devel-common/src/tests_common/test_utils/team.py
@@ -0,0 +1,68 @@
+# 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.
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import TYPE_CHECKING
+
+from sqlalchemy import delete, select, update
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+
+ from sqlalchemy.orm import Session
+
+
+@contextmanager
+def attach_dag_to_team(session: Session, dag_id: str, *, bundle_name: str,
team_name: str) -> Iterator[None]:
+ """
+ Associate a Dag with a team through a team-scoped bundle, for multi-team
tests.
+
+ On exit the Dag is moved back to the bundle it started in before the
bundle and team
+ created here are dropped, because ``DagModel.bundle_name`` is a foreign
key with no
+ ``ON DELETE`` action.
+
+ :param session: session used for both the setup and the teardown writes
+ :param dag_id: Dag to move under the team-scoped bundle
+ :param bundle_name: name of the bundle to create; must not already exist
+ :param team_name: name of the team to create and attach the bundle to
+ """
+ from airflow.models.dag import DagModel, clear_team_name_cache
+ from airflow.models.dagbundle import DagBundleModel
+ from airflow.models.team import Team
+
+ original_bundle_name =
session.scalar(select(DagModel.bundle_name).where(DagModel.dag_id == dag_id))
+ bundle = DagBundleModel(name=bundle_name)
+ bundle.teams.append(Team(name=team_name))
+ session.add(bundle)
+ session.flush()
+ session.execute(update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=bundle_name))
+ session.commit()
+ # DagModel.get_team_name caches by dag_id, so a lookup made before this
re-association
+ # would otherwise keep resolving to the Dag's previous team.
+ clear_team_name_cache()
+ try:
+ yield
+ finally:
+ session.execute(
+ update(DagModel).where(DagModel.dag_id ==
dag_id).values(bundle_name=original_bundle_name)
+ )
+ session.execute(delete(DagBundleModel).where(DagBundleModel.name ==
bundle_name))
+ session.execute(delete(Team).where(Team.name == team_name))
+ session.commit()
+ clear_team_name_cache()