From 5b5f8f4650e52a5a6c7e1ed6fb2cbbca1145d14e Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Wed, 4 Dec 2024 20:26:16 +0800 Subject: [PATCH] AIP-84 De-nest Dag Tags endpoint (#44608) * AIP-84 De-nest Tag Tags endpoint * Fix CI --- .../core_api/datamodels/dag_tags.py | 27 ++ .../api_fastapi/core_api/datamodels/dags.py | 7 - .../core_api/openapi/v1-generated.yaml | 128 ++++----- .../core_api/routes/public/__init__.py | 2 + .../core_api/routes/public/dag_tags.py | 71 +++++ .../core_api/routes/public/dags.py | 38 +-- airflow/ui/openapi-gen/queries/common.ts | 50 ++-- airflow/ui/openapi-gen/queries/prefetch.ts | 70 ++--- airflow/ui/openapi-gen/queries/queries.ts | 88 +++--- airflow/ui/openapi-gen/queries/suspense.ts | 88 +++--- .../ui/openapi-gen/requests/services.gen.ts | 66 ++--- airflow/ui/openapi-gen/requests/types.gen.ts | 64 ++--- .../core_api/routes/public/test_dag_tags.py | 250 ++++++++++++++++++ .../core_api/routes/public/test_dags.py | 114 -------- 14 files changed, 629 insertions(+), 434 deletions(-) create mode 100644 airflow/api_fastapi/core_api/datamodels/dag_tags.py create mode 100644 airflow/api_fastapi/core_api/routes/public/dag_tags.py create mode 100644 tests/api_fastapi/core_api/routes/public/test_dag_tags.py diff --git a/airflow/api_fastapi/core_api/datamodels/dag_tags.py b/airflow/api_fastapi/core_api/datamodels/dag_tags.py new file mode 100644 index 0000000000000..9e67e1ce7b196 --- /dev/null +++ b/airflow/api_fastapi/core_api/datamodels/dag_tags.py @@ -0,0 +1,27 @@ +# 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 pydantic import BaseModel + + +class DAGTagCollectionResponse(BaseModel): + """DAG Tags Collection serializer for responses.""" + + tags: list[str] + total_entries: int diff --git a/airflow/api_fastapi/core_api/datamodels/dags.py b/airflow/api_fastapi/core_api/datamodels/dags.py index f1dd7bd798044..eddf0e1be22e7 100644 --- a/airflow/api_fastapi/core_api/datamodels/dags.py +++ b/airflow/api_fastapi/core_api/datamodels/dags.py @@ -154,10 +154,3 @@ def get_params(cls, params: abc.MutableMapping | None) -> dict | None: def concurrency(self) -> int: """Return max_active_tasks as concurrency.""" return self.max_active_tasks - - -class DAGTagCollectionResponse(BaseModel): - """DAG Tags Collection serializer for responses.""" - - tags: list[str] - total_entries: int diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 30a89a589f9dc..d6ea8dcec50e6 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -2636,70 +2636,6 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /public/dags/tags: - get: - tags: - - DAG - summary: Get Dag Tags - description: Get all DAG tags. - operationId: get_dag_tags - parameters: - - name: limit - in: query - required: false - schema: - type: integer - minimum: 0 - default: 100 - title: Limit - - name: offset - in: query - required: false - schema: - type: integer - minimum: 0 - default: 0 - title: Offset - - name: order_by - in: query - required: false - schema: - type: string - default: name - title: Order By - - name: tag_name_pattern - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Tag Name Pattern - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/DAGTagCollectionResponse' - '401': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' - description: Unauthorized - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' - description: Forbidden - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' /public/dags/{dag_id}: get: tags: @@ -5702,6 +5638,70 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /public/dagTags: + get: + tags: + - DAG + summary: Get Dag Tags + description: Get all DAG tags. + operationId: get_dag_tags + parameters: + - name: limit + in: query + required: false + schema: + type: integer + minimum: 0 + default: 100 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + minimum: 0 + default: 0 + title: Offset + - name: order_by + in: query + required: false + schema: + type: string + default: name + title: Order By + - name: tag_name_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Tag Name Pattern + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DAGTagCollectionResponse' + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Forbidden + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /public/monitor/health: get: tags: diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py b/airflow/api_fastapi/core_api/routes/public/__init__.py index fd9cfa1a2d345..2a7c81782a9da 100644 --- a/airflow/api_fastapi/core_api/routes/public/__init__.py +++ b/airflow/api_fastapi/core_api/routes/public/__init__.py @@ -29,6 +29,7 @@ from airflow.api_fastapi.core_api.routes.public.dag_run import dag_run_router from airflow.api_fastapi.core_api.routes.public.dag_sources import dag_sources_router from airflow.api_fastapi.core_api.routes.public.dag_stats import dag_stats_router +from airflow.api_fastapi.core_api.routes.public.dag_tags import dag_tags_router from airflow.api_fastapi.core_api.routes.public.dag_warning import dag_warning_router from airflow.api_fastapi.core_api.routes.public.dags import dags_router from airflow.api_fastapi.core_api.routes.public.event_logs import event_logs_router @@ -75,6 +76,7 @@ authenticated_router.include_router(variables_router) authenticated_router.include_router(task_instances_log_router) authenticated_router.include_router(dag_parsing_router) +authenticated_router.include_router(dag_tags_router) # Include authenticated router in public router diff --git a/airflow/api_fastapi/core_api/routes/public/dag_tags.py b/airflow/api_fastapi/core_api/routes/public/dag_tags.py new file mode 100644 index 0000000000000..73714177d0b0d --- /dev/null +++ b/airflow/api_fastapi/core_api/routes/public/dag_tags.py @@ -0,0 +1,71 @@ +# 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 typing import Annotated + +from fastapi import Depends +from sqlalchemy import select + +from airflow.api_fastapi.common.db.common import ( + SessionDep, + paginated_select, +) +from airflow.api_fastapi.common.parameters import ( + QueryDagTagPatternSearch, + QueryLimit, + QueryOffset, + SortParam, +) +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.datamodels.dag_tags import DAGTagCollectionResponse +from airflow.models.dag import DagTag + +dag_tags_router = AirflowRouter(tags=["DAG"], prefix="/dagTags") + + +@dag_tags_router.get( + "", +) +def get_dag_tags( + limit: QueryLimit, + offset: QueryOffset, + order_by: Annotated[ + SortParam, + Depends( + SortParam( + ["name"], + DagTag, + ).dynamic_depends() + ), + ], + tag_name_pattern: QueryDagTagPatternSearch, + session: SessionDep, +) -> DAGTagCollectionResponse: + """Get all DAG tags.""" + query = select(DagTag.name).group_by(DagTag.name) + dag_tags_select, total_entries = paginated_select( + statement=query, + filters=[tag_name_pattern], + order_by=order_by, + offset=offset, + limit=limit, + session=session, + ) + dag_tags = session.execute(dag_tags_select).scalars().all() + return DAGTagCollectionResponse(tags=[x for x in dag_tags], total_entries=total_entries) diff --git a/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow/api_fastapi/core_api/routes/public/dags.py index 9663c0f866280..395855a0a0579 100644 --- a/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow/api_fastapi/core_api/routes/public/dags.py @@ -20,7 +20,7 @@ from typing import Annotated from fastapi import Depends, HTTPException, Query, Request, Response, status -from sqlalchemy import select, update +from sqlalchemy import update from airflow.api.common import delete_dag as delete_dag_module from airflow.api_fastapi.common.db.common import ( @@ -32,7 +32,6 @@ QueryDagDisplayNamePatternSearch, QueryDagIdPatternSearch, QueryDagIdPatternSearchWithNone, - QueryDagTagPatternSearch, QueryLastDagRunStateFilter, QueryLimit, QueryOffset, @@ -48,11 +47,10 @@ DAGDetailsResponse, DAGPatchBody, DAGResponse, - DAGTagCollectionResponse, ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.exceptions import AirflowException, DagNotFound -from airflow.models import DAG, DagModel, DagTag +from airflow.models import DAG, DagModel from airflow.models.dagrun import DagRun dags_router = AirflowRouter(tags=["DAG"], prefix="/dags") @@ -107,38 +105,6 @@ def get_dags( ) -@dags_router.get( - "/tags", -) -def get_dag_tags( - limit: QueryLimit, - offset: QueryOffset, - order_by: Annotated[ - SortParam, - Depends( - SortParam( - ["name"], - DagTag, - ).dynamic_depends() - ), - ], - tag_name_pattern: QueryDagTagPatternSearch, - session: SessionDep, -) -> DAGTagCollectionResponse: - """Get all DAG tags.""" - query = select(DagTag.name).group_by(DagTag.name) - dag_tags_select, total_entries = paginated_select( - statement=query, - filters=[tag_name_pattern], - order_by=order_by, - offset=offset, - limit=limit, - session=session, - ) - dag_tags = session.execute(dag_tags_select).scalars().all() - return DAGTagCollectionResponse(tags=[x for x in dag_tags], total_entries=total_entries) - - @dags_router.get( "/{dag_id}", responses=create_openapi_http_exception_doc( diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 766c08ae9092b..c8bc4d067299d 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -656,31 +656,6 @@ export const UseDagServiceGetDagsKeyFn = ( }, ]), ]; -export type DagServiceGetDagTagsDefaultResponse = Awaited< - ReturnType ->; -export type DagServiceGetDagTagsQueryResult< - TData = DagServiceGetDagTagsDefaultResponse, - TError = unknown, -> = UseQueryResult; -export const useDagServiceGetDagTagsKey = "DagServiceGetDagTags"; -export const UseDagServiceGetDagTagsKeyFn = ( - { - limit, - offset, - orderBy, - tagNamePattern, - }: { - limit?: number; - offset?: number; - orderBy?: string; - tagNamePattern?: string; - } = {}, - queryKey?: Array, -) => [ - useDagServiceGetDagTagsKey, - ...(queryKey ?? [{ limit, offset, orderBy, tagNamePattern }]), -]; export type DagServiceGetDagDefaultResponse = Awaited< ReturnType >; @@ -713,6 +688,31 @@ export const UseDagServiceGetDagDetailsKeyFn = ( }, queryKey?: Array, ) => [useDagServiceGetDagDetailsKey, ...(queryKey ?? [{ dagId }])]; +export type DagServiceGetDagTagsDefaultResponse = Awaited< + ReturnType +>; +export type DagServiceGetDagTagsQueryResult< + TData = DagServiceGetDagTagsDefaultResponse, + TError = unknown, +> = UseQueryResult; +export const useDagServiceGetDagTagsKey = "DagServiceGetDagTags"; +export const UseDagServiceGetDagTagsKeyFn = ( + { + limit, + offset, + orderBy, + tagNamePattern, + }: { + limit?: number; + offset?: number; + orderBy?: string; + tagNamePattern?: string; + } = {}, + queryKey?: Array, +) => [ + useDagServiceGetDagTagsKey, + ...(queryKey ?? [{ limit, offset, orderBy, tagNamePattern }]), +]; export type EventLogServiceGetEventLogDefaultResponse = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index aa5edc206beef..40e7006e8f8e3 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -850,41 +850,6 @@ export const prefetchUseDagServiceGetDags = ( tags, }), }); -/** - * Get Dag Tags - * Get all DAG tags. - * @param data The data for the request. - * @param data.limit - * @param data.offset - * @param data.orderBy - * @param data.tagNamePattern - * @returns DAGTagCollectionResponse Successful Response - * @throws ApiError - */ -export const prefetchUseDagServiceGetDagTags = ( - queryClient: QueryClient, - { - limit, - offset, - orderBy, - tagNamePattern, - }: { - limit?: number; - offset?: number; - orderBy?: string; - tagNamePattern?: string; - } = {}, -) => - queryClient.prefetchQuery({ - queryKey: Common.UseDagServiceGetDagTagsKeyFn({ - limit, - offset, - orderBy, - tagNamePattern, - }), - queryFn: () => - DagService.getDagTags({ limit, offset, orderBy, tagNamePattern }), - }); /** * Get Dag * Get basic information about a DAG. @@ -925,6 +890,41 @@ export const prefetchUseDagServiceGetDagDetails = ( queryKey: Common.UseDagServiceGetDagDetailsKeyFn({ dagId }), queryFn: () => DagService.getDagDetails({ dagId }), }); +/** + * Get Dag Tags + * Get all DAG tags. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @param data.tagNamePattern + * @returns DAGTagCollectionResponse Successful Response + * @throws ApiError + */ +export const prefetchUseDagServiceGetDagTags = ( + queryClient: QueryClient, + { + limit, + offset, + orderBy, + tagNamePattern, + }: { + limit?: number; + offset?: number; + orderBy?: string; + tagNamePattern?: string; + } = {}, +) => + queryClient.prefetchQuery({ + queryKey: Common.UseDagServiceGetDagTagsKeyFn({ + limit, + offset, + orderBy, + tagNamePattern, + }), + queryFn: () => + DagService.getDagTags({ limit, offset, orderBy, tagNamePattern }), + }); /** * Get Event Log * @param data The data for the request. diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index efaaf0002d817..624b9c38be86c 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -1046,50 +1046,6 @@ export const useDagServiceGetDags = < }) as TData, ...options, }); -/** - * Get Dag Tags - * Get all DAG tags. - * @param data The data for the request. - * @param data.limit - * @param data.offset - * @param data.orderBy - * @param data.tagNamePattern - * @returns DAGTagCollectionResponse Successful Response - * @throws ApiError - */ -export const useDagServiceGetDagTags = < - TData = Common.DagServiceGetDagTagsDefaultResponse, - TError = unknown, - TQueryKey extends Array = unknown[], ->( - { - limit, - offset, - orderBy, - tagNamePattern, - }: { - limit?: number; - offset?: number; - orderBy?: string; - tagNamePattern?: string; - } = {}, - queryKey?: TQueryKey, - options?: Omit, "queryKey" | "queryFn">, -) => - useQuery({ - queryKey: Common.UseDagServiceGetDagTagsKeyFn( - { limit, offset, orderBy, tagNamePattern }, - queryKey, - ), - queryFn: () => - DagService.getDagTags({ - limit, - offset, - orderBy, - tagNamePattern, - }) as TData, - ...options, - }); /** * Get Dag * Get basic information about a DAG. @@ -1142,6 +1098,50 @@ export const useDagServiceGetDagDetails = < queryFn: () => DagService.getDagDetails({ dagId }) as TData, ...options, }); +/** + * Get Dag Tags + * Get all DAG tags. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @param data.tagNamePattern + * @returns DAGTagCollectionResponse Successful Response + * @throws ApiError + */ +export const useDagServiceGetDagTags = < + TData = Common.DagServiceGetDagTagsDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + orderBy, + tagNamePattern, + }: { + limit?: number; + offset?: number; + orderBy?: string; + tagNamePattern?: string; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useQuery({ + queryKey: Common.UseDagServiceGetDagTagsKeyFn( + { limit, offset, orderBy, tagNamePattern }, + queryKey, + ), + queryFn: () => + DagService.getDagTags({ + limit, + offset, + orderBy, + tagNamePattern, + }) as TData, + ...options, + }); /** * Get Event Log * @param data The data for the request. diff --git a/airflow/ui/openapi-gen/queries/suspense.ts b/airflow/ui/openapi-gen/queries/suspense.ts index e1349743f2bbb..250b2038a8c41 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -1021,50 +1021,6 @@ export const useDagServiceGetDagsSuspense = < }) as TData, ...options, }); -/** - * Get Dag Tags - * Get all DAG tags. - * @param data The data for the request. - * @param data.limit - * @param data.offset - * @param data.orderBy - * @param data.tagNamePattern - * @returns DAGTagCollectionResponse Successful Response - * @throws ApiError - */ -export const useDagServiceGetDagTagsSuspense = < - TData = Common.DagServiceGetDagTagsDefaultResponse, - TError = unknown, - TQueryKey extends Array = unknown[], ->( - { - limit, - offset, - orderBy, - tagNamePattern, - }: { - limit?: number; - offset?: number; - orderBy?: string; - tagNamePattern?: string; - } = {}, - queryKey?: TQueryKey, - options?: Omit, "queryKey" | "queryFn">, -) => - useSuspenseQuery({ - queryKey: Common.UseDagServiceGetDagTagsKeyFn( - { limit, offset, orderBy, tagNamePattern }, - queryKey, - ), - queryFn: () => - DagService.getDagTags({ - limit, - offset, - orderBy, - tagNamePattern, - }) as TData, - ...options, - }); /** * Get Dag * Get basic information about a DAG. @@ -1117,6 +1073,50 @@ export const useDagServiceGetDagDetailsSuspense = < queryFn: () => DagService.getDagDetails({ dagId }) as TData, ...options, }); +/** + * Get Dag Tags + * Get all DAG tags. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @param data.tagNamePattern + * @returns DAGTagCollectionResponse Successful Response + * @throws ApiError + */ +export const useDagServiceGetDagTagsSuspense = < + TData = Common.DagServiceGetDagTagsDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + orderBy, + tagNamePattern, + }: { + limit?: number; + offset?: number; + orderBy?: string; + tagNamePattern?: string; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useSuspenseQuery({ + queryKey: Common.UseDagServiceGetDagTagsKeyFn( + { limit, offset, orderBy, tagNamePattern }, + queryKey, + ), + queryFn: () => + DagService.getDagTags({ + limit, + offset, + orderBy, + tagNamePattern, + }) as TData, + ...options, + }); /** * Get Event Log * @param data The data for the request. diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index b4952122c0ea7..4bec27ae4651c 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -88,8 +88,6 @@ import type { GetDagsResponse, PatchDagsData, PatchDagsResponse, - GetDagTagsData, - GetDagTagsResponse, GetDagData, GetDagResponse, PatchDagData, @@ -98,6 +96,8 @@ import type { DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, + GetDagTagsData, + GetDagTagsResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, @@ -1511,37 +1511,6 @@ export class DagService { }); } - /** - * Get Dag Tags - * Get all DAG tags. - * @param data The data for the request. - * @param data.limit - * @param data.offset - * @param data.orderBy - * @param data.tagNamePattern - * @returns DAGTagCollectionResponse Successful Response - * @throws ApiError - */ - public static getDagTags( - data: GetDagTagsData = {}, - ): CancelablePromise { - return __request(OpenAPI, { - method: "GET", - url: "/public/dags/tags", - query: { - limit: data.limit, - offset: data.offset, - order_by: data.orderBy, - tag_name_pattern: data.tagNamePattern, - }, - errors: { - 401: "Unauthorized", - 403: "Forbidden", - 422: "Validation Error", - }, - }); - } - /** * Get Dag * Get basic information about a DAG. @@ -1654,6 +1623,37 @@ export class DagService { }, }); } + + /** + * Get Dag Tags + * Get all DAG tags. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @param data.tagNamePattern + * @returns DAGTagCollectionResponse Successful Response + * @throws ApiError + */ + public static getDagTags( + data: GetDagTagsData = {}, + ): CancelablePromise { + return __request(OpenAPI, { + method: "GET", + url: "/public/dagTags", + query: { + limit: data.limit, + offset: data.offset, + order_by: data.orderBy, + tag_name_pattern: data.tagNamePattern, + }, + errors: { + 401: "Unauthorized", + 403: "Forbidden", + 422: "Validation Error", + }, + }); + } } export class EventLogService { diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 9fc97c3adeb89..e5aed24a98cd5 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1636,15 +1636,6 @@ export type PatchDagsData = { export type PatchDagsResponse = DAGCollectionResponse; -export type GetDagTagsData = { - limit?: number; - offset?: number; - orderBy?: string; - tagNamePattern?: string | null; -}; - -export type GetDagTagsResponse = DAGTagCollectionResponse; - export type GetDagData = { dagId: string; }; @@ -1671,6 +1662,15 @@ export type GetDagDetailsData = { export type GetDagDetailsResponse = DAGDetailsResponse; +export type GetDagTagsData = { + limit?: number; + offset?: number; + orderBy?: string; + tagNamePattern?: string | null; +}; + +export type GetDagTagsResponse = DAGTagCollectionResponse; + export type GetEventLogData = { eventLogId: number; }; @@ -3188,29 +3188,6 @@ export type $OpenApiTs = { }; }; }; - "/public/dags/tags": { - get: { - req: GetDagTagsData; - res: { - /** - * Successful Response - */ - 200: DAGTagCollectionResponse; - /** - * Unauthorized - */ - 401: HTTPExceptionResponse; - /** - * Forbidden - */ - 403: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; "/public/dags/{dag_id}": { get: { req: GetDagData; @@ -3331,6 +3308,29 @@ export type $OpenApiTs = { }; }; }; + "/public/dagTags": { + get: { + req: GetDagTagsData; + res: { + /** + * Successful Response + */ + 200: DAGTagCollectionResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; "/public/eventLogs/{event_log_id}": { get: { req: GetEventLogData; diff --git a/tests/api_fastapi/core_api/routes/public/test_dag_tags.py b/tests/api_fastapi/core_api/routes/public/test_dag_tags.py new file mode 100644 index 0000000000000..784bb480c431b --- /dev/null +++ b/tests/api_fastapi/core_api/routes/public/test_dag_tags.py @@ -0,0 +1,250 @@ +# 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 datetime import datetime, timezone + +import pendulum +import pytest + +from airflow.models.dag import DagModel, DagTag +from airflow.models.dagrun import DagRun +from airflow.operators.empty import EmptyOperator +from airflow.utils.session import provide_session +from airflow.utils.state import DagRunState +from airflow.utils.types import DagRunTriggeredByType, DagRunType + +from tests_common.test_utils.db import clear_db_dags, clear_db_runs, clear_db_serialized_dags + +pytestmark = pytest.mark.db_test + +DAG1_ID = "test_dag1" +DAG1_DISPLAY_NAME = "display1" +DAG2_ID = "test_dag2" +DAG2_START_DATE = datetime(2021, 6, 15, tzinfo=timezone.utc) +DAG3_ID = "test_dag3" +DAG4_ID = "test_dag4" +DAG4_DISPLAY_NAME = "display4" +DAG5_ID = "test_dag5" +DAG5_DISPLAY_NAME = "display5" +TASK_ID = "op1" +UTC_JSON_REPR = "UTC" if pendulum.__version__.startswith("3") else "Timezone('UTC')" +API_PREFIX = "/public/dags" + + +class TestDagEndpoint: + """Common class for /public/dags related unit tests.""" + + @staticmethod + def _clear_db(): + clear_db_runs() + clear_db_dags() + clear_db_serialized_dags() + + def _create_deactivated_paused_dag(self, session=None): + dag_model = DagModel( + dag_id=DAG3_ID, + fileloc="/tmp/dag_del_1.py", + timetable_summary="2 2 * * *", + is_active=False, + is_paused=True, + owners="test_owner,another_test_owner", + next_dagrun=datetime(2021, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + ) + + dagrun_failed = DagRun( + dag_id=DAG3_ID, + run_id="run1", + logical_date=datetime(2018, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + start_date=datetime(2018, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + run_type=DagRunType.SCHEDULED, + state=DagRunState.FAILED, + triggered_by=DagRunTriggeredByType.TEST, + ) + + dagrun_success = DagRun( + dag_id=DAG3_ID, + run_id="run2", + logical_date=datetime(2019, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + start_date=datetime(2019, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + run_type=DagRunType.MANUAL, + state=DagRunState.SUCCESS, + triggered_by=DagRunTriggeredByType.TEST, + ) + + session.add(dag_model) + session.add(dagrun_failed) + session.add(dagrun_success) + + def _create_dag_tags(self, session=None): + session.add(DagTag(dag_id=DAG1_ID, name="tag_2")) + session.add(DagTag(dag_id=DAG2_ID, name="tag_1")) + session.add(DagTag(dag_id=DAG3_ID, name="tag_1")) + + @pytest.fixture(autouse=True) + @provide_session + def setup(self, dag_maker, session=None) -> None: + self._clear_db() + + with dag_maker( + DAG1_ID, + dag_display_name=DAG1_DISPLAY_NAME, + schedule=None, + start_date=datetime(2018, 6, 15, 0, 0, tzinfo=timezone.utc), + doc_md="details", + params={"foo": 1}, + tags=["example"], + ): + EmptyOperator(task_id=TASK_ID) + + dag_maker.create_dagrun(state=DagRunState.FAILED) + + with dag_maker( + DAG2_ID, + schedule=None, + start_date=DAG2_START_DATE, + doc_md="details", + params={"foo": 1}, + max_active_tasks=16, + max_active_runs=16, + ): + EmptyOperator(task_id=TASK_ID) + + self._create_deactivated_paused_dag(session) + self._create_dag_tags(session) + + dag_maker.dagbag.sync_to_db() + dag_maker.dag_model.has_task_concurrency_limits = True + session.merge(dag_maker.dag_model) + session.commit() + + def teardown_method(self) -> None: + self._clear_db() + + +class TestDagTags(TestDagEndpoint): + """Unit tests for Get DAG Tags.""" + + @pytest.mark.parametrize( + "query_params, expected_status_code, expected_dag_tags, expected_total_entries", + [ + # test with offset, limit, and without any tag_name_pattern + ( + {}, + 200, + [ + "example", + "tag_1", + "tag_2", + ], + 3, + ), + ( + {"offset": 1}, + 200, + [ + "tag_1", + "tag_2", + ], + 3, + ), + ( + {"limit": 2}, + 200, + [ + "example", + "tag_1", + ], + 3, + ), + ( + {"offset": 1, "limit": 2}, + 200, + [ + "tag_1", + "tag_2", + ], + 3, + ), + # test with tag_name_pattern + ( + {"tag_name_pattern": "invalid"}, + 200, + [], + 0, + ), + ( + {"tag_name_pattern": "1"}, + 200, + ["tag_1"], + 1, + ), + ( + {"tag_name_pattern": "tag%"}, + 200, + ["tag_1", "tag_2"], + 2, + ), + # test order_by + ( + {"order_by": "-name"}, + 200, + ["tag_2", "tag_1", "example"], + 3, + ), + # test all query params + ( + {"tag_name_pattern": "t%", "order_by": "-name", "offset": 1, "limit": 1}, + 200, + ["tag_1"], + 2, + ), + ( + {"tag_name_pattern": "~", "offset": 1, "limit": 2}, + 200, + ["tag_1", "tag_2"], + 3, + ), + # test invalid query params + ( + {"order_by": "dag_id"}, + 400, + None, + None, + ), + ( + {"order_by": "-dag_id"}, + 400, + None, + None, + ), + ], + ) + def test_get_dag_tags( + self, test_client, query_params, expected_status_code, expected_dag_tags, expected_total_entries + ): + response = test_client.get("/public/dagTags", params=query_params) + assert response.status_code == expected_status_code + if expected_status_code != 200: + return + + res_json = response.json() + expected = { + "tags": expected_dag_tags, + "total_entries": expected_total_entries, + } + assert res_json == expected diff --git a/tests/api_fastapi/core_api/routes/public/test_dags.py b/tests/api_fastapi/core_api/routes/public/test_dags.py index daa680b42d14e..05a49c44dfeea 100644 --- a/tests/api_fastapi/core_api/routes/public/test_dags.py +++ b/tests/api_fastapi/core_api/routes/public/test_dags.py @@ -387,120 +387,6 @@ def test_get_dag(self, test_client, query_params, dag_id, expected_status_code, assert res_json == expected -class TestGetDagTags(TestDagEndpoint): - """Unit tests for Get DAG Tags.""" - - @pytest.mark.parametrize( - "query_params, expected_status_code, expected_dag_tags, expected_total_entries", - [ - # test with offset, limit, and without any tag_name_pattern - ( - {}, - 200, - [ - "example", - "tag_1", - "tag_2", - ], - 3, - ), - ( - {"offset": 1}, - 200, - [ - "tag_1", - "tag_2", - ], - 3, - ), - ( - {"limit": 2}, - 200, - [ - "example", - "tag_1", - ], - 3, - ), - ( - {"offset": 1, "limit": 2}, - 200, - [ - "tag_1", - "tag_2", - ], - 3, - ), - # test with tag_name_pattern - ( - {"tag_name_pattern": "invalid"}, - 200, - [], - 0, - ), - ( - {"tag_name_pattern": "1"}, - 200, - ["tag_1"], - 1, - ), - ( - {"tag_name_pattern": "tag%"}, - 200, - ["tag_1", "tag_2"], - 2, - ), - # test order_by - ( - {"order_by": "-name"}, - 200, - ["tag_2", "tag_1", "example"], - 3, - ), - # test all query params - ( - {"tag_name_pattern": "t%", "order_by": "-name", "offset": 1, "limit": 1}, - 200, - ["tag_1"], - 2, - ), - ( - {"tag_name_pattern": "~", "offset": 1, "limit": 2}, - 200, - ["tag_1", "tag_2"], - 3, - ), - # test invalid query params - ( - {"order_by": "dag_id"}, - 400, - None, - None, - ), - ( - {"order_by": "-dag_id"}, - 400, - None, - None, - ), - ], - ) - def test_get_dag_tags( - self, test_client, query_params, expected_status_code, expected_dag_tags, expected_total_entries - ): - response = test_client.get("/public/dags/tags", params=query_params) - assert response.status_code == expected_status_code - if expected_status_code != 200: - return - - res_json = response.json() - expected = { - "tags": expected_dag_tags, - "total_entries": expected_total_entries, - } - assert res_json == expected - - class TestDeleteDAG(TestDagEndpoint): """Unit tests for Delete DAG."""