From 3f52c830584d9f077b00572d9ff5f54e101c7441 Mon Sep 17 00:00:00 2001 From: Omkar P <45419097+omkar-foss@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:08:32 +0530 Subject: [PATCH 001/258] Fix intermittent failure in UI Time test (#43147) --- airflow/ui/src/components/Time.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airflow/ui/src/components/Time.test.tsx b/airflow/ui/src/components/Time.test.tsx index 9e59d96dd4714..2b6b26d6a2f20 100644 --- a/airflow/ui/src/components/Time.test.tsx +++ b/airflow/ui/src/components/Time.test.tsx @@ -54,11 +54,12 @@ describe("Test Time and TimezoneProvider", () => { }, ); - const samoaTime = screen.getByText(dayjs(now).tz(tz).format(defaultFormat)); + const nowTime = dayjs(now); + const samoaTime = screen.getByText(nowTime.tz(tz).format(defaultFormat)); expect(samoaTime).toBeDefined(); expect(samoaTime.title).toEqual( - dayjs().tz("UTC").format(defaultFormatWithTZ), + nowTime.tz("UTC").format(defaultFormatWithTZ), ); }); }); From fb49aa323bd5bad4cdd9e3027d6ae918fff9f768 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 18 Oct 2024 21:20:10 +0800 Subject: [PATCH 002/258] Always use logical date in DAG run header (#43148) Co-authored-by: Kaxil Naik --- airflow/www/static/js/utils/index.test.ts | 8 ++++---- airflow/www/static/js/utils/index.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/airflow/www/static/js/utils/index.test.ts b/airflow/www/static/js/utils/index.test.ts index a12f66b36c5f3..569d3af98b537 100644 --- a/airflow/www/static/js/utils/index.test.ts +++ b/airflow/www/static/js/utils/index.test.ts @@ -146,14 +146,14 @@ describe("Test getDagRunLabel", () => { note: "someRandomValue", } as DagRun; - test("Defaults to dataIntervalStart", async () => { + test("Defaults to executionDate", async () => { const runLabel = getDagRunLabel({ dagRun }); - expect(runLabel).toBe(dagRun.dataIntervalStart); + expect(runLabel).toBe(dagRun.executionDate); }); test("Passing an order overrides default", async () => { - const runLabel = getDagRunLabel({ dagRun, ordering: ["executionDate"] }); - expect(runLabel).toBe(dagRun.executionDate); + const runLabel = getDagRunLabel({ dagRun, ordering: ["dataIntervalEnd"] }); + expect(runLabel).toBe(dagRun.dataIntervalEnd); }); }); diff --git a/airflow/www/static/js/utils/index.ts b/airflow/www/static/js/utils/index.ts index 747ca474c73a1..87428ce8363d1 100644 --- a/airflow/www/static/js/utils/index.ts +++ b/airflow/www/static/js/utils/index.ts @@ -169,7 +169,7 @@ interface RunLabelProps { const getDagRunLabel = ({ dagRun, - ordering = ["dataIntervalStart", "executionDate"], + ordering = ["executionDate"], }: RunLabelProps) => dagRun[ordering[0]] ?? dagRun[ordering[1]]; const getStatusBackgroundColor = (color: string, hasNote: boolean) => From b4269f33c7151e6d61e07333003ec1e219285b07 Mon Sep 17 00:00:00 2001 From: Jasmin Patel Date: Fri, 18 Oct 2024 18:57:54 +0530 Subject: [PATCH 003/258] Add `SageMakerProcessingSensor` (#43144) Add SageMakerProcessingSensor which can be used to wait on a SageMaker processing job. Co-authored-by: Jasmin --- .../operators/sagemaker.rst | 14 +++ .../providers/amazon/aws/hooks/sagemaker.py | 2 + .../providers/amazon/aws/sensors/sagemaker.py | 32 +++++ .../aws/sensors/test_sagemaker_processing.py | 110 ++++++++++++++++++ .../system/amazon/aws/example_sagemaker.py | 13 +++ 5 files changed, 171 insertions(+) create mode 100644 providers/tests/amazon/aws/sensors/test_sagemaker_processing.py diff --git a/docs/apache-airflow-providers-amazon/operators/sagemaker.rst b/docs/apache-airflow-providers-amazon/operators/sagemaker.rst index c2f433267c306..03e5a7a921c27 100644 --- a/docs/apache-airflow-providers-amazon/operators/sagemaker.rst +++ b/docs/apache-airflow-providers-amazon/operators/sagemaker.rst @@ -366,6 +366,20 @@ you can use :class:`~airflow.providers.amazon.aws.sensors.sagemaker.SageMakerAut :start-after: [START howto_operator_sagemaker_auto_ml] :end-before: [END howto_operator_sagemaker_auto_ml] +.. _howto/sensor:SageMakerProcessingSensor: + +Wait on an Amazon SageMaker processing job state +================================================ + +To check the state of an Amazon Sagemaker processing job until it reaches a terminal state +you can use :class:`~airflow.providers.amazon.aws.sensors.sagemaker.SageMakerProcessingSensor`. + +.. exampleinclude:: /../../providers/tests/system/amazon/aws/example_sagemaker.py + :language: python + :dedent: 4 + :start-after: [START howto_sensor_sagemaker_processing] + :end-before: [END howto_sensor_sagemaker_processing] + Reference --------- diff --git a/providers/src/airflow/providers/amazon/aws/hooks/sagemaker.py b/providers/src/airflow/providers/amazon/aws/hooks/sagemaker.py index e16ab11b0c95c..10d7b8436c0d0 100644 --- a/providers/src/airflow/providers/amazon/aws/hooks/sagemaker.py +++ b/providers/src/airflow/providers/amazon/aws/hooks/sagemaker.py @@ -153,7 +153,9 @@ class SageMakerHook(AwsBaseHook): non_terminal_states = {"InProgress", "Stopping"} endpoint_non_terminal_states = {"Creating", "Updating", "SystemUpdating", "RollingBack", "Deleting"} pipeline_non_terminal_states = {"Executing", "Stopping"} + processing_job_non_terminal_states = {"InProgress", "Stopping"} failed_states = {"Failed"} + processing_job_failed_states = {*failed_states, "Stopped"} training_failed_states = {*failed_states, "Stopped"} def __init__(self, *args, **kwargs): diff --git a/providers/src/airflow/providers/amazon/aws/sensors/sagemaker.py b/providers/src/airflow/providers/amazon/aws/sensors/sagemaker.py index e77628cf8d596..5863de92d9339 100644 --- a/providers/src/airflow/providers/amazon/aws/sensors/sagemaker.py +++ b/providers/src/airflow/providers/amazon/aws/sensors/sagemaker.py @@ -330,3 +330,35 @@ def get_sagemaker_response(self) -> dict: def state_from_response(self, response: dict) -> str: return response["AutoMLJobStatus"] + + +class SageMakerProcessingSensor(SageMakerBaseSensor): + """ + Poll the processing job until it reaches a terminal state; raise AirflowException with the failure reason. + + .. seealso:: + For more information on how to use this sensor, take a look at the guide: + :ref:`howto/sensor:SageMakerProcessingSensor` + + :param job_name: Name of the processing job to watch. + """ + + template_fields: Sequence[str] = ("job_name",) + template_ext: Sequence[str] = () + + def __init__(self, *, job_name: str, **kwargs): + super().__init__(**kwargs) + self.job_name = job_name + + def non_terminal_states(self) -> set[str]: + return SageMakerHook.processing_job_non_terminal_states + + def failed_states(self) -> set[str]: + return SageMakerHook.processing_job_failed_states + + def get_sagemaker_response(self) -> dict: + self.log.info("Poking Sagemaker ProcessingJob %s", self.job_name) + return self.hook.describe_processing_job(self.job_name) + + def state_from_response(self, response: dict) -> str: + return response["ProcessingJobStatus"] diff --git a/providers/tests/amazon/aws/sensors/test_sagemaker_processing.py b/providers/tests/amazon/aws/sensors/test_sagemaker_processing.py new file mode 100644 index 0000000000000..0529a0b9b19d5 --- /dev/null +++ b/providers/tests/amazon/aws/sensors/test_sagemaker_processing.py @@ -0,0 +1,110 @@ +# +# 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 unittest import mock + +import pytest + +from airflow.exceptions import AirflowException +from airflow.providers.amazon.aws.hooks.sagemaker import SageMakerHook +from airflow.providers.amazon.aws.sensors.sagemaker import SageMakerProcessingSensor + +DESCRIBE_PROCESSING_INPROGRESS_RESPONSE = { + "ProcessingJobStatus": "InProgress", + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, +} + +DESCRIBE_PROCESSING_COMPLETED_RESPONSE = { + "ProcessingJobStatus": "Completed", + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, +} + +DESCRIBE_PROCESSING_FAILED_RESPONSE = { + "ProcessingJobStatus": "Failed", + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, + "FailureReason": "Unknown", +} + +DESCRIBE_PROCESSING_STOPPING_RESPONSE = { + "ProcessingJobStatus": "Stopping", + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, +} + +DESCRIBE_PROCESSING_STOPPED_RESPONSE = { + "ProcessingJobStatus": "Stopped", + "ResponseMetadata": { + "HTTPStatusCode": 200, + }, +} + + +class TestSageMakerProcessingSensor: + @mock.patch.object(SageMakerHook, "get_conn") + @mock.patch.object(SageMakerHook, "describe_processing_job") + def test_sensor_with_failure(self, mock_describe_job, mock_client): + mock_describe_job.side_effect = [DESCRIBE_PROCESSING_FAILED_RESPONSE] + sensor = SageMakerProcessingSensor( + task_id="test_task", poke_interval=2, aws_conn_id="aws_test", job_name="test_job_name" + ) + with pytest.raises(AirflowException): + sensor.execute(None) + mock_describe_job.assert_called_once_with("test_job_name") + + @mock.patch.object(SageMakerHook, "get_conn") + @mock.patch.object(SageMakerHook, "describe_processing_job") + def test_sensor_with_stopped(self, mock_describe_job, mock_client): + mock_describe_job.side_effect = [DESCRIBE_PROCESSING_STOPPED_RESPONSE] + sensor = SageMakerProcessingSensor( + task_id="test_task", poke_interval=2, aws_conn_id="aws_test", job_name="test_job_name" + ) + with pytest.raises(AirflowException): + sensor.execute(None) + mock_describe_job.assert_called_once_with("test_job_name") + + @mock.patch.object(SageMakerHook, "get_conn") + @mock.patch.object(SageMakerHook, "__init__") + @mock.patch.object(SageMakerHook, "describe_processing_job") + def test_sensor(self, mock_describe_job, hook_init, mock_client): + hook_init.return_value = None + + mock_describe_job.side_effect = [ + DESCRIBE_PROCESSING_INPROGRESS_RESPONSE, + DESCRIBE_PROCESSING_STOPPING_RESPONSE, + DESCRIBE_PROCESSING_COMPLETED_RESPONSE, + ] + sensor = SageMakerProcessingSensor( + task_id="test_task", poke_interval=0, aws_conn_id="aws_test", job_name="test_job_name" + ) + + sensor.execute(None) + + # make sure we called 3 times(terminated when its completed) + assert mock_describe_job.call_count == 3 + + # make sure the hook was initialized with the specific params + calls = [mock.call(aws_conn_id="aws_test")] + hook_init.assert_has_calls(calls) diff --git a/providers/tests/system/amazon/aws/example_sagemaker.py b/providers/tests/system/amazon/aws/example_sagemaker.py index 96e9756659975..3098ac647ea75 100644 --- a/providers/tests/system/amazon/aws/example_sagemaker.py +++ b/providers/tests/system/amazon/aws/example_sagemaker.py @@ -47,6 +47,7 @@ ) from airflow.providers.amazon.aws.sensors.sagemaker import ( SageMakerAutoMLSensor, + SageMakerProcessingSensor, SageMakerTrainingSensor, SageMakerTransformSensor, SageMakerTuningSensor, @@ -390,6 +391,7 @@ def set_up(env_id, role_arn): ti.xcom_push(key="raw_data_s3_key", value=raw_data_s3_key) ti.xcom_push(key="ecr_repository_name", value=ecr_repository_name) ti.xcom_push(key="processing_config", value=processing_config) + ti.xcom_push(key="processing_job_name", value=processing_job_name) ti.xcom_push(key="input_data_uri", value=input_data_uri) ti.xcom_push(key="output_data_uri", value=f"s3://{bucket_name}/{training_output_s3_key}") ti.xcom_push(key="training_config", value=training_config) @@ -518,8 +520,18 @@ def delete_docker_image(image_name): task_id="preprocess_raw_data", config=test_setup["processing_config"], ) + + # SageMakerProcessingOperator waits by default, setting as False to test the Sensor below. + preprocess_raw_data.wait_for_completion = False + # [END howto_operator_sagemaker_processing] + # [START howto_sensor_sagemaker_processing] + await_preprocess = SageMakerProcessingSensor( + task_id="await_preprocess", job_name=test_setup["processing_job_name"] + ) + # [END howto_sensor_sagemaker_processing] + # [START howto_operator_sagemaker_training] train_model = SageMakerTrainingOperator( task_id="train_model", @@ -622,6 +634,7 @@ def delete_docker_image(image_name): await_automl, create_experiment, preprocess_raw_data, + await_preprocess, train_model, await_training, create_model, From 68f8164a36db66e7096c1205abead183d34b8ae6 Mon Sep 17 00:00:00 2001 From: Oleksandr Slynko Date: Fri, 18 Oct 2024 15:07:10 +0100 Subject: [PATCH 004/258] Fix provider title in documentation (#43157) --------- Co-authored-by: Kaxil Naik --- docs/apache-airflow-providers-common-compat/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apache-airflow-providers-common-compat/index.rst b/docs/apache-airflow-providers-common-compat/index.rst index b5eadbddb0905..11d8d3f65b259 100644 --- a/docs/apache-airflow-providers-common-compat/index.rst +++ b/docs/apache-airflow-providers-common-compat/index.rst @@ -16,8 +16,8 @@ specific language governing permissions and limitations under the License. -``apache-airflow-providers-common-io`` -======================================= +``apache-airflow-providers-common-compat`` +========================================== .. toctree:: From 6ad428875e57e2d90678e657608d2d43c86b00c0 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Fri, 18 Oct 2024 22:23:47 +0800 Subject: [PATCH 005/258] AIP-84 Get Variables (#43083) * AIP-84 Get Variables * Remove value sorting for Variable --- .../endpoints/variable_endpoint.py | 1 + .../core_api/openapi/v1-generated.yaml | 75 ++++++++++++++++++- .../core_api/routes/public/variables.py | 45 ++++++++++- .../core_api/serializers/variables.py | 7 ++ airflow/ui/openapi-gen/queries/common.ts | 23 ++++++ airflow/ui/openapi-gen/queries/prefetch.ts | 30 ++++++++ airflow/ui/openapi-gen/queries/queries.ts | 36 +++++++++ airflow/ui/openapi-gen/queries/suspense.ts | 36 +++++++++ .../ui/openapi-gen/requests/schemas.gen.ts | 20 +++++ .../ui/openapi-gen/requests/services.gen.ts | 31 ++++++++ airflow/ui/openapi-gen/requests/types.gen.ts | 37 +++++++++ .../core_api/routes/public/test_variables.py | 42 +++++++++-- 12 files changed, 371 insertions(+), 12 deletions(-) diff --git a/airflow/api_connexion/endpoints/variable_endpoint.py b/airflow/api_connexion/endpoints/variable_endpoint.py index 20e7ce1edeabe..00f5abf00bbaa 100644 --- a/airflow/api_connexion/endpoints/variable_endpoint.py +++ b/airflow/api_connexion/endpoints/variable_endpoint.py @@ -70,6 +70,7 @@ def get_variable(*, variable_key: str, session: Session = NEW_SESSION) -> Respon return variable_schema.dump(var) +@mark_fastapi_migration_done @security.requires_access_variable("GET") @format_parameters({"limit": check_limit}) @provide_session diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index d7ee6340c6680..88e292364767f 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -805,6 +805,59 @@ paths: schema: $ref: '#/components/schemas/HTTPValidationError' /public/variables/: + get: + tags: + - Variable + summary: Get Variables + description: Get all Variables entries. + operationId: get_variables + parameters: + - name: limit + in: query + required: false + schema: + type: integer + default: 100 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + - name: order_by + in: query + required: false + schema: + type: string + default: id + title: Order By + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VariableCollectionResponse' + '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' post: tags: - Variable @@ -812,11 +865,11 @@ paths: description: Create a variable. operationId: post_variable requestBody: + required: true content: application/json: schema: $ref: '#/components/schemas/VariableBody' - required: true responses: '201': description: Successful Response @@ -825,17 +878,17 @@ paths: schema: $ref: '#/components/schemas/VariableResponse' '401': - description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/HTTPExceptionResponse' + description: Unauthorized '403': - description: Forbidden content: application/json: schema: $ref: '#/components/schemas/HTTPExceptionResponse' + description: Forbidden '422': description: Validation Error content: @@ -1840,6 +1893,22 @@ components: - value title: VariableBody description: Variable serializer for bodies. + VariableCollectionResponse: + properties: + variables: + items: + $ref: '#/components/schemas/VariableResponse' + type: array + title: Variables + total_entries: + type: integer + title: Total Entries + type: object + required: + - variables + - total_entries + title: VariableCollectionResponse + description: Variable Collection serializer for responses. VariableResponse: properties: key: diff --git a/airflow/api_fastapi/core_api/routes/public/variables.py b/airflow/api_fastapi/core_api/routes/public/variables.py index 3a46c519c9234..3110512c2cc65 100644 --- a/airflow/api_fastapi/core_api/routes/public/variables.py +++ b/airflow/api_fastapi/core_api/routes/public/variables.py @@ -21,10 +21,15 @@ from sqlalchemy.orm import Session from typing_extensions import Annotated -from airflow.api_fastapi.common.db.common import get_session +from airflow.api_fastapi.common.db.common import get_session, paginated_select +from airflow.api_fastapi.common.parameters import QueryLimit, QueryOffset, SortParam from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc -from airflow.api_fastapi.core_api.serializers.variables import VariableBody, VariableResponse +from airflow.api_fastapi.core_api.serializers.variables import ( + VariableBody, + VariableCollectionResponse, + VariableResponse, +) from airflow.models.variable import Variable variables_router = AirflowRouter(tags=["Variable"], prefix="/variables") @@ -58,6 +63,42 @@ async def get_variable( return VariableResponse.model_validate(variable, from_attributes=True) +@variables_router.get( + "/", + responses=create_openapi_http_exception_doc([401, 403]), +) +async def get_variables( + limit: QueryLimit, + offset: QueryOffset, + order_by: Annotated[ + SortParam, + Depends( + SortParam( + ["key", "id"], + Variable, + ).dynamic_depends() + ), + ], + session: Annotated[Session, Depends(get_session)], +) -> VariableCollectionResponse: + """Get all Variables entries.""" + variable_select, total_entries = paginated_select( + select(Variable), + [], + order_by=order_by, + offset=offset, + limit=limit, + session=session, + ) + + variables = session.scalars(variable_select).all() + + return VariableCollectionResponse( + variables=[VariableResponse.model_validate(variable, from_attributes=True) for variable in variables], + total_entries=total_entries, + ) + + @variables_router.patch("/{variable_key}", responses=create_openapi_http_exception_doc([400, 401, 403, 404])) async def patch_variable( variable_key: str, diff --git a/airflow/api_fastapi/core_api/serializers/variables.py b/airflow/api_fastapi/core_api/serializers/variables.py index 1ecc87425a24f..b328972544fd0 100644 --- a/airflow/api_fastapi/core_api/serializers/variables.py +++ b/airflow/api_fastapi/core_api/serializers/variables.py @@ -58,3 +58,10 @@ class VariableBody(VariableBase): """Variable serializer for bodies.""" value: str | None + + +class VariableCollectionResponse(BaseModel): + """Variable Collection serializer for responses.""" + + variables: list[VariableResponse] + total_entries: int diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 2a05104c8fa70..7c0ab1c646163 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -192,6 +192,29 @@ export const UseVariableServiceGetVariableKeyFn = ( }, queryKey?: Array, ) => [useVariableServiceGetVariableKey, ...(queryKey ?? [{ variableKey }])]; +export type VariableServiceGetVariablesDefaultResponse = Awaited< + ReturnType +>; +export type VariableServiceGetVariablesQueryResult< + TData = VariableServiceGetVariablesDefaultResponse, + TError = unknown, +> = UseQueryResult; +export const useVariableServiceGetVariablesKey = "VariableServiceGetVariables"; +export const UseVariableServiceGetVariablesKeyFn = ( + { + limit, + offset, + orderBy, + }: { + limit?: number; + offset?: number; + orderBy?: string; + } = {}, + queryKey?: Array, +) => [ + useVariableServiceGetVariablesKey, + ...(queryKey ?? [{ limit, offset, orderBy }]), +]; export type DagRunServiceGetDagRunDefaultResponse = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index 28145dc536470..98c706a63fc12 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -238,6 +238,36 @@ export const prefetchUseVariableServiceGetVariable = ( queryKey: Common.UseVariableServiceGetVariableKeyFn({ variableKey }), queryFn: () => VariableService.getVariable({ variableKey }), }); +/** + * Get Variables + * Get all Variables entries. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @returns VariableCollectionResponse Successful Response + * @throws ApiError + */ +export const prefetchUseVariableServiceGetVariables = ( + queryClient: QueryClient, + { + limit, + offset, + orderBy, + }: { + limit?: number; + offset?: number; + orderBy?: string; + } = {}, +) => + queryClient.prefetchQuery({ + queryKey: Common.UseVariableServiceGetVariablesKeyFn({ + limit, + offset, + orderBy, + }), + queryFn: () => VariableService.getVariables({ limit, offset, orderBy }), + }); /** * Get Dag Run * @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 7009a7dee7afe..9d3e640fbf98d 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -301,6 +301,42 @@ export const useVariableServiceGetVariable = < queryFn: () => VariableService.getVariable({ variableKey }) as TData, ...options, }); +/** + * Get Variables + * Get all Variables entries. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @returns VariableCollectionResponse Successful Response + * @throws ApiError + */ +export const useVariableServiceGetVariables = < + TData = Common.VariableServiceGetVariablesDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + orderBy, + }: { + limit?: number; + offset?: number; + orderBy?: string; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useQuery({ + queryKey: Common.UseVariableServiceGetVariablesKeyFn( + { limit, offset, orderBy }, + queryKey, + ), + queryFn: () => + VariableService.getVariables({ limit, offset, orderBy }) as TData, + ...options, + }); /** * Get Dag Run * @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 51e35f321a81c..d3d8b7b3441a4 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -296,6 +296,42 @@ export const useVariableServiceGetVariableSuspense = < queryFn: () => VariableService.getVariable({ variableKey }) as TData, ...options, }); +/** + * Get Variables + * Get all Variables entries. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @returns VariableCollectionResponse Successful Response + * @throws ApiError + */ +export const useVariableServiceGetVariablesSuspense = < + TData = Common.VariableServiceGetVariablesDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + orderBy, + }: { + limit?: number; + offset?: number; + orderBy?: string; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useSuspenseQuery({ + queryKey: Common.UseVariableServiceGetVariablesKeyFn( + { limit, offset, orderBy }, + queryKey, + ), + queryFn: () => + VariableService.getVariables({ limit, offset, orderBy }) as TData, + ...options, + }); /** * Get Dag Run * @param data The data for the request. diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index 981c8f659a3dd..f2214e2e30483 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1371,6 +1371,26 @@ export const $VariableBody = { description: "Variable serializer for bodies.", } as const; +export const $VariableCollectionResponse = { + properties: { + variables: { + items: { + $ref: "#/components/schemas/VariableResponse", + }, + type: "array", + title: "Variables", + }, + total_entries: { + type: "integer", + title: "Total Entries", + }, + }, + type: "object", + required: ["variables", "total_entries"], + title: "VariableCollectionResponse", + description: "Variable Collection serializer for responses.", +} as const; + export const $VariableResponse = { properties: { key: { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index 7516a625d71b0..1561c024a83d1 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -31,6 +31,8 @@ import type { GetVariableResponse, PatchVariableData, PatchVariableResponse, + GetVariablesData, + GetVariablesResponse, PostVariableData, PostVariableResponse, GetDagRunData, @@ -465,6 +467,35 @@ export class VariableService { }); } + /** + * Get Variables + * Get all Variables entries. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.orderBy + * @returns VariableCollectionResponse Successful Response + * @throws ApiError + */ + public static getVariables( + data: GetVariablesData = {}, + ): CancelablePromise { + return __request(OpenAPI, { + method: "GET", + url: "/public/variables/", + query: { + limit: data.limit, + offset: data.offset, + order_by: data.orderBy, + }, + errors: { + 401: "Unauthorized", + 403: "Forbidden", + 422: "Validation Error", + }, + }); + } + /** * Post Variable * Create a variable. diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 5f5ce6da71e3c..aae457b13afd1 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -308,6 +308,14 @@ export type VariableBody = { value: string | null; }; +/** + * Variable Collection serializer for responses. + */ +export type VariableCollectionResponse = { + variables: Array; + total_entries: number; +}; + /** * Variable serializer for responses. */ @@ -428,6 +436,14 @@ export type PatchVariableData = { export type PatchVariableResponse = VariableResponse; +export type GetVariablesData = { + limit?: number; + offset?: number; + orderBy?: string; +}; + +export type GetVariablesResponse = VariableCollectionResponse; + export type PostVariableData = { requestBody: VariableBody; }; @@ -812,6 +828,27 @@ export type $OpenApiTs = { }; }; "/public/variables/": { + get: { + req: GetVariablesData; + res: { + /** + * Successful Response + */ + 200: VariableCollectionResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; post: { req: PostVariableData; res: { diff --git a/tests/api_fastapi/core_api/routes/public/test_variables.py b/tests/api_fastapi/core_api/routes/public/test_variables.py index 0a52d1971075b..8e412c94cb98b 100644 --- a/tests/api_fastapi/core_api/routes/public/test_variables.py +++ b/tests/api_fastapi/core_api/routes/public/test_variables.py @@ -41,7 +41,7 @@ @provide_session -def _create_variable(session) -> None: +def _create_variables(session) -> None: Variable.set( key=TEST_VARIABLE_KEY, value=TEST_VARIABLE_VALUE, @@ -72,13 +72,13 @@ def setup(self) -> None: def teardown_method(self) -> None: clear_db_variables() - def create_variable(self): - _create_variable() + def create_variables(self): + _create_variables() class TestDeleteVariable(TestVariableEndpoint): def test_delete_should_respond_204(self, test_client, session): - self.create_variable() + self.create_variables() variables = session.query(Variable).all() assert len(variables) == 3 response = test_client.delete(f"/public/variables/{TEST_VARIABLE_KEY}") @@ -125,7 +125,7 @@ class TestGetVariable(TestVariableEndpoint): ], ) def test_get_should_respond_200(self, test_client, session, key, expected_response): - self.create_variable() + self.create_variables() response = test_client.get(f"/public/variables/{key}") assert response.status_code == 200 assert response.json() == expected_response @@ -137,6 +137,34 @@ def test_get_should_respond_404(self, test_client): assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found" == body["detail"] +class TestGetVariables(TestVariableEndpoint): + @pytest.mark.enable_redact + @pytest.mark.parametrize( + "query_params, expected_total_entries, expected_keys", + [ + # Filters + ({}, 3, [TEST_VARIABLE_KEY, TEST_VARIABLE_KEY2, TEST_VARIABLE_KEY3]), + ({"limit": 1}, 3, [TEST_VARIABLE_KEY]), + ({"limit": 1, "offset": 1}, 3, [TEST_VARIABLE_KEY2]), + # Sort + ({"order_by": "id"}, 3, [TEST_VARIABLE_KEY, TEST_VARIABLE_KEY2, TEST_VARIABLE_KEY3]), + ({"order_by": "-id"}, 3, [TEST_VARIABLE_KEY3, TEST_VARIABLE_KEY2, TEST_VARIABLE_KEY]), + ({"order_by": "key"}, 3, [TEST_VARIABLE_KEY3, TEST_VARIABLE_KEY2, TEST_VARIABLE_KEY]), + ({"order_by": "-key"}, 3, [TEST_VARIABLE_KEY, TEST_VARIABLE_KEY2, TEST_VARIABLE_KEY3]), + ], + ) + def test_should_respond_200( + self, session, test_client, query_params, expected_total_entries, expected_keys + ): + self.create_variables() + response = test_client.get("/public/variables/", params=query_params) + + assert response.status_code == 200 + body = response.json() + assert body["total_entries"] == expected_total_entries + assert [variable["key"] for variable in body["variables"]] == expected_keys + + class TestPatchVariable(TestVariableEndpoint): @pytest.mark.enable_redact @pytest.mark.parametrize( @@ -201,7 +229,7 @@ class TestPatchVariable(TestVariableEndpoint): ], ) def test_patch_should_respond_200(self, test_client, session, key, body, params, expected_response): - self.create_variable() + self.create_variables() response = test_client.patch(f"/public/variables/{key}", json=body, params=params) assert response.status_code == 200 assert response.json() == expected_response @@ -269,7 +297,7 @@ class TestPostVariable(TestVariableEndpoint): ], ) def test_post_should_respond_201(self, test_client, session, body, expected_response): - self.create_variable() + self.create_variables() response = test_client.post("/public/variables/", json=body) assert response.status_code == 201 assert response.json() == expected_response From 2a791feb1d4e69cd8cb26c5405e0d75828d82cdc Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Fri, 18 Oct 2024 16:58:49 +0100 Subject: [PATCH 006/258] Remove hard-coded logger in tests (#43160) The change itself should be ok but on checking other patterns from the codebase, I am changing it to using the logger from the sensor --- providers/tests/apache/hdfs/sensors/test_web_hdfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/tests/apache/hdfs/sensors/test_web_hdfs.py b/providers/tests/apache/hdfs/sensors/test_web_hdfs.py index 41ccb88e28e18..cc2db040705c9 100644 --- a/providers/tests/apache/hdfs/sensors/test_web_hdfs.py +++ b/providers/tests/apache/hdfs/sensors/test_web_hdfs.py @@ -72,7 +72,7 @@ def test_poke(self, mock_hook, caplog): expected_filenames=TEST_HDFS_FILENAMES, ) - with caplog.at_level("DEBUG", logger="airflow.task"): + with caplog.at_level("DEBUG", logger=sensor.log.name): result = sensor.poke(dict()) assert result From 7d6990fc7c5951e9b36000e3ee7282ba9c8aa9e3 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Fri, 18 Oct 2024 17:36:16 +0100 Subject: [PATCH 007/258] Add `omkar-foss` to the triage team (#43169) This will allow him to interact with the GitHub project for sig-debugging: https://github.com/apache/airflow/issues/40975 --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.asf.yaml b/.asf.yaml index ce0d702834bfa..7c6efaf928f85 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -123,6 +123,7 @@ github: # https://github.com/apache/infrastructure-asfyaml/blob/main/README.md#assigning-the-github-triage-role-to-external-collaborators - aritra24 - dirrao + - omkar-foss - rawwar - nathadfield - sunank200 From a6c2f37de29c66f377719df0965e49b7d0a63de4 Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:49:00 -0400 Subject: [PATCH 008/258] Remove `default` as auth backend (#43096) --- airflow/api/__init__.py | 8 ++--- airflow/api/auth/backend/default.py | 42 ------------------------- airflow/config_templates/config.yml | 5 ++- airflow/config_templates/unit_tests.cfg | 2 +- airflow/configuration.py | 10 +++--- airflow/www/extensions/init_security.py | 8 ++--- newsfragments/43096.significant.rst | 1 + tests/core/test_configuration.py | 2 +- 8 files changed, 14 insertions(+), 64 deletions(-) delete mode 100644 airflow/api/auth/backend/default.py create mode 100644 newsfragments/43096.significant.rst diff --git a/airflow/api/__init__.py b/airflow/api/__init__.py index d0613bb651faa..10c1ce6cea3c3 100644 --- a/airflow/api/__init__.py +++ b/airflow/api/__init__.py @@ -23,18 +23,14 @@ from importlib import import_module from airflow.configuration import conf -from airflow.exceptions import AirflowConfigException, AirflowException +from airflow.exceptions import AirflowException log = logging.getLogger(__name__) def load_auth(): """Load authentication backends.""" - auth_backends = "airflow.api.auth.backend.default" - try: - auth_backends = conf.get("api", "auth_backends") - except AirflowConfigException: - pass + auth_backends = conf.get("api", "auth_backends") backends = [] try: diff --git a/airflow/api/auth/backend/default.py b/airflow/api/auth/backend/default.py deleted file mode 100644 index afe2c88f35f0c..0000000000000 --- a/airflow/api/auth/backend/default.py +++ /dev/null @@ -1,42 +0,0 @@ -# -# 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. -"""Default authentication backend - everything is allowed.""" - -from __future__ import annotations - -from functools import wraps -from typing import Any, Callable, TypeVar, cast - -CLIENT_AUTH: tuple[str, str] | Any | None = None - - -def init_app(_): - """Initialize authentication backend.""" - - -T = TypeVar("T", bound=Callable) - - -def requires_authentication(function: T): - """Decorate functions that require authentication.""" - - @wraps(function) - def decorated(*args, **kwargs): - return function(*args, **kwargs) - - return cast(T, decorated) diff --git a/airflow/config_templates/config.yml b/airflow/config_templates/config.yml index 0be77a3b6829a..b7c810cfe5164 100644 --- a/airflow/config_templates/config.yml +++ b/airflow/config_templates/config.yml @@ -1377,12 +1377,11 @@ api: description: | Comma separated list of auth backends to authenticate users of the API. See `Security: API - `__ for possible values. - ("airflow.api.auth.backend.default" allows all requests for historic reasons) + `__ for possible values version_added: 2.3.0 type: string example: ~ - default: "airflow.api.auth.backend.session" + default: "airflow.providers.fab.auth_manager.api.auth.backend.session" maximum_page_limit: description: | Used to set the maximum page limit for API requests. If limit passed as param diff --git a/airflow/config_templates/unit_tests.cfg b/airflow/config_templates/unit_tests.cfg index 27134c7218215..b29c642afe77f 100644 --- a/airflow/config_templates/unit_tests.cfg +++ b/airflow/config_templates/unit_tests.cfg @@ -71,7 +71,7 @@ celery_logging_level = INFO smtp_mail_from = airflow@example.com [api] -auth_backends = airflow.api.auth.backend.default +auth_backends = airflow.providers.fab.auth_manager.api.auth.backend.session [hive] # Hive uses the configuration below to run the tests diff --git a/airflow/configuration.py b/airflow/configuration.py index 81dc18365392e..e59b5b5e9ec10 100644 --- a/airflow/configuration.py +++ b/airflow/configuration.py @@ -670,11 +670,11 @@ def _upgrade_auth_backends(self): This is required by the UI for ajax queries. """ old_value = self.get("api", "auth_backends", fallback="") - if old_value in ("airflow.api.auth.backend.default", ""): - # handled by deprecated_values - pass - elif old_value.find("airflow.api.auth.backend.session") == -1: - new_value = old_value + ",airflow.api.auth.backend.session" + if ( + old_value.find("airflow.api.auth.backend.session") == -1 + and old_value.find("airflow.providers.fab.auth_manager.api.auth.backend.session") == -1 + ): + new_value = old_value + ",airflow.providers.fab.auth_manager.api.auth.backend.session" self._update_env_var(section="api", name="auth_backends", new_value=new_value) self.upgraded_values[("api", "auth_backends")] = old_value diff --git a/airflow/www/extensions/init_security.py b/airflow/www/extensions/init_security.py index 28e96a06ca859..76b2944c47b18 100644 --- a/airflow/www/extensions/init_security.py +++ b/airflow/www/extensions/init_security.py @@ -20,7 +20,7 @@ from importlib import import_module from airflow.configuration import conf -from airflow.exceptions import AirflowConfigException, AirflowException +from airflow.exceptions import AirflowException log = logging.getLogger(__name__) @@ -46,11 +46,7 @@ def apply_caching(response): def init_api_auth(app): """Load authentication backends.""" - auth_backends = "airflow.api.auth.backend.default" - try: - auth_backends = conf.get("api", "auth_backends") - except AirflowConfigException: - pass + auth_backends = conf.get("api", "auth_backends") app.api_auth = [] try: diff --git a/newsfragments/43096.significant.rst b/newsfragments/43096.significant.rst new file mode 100644 index 0000000000000..b252e39916c03 --- /dev/null +++ b/newsfragments/43096.significant.rst @@ -0,0 +1 @@ +Removed auth backend ``airflow.api.auth.backend.default`` diff --git a/tests/core/test_configuration.py b/tests/core/test_configuration.py index 20109c6cd29b7..096b55e0f8e6f 100644 --- a/tests/core/test_configuration.py +++ b/tests/core/test_configuration.py @@ -664,7 +664,7 @@ def test_auth_backends_adds_session(self): test_conf.validate() assert ( test_conf.get("api", "auth_backends") - == "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth,airflow.api.auth.backend.session" + == "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth,airflow.providers.fab.auth_manager.api.auth.backend.session" ) def test_command_from_env(self): From 4edcfc879925b73c9582e88c8a73ee8619d5f2df Mon Sep 17 00:00:00 2001 From: Kalyan R Date: Fri, 18 Oct 2024 22:47:30 +0530 Subject: [PATCH 009/258] pin min opentelemetry-exporter-prom version (#43143) --- hatch_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index d289d36d8f519..2476745730a32 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -104,7 +104,7 @@ "plyvel>=1.5.1", ], "otel": [ - "opentelemetry-exporter-prometheus", + "opentelemetry-exporter-prometheus>=0.47b0", ], "pandas": [ # In pandas 2.2 minimal version of the sqlalchemy is 2.0 From 7635402e95be49cdb19b1ce871043c05b0101549 Mon Sep 17 00:00:00 2001 From: Amogh Desai Date: Sat, 19 Oct 2024 02:00:44 +0530 Subject: [PATCH 010/258] Making the security model more explicit (#43155) --- .github/SECURITY.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 4372b4528b477..4bcbd30dcad19 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -99,10 +99,11 @@ do not apply to Airflow, or have a different severity than some generic scoring The Airflow Security Team will get back to you after assessing the report. You will usually get confirmation that the issue is being worked (or that we quickly assessed it as invalid) within several -business days. Note that this is an Open-Source projects and members of the security team are volunteers -so please make sure to be patient. If you do not get a response within a week or so, please send a -kind reminder to the security team. We will usually let you know the CVE number that will be assigned -to the issue and the severity of the issue as well as release the issue is scheduled to be fixed +business days. Note that this is an Open-Source projects and members of the security team are volunteers, +so please make sure to be patient. If you do not get a response within a week, please send a kind reminder +to the security team about a lack of response; however, reminders should only be for the initial response +and not for updates on the assessment or remediation. We will usually let you know the CVE number that will +be assigned to the issue and the severity of the issue as well as release the issue is scheduled to be fixed after we assess the issue (which might take longer or shorter time depending on the issue complexity and potential impact, severity, whether we want to address a whole class issues in a single fix and a number of other factors). You should subscribe and monitor the `users@airflow.apache.org` mailing From c497c5dea686f2337d94094ef3aa25b271ac0881 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 18 Oct 2024 23:00:42 +0200 Subject: [PATCH 011/258] Fix side-effect of setting log handlers in tests (#43175) In some of our tests, handlers for loggers were set to the "logging.Handler()" and that created a side effect where when they were not cleaned by another test, they produced `NotImplementedError: emit must be implemented by Handler subclasses` message - because indeed `logging.Handler()` emit message raised NotImplementedError. The problem was that those tests (and some others) cleaned-up the handlers only BEFORE the tests but not AFTER - so if it happened that the same "test worker" run another test afterwards, that cleaned the handlers but did not set the handlers back, the side effect was gone. But when xdist arranged the tests to different pytest workers, the Handlers could remain registered in loggers so other tests could attempt to emit some logs in case they logged something to a parent logger. The fix applied is to create a fixture that cleans all loggers BEFORE and AFTER a test - and add the fixture to tests that modify handlers. --- tests/cli/commands/test_task_command.py | 2 +- tests/conftest.py | 23 +++++++++++++++++++++++ tests/jobs/test_triggerer_job.py | 4 +--- tests/jobs/test_triggerer_job_logging.py | 20 ++++---------------- tests_common/test_utils/log_handlers.py | 21 +++++++++++++++++++++ 5 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 tests_common/test_utils/log_handlers.py diff --git a/tests/cli/commands/test_task_command.py b/tests/cli/commands/test_task_command.py index 67204a1ad7b70..1de3f7b68d394 100644 --- a/tests/cli/commands/test_task_command.py +++ b/tests/cli/commands/test_task_command.py @@ -1070,7 +1070,7 @@ def test_apply(self, target_name): assert tgt.propagate is False if target_name else True # root propagate unchanged assert tgt.level == -1 - def test_apply_no_replace(self): + def test_apply_no_replace(self, clear_all_logger_handlers): """ Handlers, level and propagate should be applied on target. """ diff --git a/tests/conftest.py b/tests/conftest.py index c5c6441f0b55f..de13fe99c4bf6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,12 +16,15 @@ # under the License. from __future__ import annotations +import logging import os import sys from typing import TYPE_CHECKING import pytest +from tests_common.test_utils.log_handlers import non_pytest_handlers + # We should set these before loading _any_ of the rest of airflow so that the # unit test mode config is set as early as possible. assert "airflow" not in sys.modules, "No airflow module can be imported before these lines" @@ -58,6 +61,26 @@ def reset_environment(): os.environ[key] = init_env[key] +def remove_non_pytest_log_handlers(logger, *classes): + for handler in non_pytest_handlers(logger.handlers): + logger.removeHandler(handler) + + +def remove_all_non_pytest_log_handlers(): + # Remove handlers from the root logger + remove_non_pytest_log_handlers(logging.getLogger()) + # Remove handlers from all other loggers + for logger_name in logging.root.manager.loggerDict: + remove_non_pytest_log_handlers(logging.getLogger(logger_name)) + + +@pytest.fixture +def clear_all_logger_handlers(): + remove_all_non_pytest_log_handlers() + yield + remove_all_non_pytest_log_handlers() + + if TYPE_CHECKING: # Static checkers do not know about pytest fixtures' types and return, # In case if them distributed through third party packages. diff --git a/tests/jobs/test_triggerer_job.py b/tests/jobs/test_triggerer_job.py index cc457687b0900..3ad03b0c35b76 100644 --- a/tests/jobs/test_triggerer_job.py +++ b/tests/jobs/test_triggerer_job.py @@ -49,6 +49,7 @@ from tests.core.test_logging_config import reset_logging from tests_common.test_utils.db import clear_db_dags, clear_db_runs +from tests_common.test_utils.log_handlers import non_pytest_handlers pytestmark = pytest.mark.db_test @@ -772,9 +773,6 @@ def test_queue_listener(): importlib.reload(airflow_local_settings) configure_logging() - def non_pytest_handlers(val): - return [h for h in val if "pytest" not in h.__module__] - import logging log = logging.getLogger() diff --git a/tests/jobs/test_triggerer_job_logging.py b/tests/jobs/test_triggerer_job_logging.py index 335f9e6b81cc2..5e6a8e3ac8930 100644 --- a/tests/jobs/test_triggerer_job_logging.py +++ b/tests/jobs/test_triggerer_job_logging.py @@ -32,10 +32,7 @@ from airflow.utils.log.trigger_handler import DropTriggerLogsFilter, TriggererHandlerWrapper from tests_common.test_utils.config import conf_vars - - -def non_pytest_handlers(val): - return [h for h in val if "pytest" not in h.__module__] +from tests_common.test_utils.log_handlers import non_pytest_handlers def assert_handlers(logger, *classes): @@ -44,12 +41,6 @@ def assert_handlers(logger, *classes): return handlers -def clear_logger_handlers(log): - for h in log.handlers[:]: - if "pytest" not in h.__module__: - log.removeHandler(h) - - @pytest.fixture(autouse=True) def reload_triggerer_job(): importlib.reload(triggerer_job_runner) @@ -64,7 +55,6 @@ def test_configure_trigger_log_handler_file(): """ # reset logging root_logger = logging.getLogger() - clear_logger_handlers(root_logger) configure_logging() # before config @@ -168,15 +158,12 @@ def _read(self, ti, try_number, metadata=None): ("non_file_task_handler", logging.Handler, not_found_message), ], ) -def test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg): +def test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg, clear_all_logger_handlers): """ No root handler configured. When non FileTaskHandler is configured, don't modify. When an incompatible subclass of FileTaskHandler is configured, don't modify. """ - # reset handlers - root_logger = logging.getLogger() - clear_logger_handlers(root_logger) with conf_vars( { @@ -190,6 +177,7 @@ def test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg): configure_logging() # no root handlers + root_logger = logging.getLogger() assert_handlers(root_logger) # default task logger @@ -328,7 +316,7 @@ def test_configure_trigger_log_handler_root_has_task_handler(): } -def test_configure_trigger_log_handler_root_not_file_task(): +def test_configure_trigger_log_handler_root_not_file_task(clear_all_logger_handlers): """ root: A handler that doesn't support trigger or inherit FileTaskHandler task: Supports triggerer diff --git a/tests_common/test_utils/log_handlers.py b/tests_common/test_utils/log_handlers.py new file mode 100644 index 0000000000000..b5335c4e1b74c --- /dev/null +++ b/tests_common/test_utils/log_handlers.py @@ -0,0 +1,21 @@ +# 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 + + +def non_pytest_handlers(handlers): + return [h for h in handlers if "pytest" not in h.__module__] From 6e9c53677a51ad234247d01a3cb8d2de725f9d56 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 19 Oct 2024 01:00:28 +0200 Subject: [PATCH 012/258] Remove ``from __future__`` from ``airflow/providers __init__.py`` (#43173) Cleans-up airflow and providers `__init__.py" files in order to get providers import work again. This is done by excluding the two `__init__.py` files from automated ruff isort rules adding `from __future__ import annotations`. That should finally get rid of the Intellij teething import problem that has been introduced in #42505. There were earlier - unsuccessful - attempts to fix it in the #43116 and #43081 that followed #42951 - but the key is that Pycharm requires the namespace's extend_path to be first "real" line of code in the `__init__.py` to understand that the package is an "explicit" namespace package - and it conflicts with the requirement of "from __future__ import annotations" to be the first line of Python code. Also this PR fixes following problem: * pytest_plugin expecting .asf.yml in "airflow" sources - even during compatibility tests with older version of airflow (where the .asf.yml is not present) --------- Co-authored-by: Kaxil Naik --- airflow/__init__.py | 5 +++- providers/src/airflow/providers/__init__.py | 4 ++- pyproject.toml | 3 +- tests_common/pytest_plugin.py | 33 +++++++++++---------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/airflow/__init__.py b/airflow/__init__.py index f6c40b5091bbc..287aa499faa42 100644 --- a/airflow/__init__.py +++ b/airflow/__init__.py @@ -15,7 +15,10 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from __future__ import annotations + +# We do not use "from __future__ import annotations" here because it is not supported +# by Pycharm when we want to make sure all imports in airflow work from namespace packages +# Adding it automatically is excluded in pyproject.toml via I002 ruff rule exclusion # Make `airflow` a namespace package, supporting installing # airflow.providers.* in different locations (i.e. one in site, and one in user diff --git a/providers/src/airflow/providers/__init__.py b/providers/src/airflow/providers/__init__.py index 66fbd04b36e4d..adfa8eb4b98ce 100644 --- a/providers/src/airflow/providers/__init__.py +++ b/providers/src/airflow/providers/__init__.py @@ -16,7 +16,9 @@ # specific language governing permissions and limitations # under the License. -from __future__ import annotations +# We do not use "from __future__ import annotations" here because it is not supported +# by Pycharm when we want to make sure all imports in airflow work from namespace packages +# Adding it automatically is excluded in pyproject.toml via I002 ruff rule exclusion # Make `airflow` a namespace package, supporting installing # airflow.providers.* in different locations (i.e. one in site, and one in user diff --git a/pyproject.toml b/pyproject.toml index 1b1f5954505d6..f4512a382621f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -346,9 +346,10 @@ section-order = [ testing = ["dev", "providers.tests", "task_sdk.tests", "tests_common", "tests"] [tool.ruff.lint.extend-per-file-ignores] -"airflow/__init__.py" = ["F401", "TCH004"] +"airflow/__init__.py" = ["F401", "TCH004", "I002"] "airflow/models/__init__.py" = ["F401", "TCH004"] "airflow/models/sqla_models.py" = ["F401"] +"providers/src/airflow/providers/__init__.py" = ["I002"] # The test_python.py is needed because adding __future__.annotations breaks runtime checks that are # needed for the test to work diff --git a/tests_common/pytest_plugin.py b/tests_common/pytest_plugin.py index f2dab17b2dddb..0c7ed57fea668 100644 --- a/tests_common/pytest_plugin.py +++ b/tests_common/pytest_plugin.py @@ -371,21 +371,24 @@ def initialize_airflow_tests(request): def pytest_configure(config: pytest.Config) -> None: # Ensure that the airflow sources dir is at the end of the sys path if it's not already there. Needed to # run import from `providers/tests/` - desired = AIRFLOW_SOURCES_ROOT_DIR.as_posix() - for path in sys.path: - if path == desired: - break - else: - # This "desired" path should be the Airflow source directory (repo root) - assert (AIRFLOW_SOURCES_ROOT_DIR / ".asf.yaml").exists(), f"Path {desired} is not Airflow root" - sys.path.append(desired) - - if (backend := config.getoption("backend", default=None)) and backend not in SUPPORTED_DB_BACKENDS: - msg = ( - f"Provided DB backend {backend!r} not supported, " - f"expected one of: {', '.join(map(repr, SUPPORTED_DB_BACKENDS))}" - ) - pytest.exit(msg, returncode=6) + if os.environ.get("USE_AIRFLOW_VERSION") == "": + # if USE_AIRFLOW_VERSION is not empty, we are running tests against the installed version of Airflow + # and providers so there is no need to add the sources directory to the path + desired = AIRFLOW_SOURCES_ROOT_DIR.as_posix() + for path in sys.path: + if path == desired: + break + else: + # This "desired" path should be the Airflow source directory (repo root) + assert (AIRFLOW_SOURCES_ROOT_DIR / ".asf.yaml").exists(), f"Path {desired} is not Airflow root" + sys.path.append(desired) + + if (backend := config.getoption("backend", default=None)) and backend not in SUPPORTED_DB_BACKENDS: + msg = ( + f"Provided DB backend {backend!r} not supported, " + f"expected one of: {', '.join(map(repr, SUPPORTED_DB_BACKENDS))}" + ) + pytest.exit(msg, returncode=6) config.addinivalue_line("markers", "integration(name): mark test to run with named integration") config.addinivalue_line("markers", "backend(name): mark test to run with named backend") From 025082aeeacc887eb06dc441d89aa81fd3d06766 Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:10:05 -0700 Subject: [PATCH 013/258] Add more robust handling of existing DagRun (#43168) Previously we would just try to create and if exists just move on. Now we'll make sure that we create the BackfillDagRun record with a note documenting that we did not create the dag run because it already existed. This is a stepping stone towards implementing "clear existing" behavior. --- ..._add_exception_reason_and_logical_date_.py | 53 + airflow/models/backfill.py | 119 +- airflow/utils/db.py | 2 +- docs/apache-airflow/img/airflow_erd.sha256 | 2 +- docs/apache-airflow/img/airflow_erd.svg | 3862 ++++++++--------- docs/apache-airflow/migrations-ref.rst | 4 +- tests/models/test_backfill.py | 29 +- 7 files changed, 2092 insertions(+), 1979 deletions(-) create mode 100644 airflow/migrations/versions/0039_3_0_0_add_exception_reason_and_logical_date_.py diff --git a/airflow/migrations/versions/0039_3_0_0_add_exception_reason_and_logical_date_.py b/airflow/migrations/versions/0039_3_0_0_add_exception_reason_and_logical_date_.py new file mode 100644 index 0000000000000..c726d3bc6532d --- /dev/null +++ b/airflow/migrations/versions/0039_3_0_0_add_exception_reason_and_logical_date_.py @@ -0,0 +1,53 @@ +# +# 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. + +""" +Add exception_reason and logical_date to BackfillDagRun. + +Revision ID: 3a8972ecb8f9 +Revises: fb2d4922cd79 +Create Date: 2024-10-18 16:24:38.932005 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +from airflow.utils.sqlalchemy import UtcDateTime + +revision = "3a8972ecb8f9" +down_revision = "fb2d4922cd79" +branch_labels = None +depends_on = None +airflow_version = "3.0.0" + + +def upgrade(): + """Apply Add exception_reason and logical_date to BackfillDagRun.""" + with op.batch_alter_table("backfill_dag_run", schema=None) as batch_op: + batch_op.add_column(sa.Column("exception_reason", sa.String(length=250), nullable=True)) + batch_op.add_column(sa.Column("logical_date", UtcDateTime(timezone=True), nullable=False)) + + +def downgrade(): + """Unapply Add exception_reason and logical_date to BackfillDagRun.""" + with op.batch_alter_table("backfill_dag_run", schema=None) as batch_op: + batch_op.drop_column("logical_date") + batch_op.drop_column("exception_reason") diff --git a/airflow/models/backfill.py b/airflow/models/backfill.py index aa9cb695b7579..37a9533113067 100644 --- a/airflow/models/backfill.py +++ b/airflow/models/backfill.py @@ -24,6 +24,7 @@ from __future__ import annotations import logging +from enum import Enum from typing import TYPE_CHECKING from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, UniqueConstraint, func, select, update @@ -82,15 +83,26 @@ def __repr__(self): return f"Backfill({self.dag_id=}, {self.from_date=}, {self.to_date=})" +class BackfillDagRunExceptionReason(str, Enum): + """ + Enum for storing reasons why dag run not created. + + :meta private: + """ + + ALREADY_EXISTS = "already exists" + UNKNOWN = "unknown" + + class BackfillDagRun(Base): """Mapping table between backfill run and dag run.""" __tablename__ = "backfill_dag_run" id = Column(Integer, primary_key=True, autoincrement=True) backfill_id = Column(Integer, nullable=False) - dag_run_id = Column( - Integer, nullable=True - ) # the run might already exist; we could store the reason we did not create + dag_run_id = Column(Integer, nullable=True) + exception_reason = Column(StringID()) + logical_date = Column(UtcDateTime, nullable=False) sort_ordinal = Column(Integer, nullable=False) backfill = relationship("Backfill", back_populates="backfill_dag_run_associations") @@ -119,6 +131,50 @@ def validate_sort_ordinal(self, key, val): return val +def _create_backfill_dag_run(dag, info, backfill_id, dag_run_conf, backfill_sort_ordinal, session): + from airflow.models import DagRun + + dr = session.scalar(select(DagRun).where(DagRun.execution_date == info.logical_date).limit(1)) + if dr: + session.add( + BackfillDagRun( + backfill_id=backfill_id, + dag_run_id=None, + logical_date=info.logical_date, + exception_reason=BackfillDagRunExceptionReason.ALREADY_EXISTS, + sort_ordinal=backfill_sort_ordinal, + ) + ) + log.info( + "dag run already exists for dag_id=%s backfill_id=%s, info=%s", + dag.dag_id, + backfill_id, + info, + ) + return + dr = dag.create_dagrun( + triggered_by=DagRunTriggeredByType.BACKFILL, + execution_date=info.logical_date, + data_interval=info.data_interval, + start_date=timezone.utcnow(), + state=DagRunState.QUEUED, + external_trigger=False, + conf=dag_run_conf, + run_type=DagRunType.BACKFILL_JOB, + creating_job_id=None, + session=session, + backfill_id=backfill_id, + ) + session.add( + BackfillDagRun( + backfill_id=backfill_id, + dag_run_id=dr.id, + sort_ordinal=backfill_sort_ordinal, + logical_date=info.logical_date, + ) + ) + + def _create_backfill( *, dag_id: str, @@ -168,37 +224,46 @@ def _create_backfill( dagrun_info_list = reversed([x for x in dag.iter_dagrun_infos_between(from_date, to_date)]) for info in dagrun_info_list: backfill_sort_ordinal += 1 - log.info("creating backfill dag run %s dag_id=%s backfill_id=%s, info=", dag.dag_id, br.id, info) - dr = None + session.commit() + from tenacity import RetryError, Retrying, stop_after_attempt + try: - dr = dag.create_dagrun( - triggered_by=DagRunTriggeredByType.BACKFILL, - execution_date=info.logical_date, - data_interval=info.data_interval, - start_date=timezone.utcnow(), - state=DagRunState.QUEUED, - external_trigger=False, - conf=br.dag_run_conf, - run_type=DagRunType.BACKFILL_JOB, - creating_job_id=None, - session=session, - backfill_id=br.id, - ) - except Exception: + for attempt in Retrying(stop=stop_after_attempt(3)): + # we do retries here because it's possible that we check to see if dr exists + # before we attempt to create the dag run. if something else creates the dag + # run in between, we'll have to retry the transaction + with attempt: + with session.begin(): + _create_backfill_dag_run( + dag=dag, + info=info, + backfill_id=br.id, + dag_run_conf=br.dag_run_conf, + backfill_sort_ordinal=backfill_sort_ordinal, + session=session, + ) + log.info( + "created backfill dag run dag_id=%s backfill_id=%s, info=%s", + dag.dag_id, + br.id, + info, + ) + except RetryError: dag.log.exception( "Error while attempting to create a dag run dag_id='%s' logical_date='%s'", dag.dag_id, info.logical_date, ) - session.rollback() - session.add( - BackfillDagRun( - backfill_id=br.id, - dag_run_id=dr.id if dr else None, # this means we failed to create the dag run - sort_ordinal=backfill_sort_ordinal, + session.add( + BackfillDagRun( + backfill_id=br.id, + dag_run_id=None, + exception_reason=BackfillDagRunExceptionReason.UNKNOWN, + logical_date=info.logical_date, + sort_ordinal=backfill_sort_ordinal, + ) ) - ) - session.commit() + session.commit() return br diff --git a/airflow/utils/db.py b/airflow/utils/db.py index 2fba821da6618..d6a367c4d91fe 100644 --- a/airflow/utils/db.py +++ b/airflow/utils/db.py @@ -96,7 +96,7 @@ class MappedClassProtocol(Protocol): "2.9.0": "1949afb29106", "2.9.2": "686269002441", "2.10.0": "22ed7efa9da2", - "3.0.0": "fb2d4922cd79", + "3.0.0": "3a8972ecb8f9", } diff --git a/docs/apache-airflow/img/airflow_erd.sha256 b/docs/apache-airflow/img/airflow_erd.sha256 index ac9ab5b14d6cd..6d096f7324eb5 100644 --- a/docs/apache-airflow/img/airflow_erd.sha256 +++ b/docs/apache-airflow/img/airflow_erd.sha256 @@ -1 +1 @@ -b39a456ac1bd3aa91198d036eaa3859f25a47151164974f6b4fe7f998d41f720 \ No newline at end of file +15cd7929c7e5bd787115396bf4e3ba6e6a228824ee8fc7f8406d19da82051d01 \ No newline at end of file diff --git a/docs/apache-airflow/img/airflow_erd.svg b/docs/apache-airflow/img/airflow_erd.svg index 833f2f73bbb3e..f9165e9ee4c4c 100644 --- a/docs/apache-airflow/img/airflow_erd.svg +++ b/docs/apache-airflow/img/airflow_erd.svg @@ -4,11 +4,11 @@ - - + + %3 - + log @@ -64,2366 +64,2338 @@ [INTEGER] - - -job - -job - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - -end_date - - [TIMESTAMP] - -executor_class - - [VARCHAR(500)] - -hostname - - [VARCHAR(500)] - -job_type - - [VARCHAR(30)] - -latest_heartbeat - - [TIMESTAMP] - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -unixname - - [VARCHAR(1000)] - - + slot_pool - -slot_pool - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -include_deferred - - [BOOLEAN] - NOT NULL - -pool - - [VARCHAR(256)] - -slots - - [INTEGER] + +slot_pool + +id + + [INTEGER] + NOT NULL + +description + + [TEXT] + +include_deferred + + [BOOLEAN] + NOT NULL + +pool + + [VARCHAR(256)] + +slots + + [INTEGER] - + callback_request - -callback_request - -id - - [INTEGER] - NOT NULL - -callback_data - - [JSON] - NOT NULL - -callback_type - - [VARCHAR(20)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -priority_weight - - [INTEGER] - NOT NULL - -processor_subdir - - [VARCHAR(2000)] + +callback_request + +id + + [INTEGER] + NOT NULL + +callback_data + + [JSON] + NOT NULL + +callback_type + + [VARCHAR(20)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +priority_weight + + [INTEGER] + NOT NULL + +processor_subdir + + [VARCHAR(2000)] - + dag_priority_parsing_request - -dag_priority_parsing_request - -id - - [VARCHAR(32)] - NOT NULL - -fileloc - - [VARCHAR(2000)] - NOT NULL + +dag_priority_parsing_request + +id + + [VARCHAR(32)] + NOT NULL + +fileloc + + [VARCHAR(2000)] + NOT NULL - + dag_code - -dag_code - -fileloc_hash - - [BIGINT] - NOT NULL - -fileloc - - [VARCHAR(2000)] - NOT NULL - -last_updated - - [TIMESTAMP] - NOT NULL - -source_code - - [TEXT] - NOT NULL + +dag_code + +fileloc_hash + + [BIGINT] + NOT NULL + +fileloc + + [VARCHAR(2000)] + NOT NULL + +last_updated + + [TIMESTAMP] + NOT NULL + +source_code + + [TEXT] + NOT NULL - + dag_pickle - -dag_pickle - -id - - [INTEGER] - NOT NULL - -created_dttm - - [TIMESTAMP] - -pickle - - [BYTEA] - -pickle_hash - - [BIGINT] + +dag_pickle + +id + + [INTEGER] + NOT NULL + +created_dttm + + [TIMESTAMP] + +pickle + + [BYTEA] + +pickle_hash + + [BIGINT] - + connection - -connection - -id - - [INTEGER] - NOT NULL - -conn_id - - [VARCHAR(250)] - NOT NULL - -conn_type - - [VARCHAR(500)] - NOT NULL - -description - - [TEXT] - -extra - - [TEXT] - -host - - [VARCHAR(500)] - -is_encrypted - - [BOOLEAN] - -is_extra_encrypted - - [BOOLEAN] - -login - - [TEXT] - -password - - [TEXT] - -port - - [INTEGER] - -schema - - [VARCHAR(500)] - - - -sla_miss - -sla_miss - -dag_id - - [VARCHAR(250)] - NOT NULL - -execution_date - - [TIMESTAMP] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -description - - [TEXT] - -email_sent - - [BOOLEAN] - -notification_sent - - [BOOLEAN] - -timestamp - - [TIMESTAMP] + +connection + +id + + [INTEGER] + NOT NULL + +conn_id + + [VARCHAR(250)] + NOT NULL + +conn_type + + [VARCHAR(500)] + NOT NULL + +description + + [TEXT] + +extra + + [TEXT] + +host + + [VARCHAR(500)] + +is_encrypted + + [BOOLEAN] + +is_extra_encrypted + + [BOOLEAN] + +login + + [TEXT] + +password + + [TEXT] + +port + + [INTEGER] + +schema + + [VARCHAR(500)] - + variable - -variable - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -is_encrypted - - [BOOLEAN] - -key - - [VARCHAR(250)] - -val - - [TEXT] + +variable + +id + + [INTEGER] + NOT NULL + +description + + [TEXT] + +is_encrypted + + [BOOLEAN] + +key + + [VARCHAR(250)] + +val + + [TEXT] + + + +import_error + +import_error + +id + + [INTEGER] + NOT NULL + +filename + + [VARCHAR(1024)] + +processor_subdir + + [VARCHAR(2000)] + +stacktrace + + [TEXT] + +timestamp + + [TIMESTAMP] + + + +job + +job + +id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + +end_date + + [TIMESTAMP] + +executor_class + + [VARCHAR(500)] + +hostname + + [VARCHAR(500)] + +job_type + + [VARCHAR(30)] + +latest_heartbeat + + [TIMESTAMP] + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +unixname + + [VARCHAR(1000)] serialized_dag - -serialized_dag - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_hash - - [VARCHAR(32)] - NOT NULL + +serialized_dag -data - - [JSON] +dag_id + + [VARCHAR(250)] + NOT NULL -data_compressed - - [BYTEA] +dag_hash + + [VARCHAR(32)] + NOT NULL -fileloc - - [VARCHAR(2000)] - NOT NULL +data + + [JSON] -fileloc_hash - - [BIGINT] - NOT NULL +data_compressed + + [BYTEA] -last_updated - - [TIMESTAMP] - NOT NULL +fileloc + + [VARCHAR(2000)] + NOT NULL -processor_subdir - - [VARCHAR(2000)] - - - -import_error - -import_error - -id - - [INTEGER] - NOT NULL - -filename - - [VARCHAR(1024)] - -processor_subdir - - [VARCHAR(2000)] - -stacktrace - - [TEXT] - -timestamp - - [TIMESTAMP] +fileloc_hash + + [BIGINT] + NOT NULL + +last_updated + + [TIMESTAMP] + NOT NULL + +processor_subdir + + [VARCHAR(2000)] - + dataset_alias - -dataset_alias - -id - - [INTEGER] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL + +dataset_alias + +id + + [INTEGER] + NOT NULL + +group + + [VARCHAR(1500)] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL - + dataset_alias_dataset - -dataset_alias_dataset - -alias_id - - [INTEGER] - NOT NULL - -dataset_id - - [INTEGER] - NOT NULL + +dataset_alias_dataset + +alias_id + + [INTEGER] + NOT NULL + +dataset_id + + [INTEGER] + NOT NULL dataset_alias--dataset_alias_dataset - -0..N -1 + +0..N +1 dataset_alias--dataset_alias_dataset - -0..N -1 + +0..N +1 - + dataset_alias_dataset_event - -dataset_alias_dataset_event - -alias_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL + +dataset_alias_dataset_event + +alias_id + + [INTEGER] + NOT NULL + +event_id + + [INTEGER] + NOT NULL dataset_alias--dataset_alias_dataset_event - -0..N -1 + +0..N +1 dataset_alias--dataset_alias_dataset_event - -0..N -1 + +0..N +1 - + dag_schedule_dataset_alias_reference - -dag_schedule_dataset_alias_reference - -alias_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + +dag_schedule_dataset_alias_reference + +alias_id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL dataset_alias--dag_schedule_dataset_alias_reference - -0..N -1 + +0..N +1 - + dataset - -dataset - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -extra - - [JSON] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL + +dataset + +id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +extra + + [JSON] + NOT NULL + +group + + [VARCHAR(1500)] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +uri + + [VARCHAR(1500)] + NOT NULL dataset--dataset_alias_dataset - -0..N -1 + +0..N +1 dataset--dataset_alias_dataset - -0..N -1 + +0..N +1 - + asset_active - -asset_active - -name - - [VARCHAR(1500)] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL + +asset_active + +name + + [VARCHAR(1500)] + NOT NULL + +uri + + [VARCHAR(1500)] + NOT NULL dataset--asset_active - -1 -1 + +1 +1 dataset--asset_active - -1 -1 + +1 +1 - + dag_schedule_dataset_reference - -dag_schedule_dataset_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -dataset_id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + +dag_schedule_dataset_reference + +dag_id + + [VARCHAR(250)] + NOT NULL + +dataset_id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL dataset--dag_schedule_dataset_reference - -0..N -1 + +0..N +1 - + task_outlet_dataset_reference - -task_outlet_dataset_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -dataset_id - - [INTEGER] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + +task_outlet_dataset_reference + +dag_id + + [VARCHAR(250)] + NOT NULL + +dataset_id + + [INTEGER] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL dataset--task_outlet_dataset_reference - -0..N -1 + +0..N +1 - + dataset_dag_run_queue - -dataset_dag_run_queue - -dataset_id - - [INTEGER] - NOT NULL - -target_dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL + +dataset_dag_run_queue + +dataset_id + + [INTEGER] + NOT NULL + +target_dag_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL dataset--dataset_dag_run_queue - -0..N -1 + +0..N +1 - + dataset_event - -dataset_event - -id - - [INTEGER] - NOT NULL - -dataset_id - - [INTEGER] - NOT NULL - -extra - - [JSON] - NOT NULL - -source_dag_id - - [VARCHAR(250)] - -source_map_index - - [INTEGER] - -source_run_id - - [VARCHAR(250)] - -source_task_id - - [VARCHAR(250)] - -timestamp - - [TIMESTAMP] - NOT NULL + +dataset_event + +id + + [INTEGER] + NOT NULL + +dataset_id + + [INTEGER] + NOT NULL + +extra + + [JSON] + NOT NULL + +source_dag_id + + [VARCHAR(250)] + +source_map_index + + [INTEGER] + +source_run_id + + [VARCHAR(250)] + +source_task_id + + [VARCHAR(250)] + +timestamp + + [TIMESTAMP] + NOT NULL dataset_event--dataset_alias_dataset_event - -0..N -1 + +0..N +1 dataset_event--dataset_alias_dataset_event - -0..N -1 + +0..N +1 - + dagrun_dataset_event - -dagrun_dataset_event - -dag_run_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL + +dagrun_dataset_event + +dag_run_id + + [INTEGER] + NOT NULL + +event_id + + [INTEGER] + NOT NULL dataset_event--dagrun_dataset_event - -0..N -1 + +0..N +1 - + dag - -dag - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_display_name - - [VARCHAR(2000)] - -dataset_expression - - [JSON] - -default_view - - [VARCHAR(25)] - -description - - [TEXT] - -fileloc - - [VARCHAR(2000)] - -has_import_errors - - [BOOLEAN] - -has_task_concurrency_limits - - [BOOLEAN] - NOT NULL - -is_active - - [BOOLEAN] - -is_paused - - [BOOLEAN] - -last_expired - - [TIMESTAMP] - -last_parsed_time - - [TIMESTAMP] - -last_pickled - - [TIMESTAMP] - -max_active_runs - - [INTEGER] - -max_active_tasks - - [INTEGER] - NOT NULL - -max_consecutive_failed_dag_runs - - [INTEGER] - NOT NULL - -next_dagrun - - [TIMESTAMP] - -next_dagrun_create_after - - [TIMESTAMP] - -next_dagrun_data_interval_end - - [TIMESTAMP] - -next_dagrun_data_interval_start - - [TIMESTAMP] - -owners - - [VARCHAR(2000)] - -pickle_id - - [INTEGER] - -processor_subdir - - [VARCHAR(2000)] - -scheduler_lock - - [BOOLEAN] - -timetable_description - - [VARCHAR(1000)] - -timetable_summary - - [TEXT] + +dag + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_display_name + + [VARCHAR(2000)] + +dataset_expression + + [JSON] + +default_view + + [VARCHAR(25)] + +description + + [TEXT] + +fileloc + + [VARCHAR(2000)] + +has_import_errors + + [BOOLEAN] + +has_task_concurrency_limits + + [BOOLEAN] + NOT NULL + +is_active + + [BOOLEAN] + +is_paused + + [BOOLEAN] + +last_expired + + [TIMESTAMP] + +last_parsed_time + + [TIMESTAMP] + +last_pickled + + [TIMESTAMP] + +max_active_runs + + [INTEGER] + +max_active_tasks + + [INTEGER] + NOT NULL + +max_consecutive_failed_dag_runs + + [INTEGER] + NOT NULL + +next_dagrun + + [TIMESTAMP] + +next_dagrun_create_after + + [TIMESTAMP] + +next_dagrun_data_interval_end + + [TIMESTAMP] + +next_dagrun_data_interval_start + + [TIMESTAMP] + +owners + + [VARCHAR(2000)] + +pickle_id + + [INTEGER] + +processor_subdir + + [VARCHAR(2000)] + +scheduler_lock + + [BOOLEAN] + +timetable_description + + [VARCHAR(1000)] + +timetable_summary + + [TEXT] dag--dag_schedule_dataset_alias_reference - -0..N -1 + +0..N +1 dag--dag_schedule_dataset_reference - -0..N -1 + +0..N +1 dag--task_outlet_dataset_reference - -0..N -1 + +0..N +1 dag--dataset_dag_run_queue - -0..N -1 + +0..N +1 - + dag_tag - -dag_tag - -dag_id - - [VARCHAR(250)] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL + +dag_tag + +dag_id + + [VARCHAR(250)] + NOT NULL + +name + + [VARCHAR(100)] + NOT NULL dag--dag_tag - -0..N -1 + +0..N +1 - + dag_owner_attributes - -dag_owner_attributes - -dag_id - - [VARCHAR(250)] - NOT NULL - -owner - - [VARCHAR(500)] - NOT NULL - -link - - [VARCHAR(500)] - NOT NULL + +dag_owner_attributes + +dag_id + + [VARCHAR(250)] + NOT NULL + +owner + + [VARCHAR(500)] + NOT NULL + +link + + [VARCHAR(500)] + NOT NULL dag--dag_owner_attributes - -0..N -1 + +0..N +1 - + dag_warning - -dag_warning - -dag_id - - [VARCHAR(250)] - NOT NULL - -warning_type - - [VARCHAR(50)] - NOT NULL - -message - - [TEXT] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL + +dag_warning + +dag_id + + [VARCHAR(250)] + NOT NULL + +warning_type + + [VARCHAR(50)] + NOT NULL + +message + + [TEXT] + NOT NULL + +timestamp + + [TIMESTAMP] + NOT NULL dag--dag_warning - -0..N -1 + +0..N +1 - + log_template - -log_template - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -elasticsearch_id - - [TEXT] - NOT NULL - -filename - - [TEXT] - NOT NULL + +log_template + +id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +elasticsearch_id + + [TEXT] + NOT NULL + +filename + + [TEXT] + NOT NULL - + dag_run - -dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - -clear_number - - [INTEGER] - NOT NULL - -conf - - [BYTEA] - -creating_job_id - - [INTEGER] - -dag_hash - - [VARCHAR(32)] - -dag_id - - [VARCHAR(250)] - NOT NULL - -data_interval_end - - [TIMESTAMP] - -data_interval_start - - [TIMESTAMP] - -end_date - - [TIMESTAMP] - -external_trigger - - [BOOLEAN] - -last_scheduling_decision - - [TIMESTAMP] - -log_template_id - - [INTEGER] - -logical_date - - [TIMESTAMP] - NOT NULL - -queued_at - - [TIMESTAMP] - -run_id - - [VARCHAR(250)] - NOT NULL - -run_type - - [VARCHAR(50)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(50)] - -triggered_by - - [VARCHAR(50)] - -updated_at - - [TIMESTAMP] + +dag_run + +id + + [INTEGER] + NOT NULL + +backfill_id + + [INTEGER] + +clear_number + + [INTEGER] + NOT NULL + +conf + + [BYTEA] + +creating_job_id + + [INTEGER] + +dag_hash + + [VARCHAR(32)] + +dag_id + + [VARCHAR(250)] + NOT NULL + +data_interval_end + + [TIMESTAMP] + +data_interval_start + + [TIMESTAMP] + +end_date + + [TIMESTAMP] + +external_trigger + + [BOOLEAN] + +last_scheduling_decision + + [TIMESTAMP] + +log_template_id + + [INTEGER] + +logical_date + + [TIMESTAMP] + NOT NULL + +queued_at + + [TIMESTAMP] + +run_id + + [VARCHAR(250)] + NOT NULL + +run_type + + [VARCHAR(50)] + NOT NULL + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(50)] + +triggered_by + + [VARCHAR(50)] + +updated_at + + [TIMESTAMP] log_template--dag_run - -0..N -{0,1} + +0..N +{0,1} dag_run--dagrun_dataset_event - -0..N -1 + +0..N +1 - + task_instance - -task_instance - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -custom_operator_name - - [VARCHAR(1000)] - -duration - - [DOUBLE_PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - -external_executor_id - - [VARCHAR(250)] - -hostname - - [VARCHAR(1000)] - -job_id - - [INTEGER] - -max_tries - - [INTEGER] - -next_kwargs - - [JSON] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - -queue - - [VARCHAR(256)] - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - -unixname - - [VARCHAR(1000)] - -updated_at - - [TIMESTAMP] + +task_instance + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +custom_operator_name + + [VARCHAR(1000)] + +duration + + [DOUBLE_PRECISION] + +end_date + + [TIMESTAMP] + +executor + + [VARCHAR(1000)] + +executor_config + + [BYTEA] + +external_executor_id + + [VARCHAR(250)] + +hostname + + [VARCHAR(1000)] + +job_id + + [INTEGER] + +max_tries + + [INTEGER] + +next_kwargs + + [JSON] + +next_method + + [VARCHAR(1000)] + +operator + + [VARCHAR(1000)] + +pid + + [INTEGER] + +pool + + [VARCHAR(256)] + NOT NULL + +pool_slots + + [INTEGER] + NOT NULL + +priority_weight + + [INTEGER] + +queue + + [VARCHAR(256)] + +queued_by_job_id + + [INTEGER] + +queued_dttm + + [TIMESTAMP] + +rendered_map_index + + [VARCHAR(250)] + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +task_display_name + + [VARCHAR(2000)] + +trigger_id + + [INTEGER] + +trigger_timeout + + [TIMESTAMP] + +try_number + + [INTEGER] + +unixname + + [VARCHAR(1000)] + +updated_at + + [TIMESTAMP] dag_run--task_instance - -0..N -1 + +0..N +1 dag_run--task_instance - -0..N -1 + +0..N +1 + + + +backfill_dag_run + +backfill_dag_run + +id + + [INTEGER] + NOT NULL + +backfill_id + + [INTEGER] + NOT NULL + +dag_run_id + + [INTEGER] + +exception_reason + + [VARCHAR(250)] + +logical_date + + [TIMESTAMP] + NOT NULL + +sort_ordinal + + [INTEGER] + NOT NULL + + + +dag_run--backfill_dag_run + +0..N +{0,1} dag_run_note - -dag_run_note - -dag_run_id - - [INTEGER] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] + +dag_run_note + +dag_run_id + + [INTEGER] + NOT NULL + +content + + [VARCHAR(1000)] + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +user_id + + [VARCHAR(128)] - -dag_run--dag_run_note - -1 -1 - - - -backfill_dag_run - -backfill_dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - NOT NULL - -dag_run_id - - [INTEGER] - -sort_ordinal - - [INTEGER] - NOT NULL - - -dag_run--backfill_dag_run - -0..N -{0,1} +dag_run--dag_run_note + +1 +1 - + task_reschedule - -task_reschedule - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -duration - - [INTEGER] - NOT NULL - -end_date - - [TIMESTAMP] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -reschedule_date - - [TIMESTAMP] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -try_number - - [INTEGER] - NOT NULL + +task_reschedule + +id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +duration + + [INTEGER] + NOT NULL + +end_date + + [TIMESTAMP] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +reschedule_date + + [TIMESTAMP] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +try_number + + [INTEGER] + NOT NULL dag_run--task_reschedule - -0..N -1 + +0..N +1 dag_run--task_reschedule - -0..N -1 + +0..N +1 task_instance--task_reschedule - -0..N -1 + +0..N +1 task_instance--task_reschedule - -0..N -1 + +0..N +1 task_instance--task_reschedule - -0..N -1 + +0..N +1 task_instance--task_reschedule - -0..N -1 + +0..N +1 - + rendered_task_instance_fields - -rendered_task_instance_fields - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -k8s_pod_yaml - - [JSON] - -rendered_fields - - [JSON] - NOT NULL + +rendered_task_instance_fields + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +k8s_pod_yaml + + [JSON] + +rendered_fields + + [JSON] + NOT NULL task_instance--rendered_task_instance_fields - -0..N -1 + +0..N +1 task_instance--rendered_task_instance_fields - -0..N -1 + +0..N +1 task_instance--rendered_task_instance_fields - -0..N -1 + +0..N +1 task_instance--rendered_task_instance_fields - -0..N -1 + +0..N +1 - + task_fail - -task_fail - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -duration - - [INTEGER] - -end_date - - [TIMESTAMP] - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -task_id - - [VARCHAR(250)] - NOT NULL + +task_fail + +id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +duration + + [INTEGER] + +end_date + + [TIMESTAMP] + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + +task_id + + [VARCHAR(250)] + NOT NULL task_instance--task_fail - -0..N -1 + +0..N +1 task_instance--task_fail - -0..N -1 + +0..N +1 task_instance--task_fail - -0..N -1 + +0..N +1 task_instance--task_fail - -0..N -1 + +0..N +1 - + task_map - -task_map - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -keys - - [JSON] - -length - - [INTEGER] - NOT NULL + +task_map + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +keys + + [JSON] + +length + + [INTEGER] + NOT NULL task_instance--task_map - -0..N -1 + +0..N +1 task_instance--task_map - -0..N -1 + +0..N +1 task_instance--task_map - -0..N -1 + +0..N +1 task_instance--task_map - -0..N -1 + +0..N +1 - + xcom - -xcom - -dag_run_id - - [INTEGER] - NOT NULL - -key - - [VARCHAR(512)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL - -value - - [BYTEA] + +xcom + +dag_run_id + + [INTEGER] + NOT NULL + +key + + [VARCHAR(512)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +timestamp + + [TIMESTAMP] + NOT NULL + +value + + [BYTEA] task_instance--xcom - -0..N -1 + +0..N +1 task_instance--xcom - -0..N -1 + +0..N +1 task_instance--xcom - -0..N -1 + +0..N +1 task_instance--xcom - -0..N -1 + +0..N +1 - + task_instance_note - -task_instance_note - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] + +task_instance_note + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +content + + [VARCHAR(1000)] + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +user_id + + [VARCHAR(128)] task_instance--task_instance_note - -0..N -1 + +0..N +1 task_instance--task_instance_note - -0..N -1 + +0..N +1 task_instance--task_instance_note - -0..N -1 + +0..N +1 task_instance--task_instance_note - -0..N -1 + +0..N +1 - + task_instance_history - -task_instance_history - -id - - [INTEGER] - NOT NULL - -custom_operator_name - - [VARCHAR(1000)] - -dag_id - - [VARCHAR(250)] - NOT NULL - -duration - - [DOUBLE_PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - -external_executor_id - - [VARCHAR(250)] - -hostname - - [VARCHAR(1000)] - -job_id - - [INTEGER] - -map_index - - [INTEGER] - NOT NULL - -max_tries - - [INTEGER] - -next_kwargs - - [JSON] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - -queue - - [VARCHAR(256)] - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -run_id - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -task_id - - [VARCHAR(250)] - NOT NULL - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - NOT NULL - -unixname - - [VARCHAR(1000)] - -updated_at - - [TIMESTAMP] + +task_instance_history + +id + + [INTEGER] + NOT NULL + +custom_operator_name + + [VARCHAR(1000)] + +dag_id + + [VARCHAR(250)] + NOT NULL + +duration + + [DOUBLE_PRECISION] + +end_date + + [TIMESTAMP] + +executor + + [VARCHAR(1000)] + +executor_config + + [BYTEA] + +external_executor_id + + [VARCHAR(250)] + +hostname + + [VARCHAR(1000)] + +job_id + + [INTEGER] + +map_index + + [INTEGER] + NOT NULL + +max_tries + + [INTEGER] + +next_kwargs + + [JSON] + +next_method + + [VARCHAR(1000)] + +operator + + [VARCHAR(1000)] + +pid + + [INTEGER] + +pool + + [VARCHAR(256)] + NOT NULL + +pool_slots + + [INTEGER] + NOT NULL + +priority_weight + + [INTEGER] + +queue + + [VARCHAR(256)] + +queued_by_job_id + + [INTEGER] + +queued_dttm + + [TIMESTAMP] + +rendered_map_index + + [VARCHAR(250)] + +run_id + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +task_display_name + + [VARCHAR(2000)] + +task_id + + [VARCHAR(250)] + NOT NULL + +trigger_id + + [INTEGER] + +trigger_timeout + + [TIMESTAMP] + +try_number + + [INTEGER] + NOT NULL + +unixname + + [VARCHAR(1000)] + +updated_at + + [TIMESTAMP] task_instance--task_instance_history - -0..N -1 + +0..N +1 task_instance--task_instance_history - -0..N -1 + +0..N +1 task_instance--task_instance_history - -0..N -1 + +0..N +1 task_instance--task_instance_history - -0..N -1 + +0..N +1 - + backfill - -backfill - -id - - [INTEGER] - NOT NULL - -completed_at - - [TIMESTAMP] - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_run_conf - - [JSON] - -from_date - - [TIMESTAMP] - NOT NULL - -is_paused - - [BOOLEAN] - -max_active_runs - - [INTEGER] - NOT NULL - -to_date - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + +backfill + +id + + [INTEGER] + NOT NULL + +completed_at + + [TIMESTAMP] + +created_at + + [TIMESTAMP] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_run_conf + + [JSON] + +from_date + + [TIMESTAMP] + NOT NULL + +is_paused + + [BOOLEAN] + +max_active_runs + + [INTEGER] + NOT NULL + +to_date + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL backfill--dag_run - -0..N -{0,1} + +0..N +{0,1} backfill--backfill_dag_run - -0..N -1 + +0..N +1 - + trigger - -trigger - -id - - [INTEGER] - NOT NULL - -classpath - - [VARCHAR(1000)] - NOT NULL - -created_date - - [TIMESTAMP] - NOT NULL - -kwargs - - [TEXT] - NOT NULL - -triggerer_id - - [INTEGER] + +trigger + +id + + [INTEGER] + NOT NULL + +classpath + + [VARCHAR(1000)] + NOT NULL + +created_date + + [TIMESTAMP] + NOT NULL + +kwargs + + [TEXT] + NOT NULL + +triggerer_id + + [INTEGER] trigger--task_instance - -0..N -{0,1} + +0..N +{0,1} - + session - -session - -id - - [INTEGER] - NOT NULL - -data - - [BYTEA] - -expiry - - [TIMESTAMP] - -session_id - - [VARCHAR(255)] + +session + +id + + [INTEGER] + NOT NULL + +data + + [BYTEA] + +expiry + + [TIMESTAMP] + +session_id + + [VARCHAR(255)] - + alembic_version - -alembic_version - -version_num - - [VARCHAR(32)] - NOT NULL + +alembic_version + +version_num + + [VARCHAR(32)] + NOT NULL - + ab_user - -ab_user - -id - - [INTEGER] - NOT NULL - -active - - [BOOLEAN] - -changed_by_fk - - [INTEGER] - -changed_on - - [TIMESTAMP] - -created_by_fk - - [INTEGER] - -created_on - - [TIMESTAMP] - -email - - [VARCHAR(512)] - NOT NULL - -fail_login_count - - [INTEGER] - -first_name - - [VARCHAR(256)] - NOT NULL - -last_login - - [TIMESTAMP] - -last_name - - [VARCHAR(256)] - NOT NULL - -login_count - - [INTEGER] - -password - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL + +ab_user + +id + + [INTEGER] + NOT NULL + +active + + [BOOLEAN] + +changed_by_fk + + [INTEGER] + +changed_on + + [TIMESTAMP] + +created_by_fk + + [INTEGER] + +created_on + + [TIMESTAMP] + +email + + [VARCHAR(512)] + NOT NULL + +fail_login_count + + [INTEGER] + +first_name + + [VARCHAR(256)] + NOT NULL + +last_login + + [TIMESTAMP] + +last_name + + [VARCHAR(256)] + NOT NULL + +login_count + + [INTEGER] + +password + + [VARCHAR(256)] + +username + + [VARCHAR(512)] + NOT NULL ab_user--ab_user - -0..N -{0,1} + +0..N +{0,1} ab_user--ab_user - -0..N -{0,1} + +0..N +{0,1} - + ab_user_role - -ab_user_role - -id - - [INTEGER] - NOT NULL - -role_id - - [INTEGER] - -user_id - - [INTEGER] + +ab_user_role + +id + + [INTEGER] + NOT NULL + +role_id + + [INTEGER] + +user_id + + [INTEGER] ab_user--ab_user_role - -0..N -{0,1} + +0..N +{0,1} - + ab_register_user - -ab_register_user - -id - - [INTEGER] - NOT NULL - -email - - [VARCHAR(512)] - NOT NULL - -first_name - - [VARCHAR(256)] - NOT NULL - -last_name - - [VARCHAR(256)] - NOT NULL - -password - - [VARCHAR(256)] - -registration_date - - [TIMESTAMP] - -registration_hash - - [VARCHAR(256)] - -username - - [VARCHAR(512)] - NOT NULL + +ab_register_user + +id + + [INTEGER] + NOT NULL + +email + + [VARCHAR(512)] + NOT NULL + +first_name + + [VARCHAR(256)] + NOT NULL + +last_name + + [VARCHAR(256)] + NOT NULL + +password + + [VARCHAR(256)] + +registration_date + + [TIMESTAMP] + +registration_hash + + [VARCHAR(256)] + +username + + [VARCHAR(512)] + NOT NULL - + ab_permission - -ab_permission - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL + +ab_permission + +id + + [INTEGER] + NOT NULL + +name + + [VARCHAR(100)] + NOT NULL - + ab_permission_view - -ab_permission_view - -id - - [INTEGER] - NOT NULL - -permission_id - - [INTEGER] - -view_menu_id - - [INTEGER] + +ab_permission_view + +id + + [INTEGER] + NOT NULL + +permission_id + + [INTEGER] + +view_menu_id + + [INTEGER] ab_permission--ab_permission_view - -0..N -{0,1} + +0..N +{0,1} - + ab_permission_view_role - -ab_permission_view_role - -id - - [INTEGER] - NOT NULL - -permission_view_id - - [INTEGER] - -role_id - - [INTEGER] + +ab_permission_view_role + +id + + [INTEGER] + NOT NULL + +permission_view_id + + [INTEGER] + +role_id + + [INTEGER] ab_permission_view--ab_permission_view_role - -0..N -{0,1} + +0..N +{0,1} - + ab_view_menu - -ab_view_menu - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(250)] - NOT NULL + +ab_view_menu + +id + + [INTEGER] + NOT NULL + +name + + [VARCHAR(250)] + NOT NULL ab_view_menu--ab_permission_view - -0..N -{0,1} + +0..N +{0,1} - + ab_role - -ab_role - -id - - [INTEGER] - NOT NULL - -name - - [VARCHAR(64)] - NOT NULL + +ab_role + +id + + [INTEGER] + NOT NULL + +name + + [VARCHAR(64)] + NOT NULL ab_role--ab_user_role - -0..N -{0,1} + +0..N +{0,1} ab_role--ab_permission_view_role - -0..N -{0,1} + +0..N +{0,1} - + alembic_version_fab - -alembic_version_fab - -version_num - - [VARCHAR(32)] - NOT NULL + +alembic_version_fab + +version_num + + [VARCHAR(32)] + NOT NULL diff --git a/docs/apache-airflow/migrations-ref.rst b/docs/apache-airflow/migrations-ref.rst index c4db207712e12..dc20d341729d5 100644 --- a/docs/apache-airflow/migrations-ref.rst +++ b/docs/apache-airflow/migrations-ref.rst @@ -39,7 +39,9 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | Revision ID | Revises ID | Airflow Version | Description | +=========================+==================+===================+==============================================================+ -| ``fb2d4922cd79`` (head) | ``5a5d66100783`` | ``3.0.0`` | Tweak AssetAliasModel to match AssetModel after AIP-76. | +| ``3a8972ecb8f9`` (head) | ``fb2d4922cd79`` | ``3.0.0`` | Add exception_reason and logical_date to BackfillDagRun. | ++-------------------------+------------------+-------------------+--------------------------------------------------------------+ +| ``fb2d4922cd79`` | ``5a5d66100783`` | ``3.0.0`` | Tweak AssetAliasModel to match AssetModel after AIP-76. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | ``5a5d66100783`` | ``c3389cd7793f`` | ``3.0.0`` | Add AssetActive to track orphaning instead of a flag. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ diff --git a/tests/models/test_backfill.py b/tests/models/test_backfill.py index 3940ac3970c1d..d22ad54603293 100644 --- a/tests/models/test_backfill.py +++ b/tests/models/test_backfill.py @@ -30,6 +30,7 @@ AlreadyRunningBackfill, Backfill, BackfillDagRun, + BackfillDagRunExceptionReason, _cancel_backfill, _create_backfill, ) @@ -91,7 +92,8 @@ def test_reverse_and_depends_on_past_fails(dep_on_past, dag_maker, session): @pytest.mark.parametrize("reverse", [True, False]) -def test_create_backfill_simple(reverse, dag_maker, session): +@pytest.mark.parametrize("existing", [["2021-01-02", "2021-01-03"], []]) +def test_create_backfill_simple(reverse, existing, dag_maker, session): """ Verify simple case behavior. @@ -101,6 +103,15 @@ def test_create_backfill_simple(reverse, dag_maker, session): """ with dag_maker(schedule="@daily") as dag: PythonOperator(task_id="hi", python_callable=print) + + for date in existing: + dag_maker.create_dagrun( + run_id=f"scheduled_{date}", + execution_date=timezone.parse(date), + session=session, + ) + session.commit() + expected_run_conf = {"param1": "valABC"} b = _create_backfill( dag_id=dag.dag_id, @@ -117,11 +128,21 @@ def test_create_backfill_simple(reverse, dag_maker, session): .order_by(BackfillDagRun.sort_ordinal) ) dag_runs = session.scalars(query).all() - dates = [str(x.logical_date.date()) for x in dag_runs] - expected_dates = ["2021-01-01", "2021-01-02", "2021-01-03", "2021-01-04", "2021-01-05"] + total_dates = ["2021-01-01", "2021-01-02", "2021-01-03", "2021-01-04", "2021-01-05"] + backfill_dates = [str(x.logical_date.date()) for x in dag_runs] + expected_dates = [x for x in total_dates if x not in existing] if reverse: expected_dates = list(reversed(expected_dates)) - assert dates == expected_dates + + for date in existing: + bdr = session.scalar( + select(BackfillDagRun).where( + BackfillDagRun.backfill_id == b.id, + BackfillDagRun.logical_date == timezone.parse(date), + ) + ) + assert bdr.exception_reason == BackfillDagRunExceptionReason.ALREADY_EXISTS + assert backfill_dates == expected_dates assert all(x.state == DagRunState.QUEUED for x in dag_runs) assert all(x.conf == expected_run_conf for x in dag_runs) From 72f2b2e951a3421e838ae715f954a1520d494464 Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:50:30 -0700 Subject: [PATCH 014/258] Remove TaskContextLogger (#43183) It was a fine idea, but we ultimately realized that it's better to just add records to the Log table. These are now easy to find through the UI (see event logs). --- airflow/config_templates/config.yml | 12 -- airflow/utils/log/file_task_handler.py | 5 - airflow/utils/log/task_context_logger.py | 188 ------------------ newsfragments/43183.significant.rst | 5 + .../airflow/providers/amazon/CHANGELOG.rst | 2 +- tests/utils/log/test_task_context_logger.py | 139 ------------- tests_common/test_utils/mock_executor.py | 4 +- 7 files changed, 7 insertions(+), 348 deletions(-) delete mode 100644 airflow/utils/log/task_context_logger.py create mode 100644 newsfragments/43183.significant.rst delete mode 100644 tests/utils/log/test_task_context_logger.py diff --git a/airflow/config_templates/config.yml b/airflow/config_templates/config.yml index b7c810cfe5164..8af963b98c232 100644 --- a/airflow/config_templates/config.yml +++ b/airflow/config_templates/config.yml @@ -1032,18 +1032,6 @@ logging: type: boolean example: ~ default: "False" - enable_task_context_logger: - description: | - If enabled, Airflow may ship messages to task logs from outside the task run context, e.g. from - the scheduler, executor, or callback execution context. This can help in circumstances such as - when there's something blocking the execution of the task and ordinarily there may be no task - logs at all. - This is set to ``True`` by default. If you encounter issues with this feature - (e.g. scheduler performance issues) it can be disabled. - version_added: 2.8.0 - type: boolean - example: ~ - default: "True" color_log_error_keywords: description: | A comma separated list of keywords related to errors whose presence should display the line in red diff --git a/airflow/utils/log/file_task_handler.py b/airflow/utils/log/file_task_handler.py index 24379d340a784..0192892603f20 100644 --- a/airflow/utils/log/file_task_handler.py +++ b/airflow/utils/log/file_task_handler.py @@ -19,7 +19,6 @@ from __future__ import annotations -import inspect import logging import os from contextlib import suppress @@ -238,10 +237,6 @@ def set_context(self, ti: TaskInstance, *, identifier: str | None = None) -> Non self.handler.setLevel(self.level) return SetContextPropagate.MAINTAIN_PROPAGATE if self.maintain_propagate else None - @cached_property - def supports_task_context_logging(self) -> bool: - return "identifier" in inspect.signature(self.set_context).parameters - @staticmethod def add_triggerer_suffix(full_path, job_id=None): """ diff --git a/airflow/utils/log/task_context_logger.py b/airflow/utils/log/task_context_logger.py deleted file mode 100644 index 1d2301b65be81..0000000000000 --- a/airflow/utils/log/task_context_logger.py +++ /dev/null @@ -1,188 +0,0 @@ -# -# 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 - -import logging -from contextlib import suppress -from copy import copy -from logging import Logger -from typing import TYPE_CHECKING - -from airflow.configuration import conf -from airflow.models.taskinstancekey import TaskInstanceKey -from airflow.utils.log.file_task_handler import _ensure_ti -from airflow.utils.session import create_session - -if TYPE_CHECKING: - from airflow.models.taskinstance import TaskInstance - from airflow.utils.log.file_task_handler import FileTaskHandler - -logger = logging.getLogger(__name__) - - -class TaskContextLogger: - """ - Class for sending messages to task instance logs from outside task execution context. - - This is intended to be used mainly in exceptional circumstances, to give visibility into - events related to task execution when otherwise there would be none. - - :meta private: - """ - - def __init__(self, component_name: str, call_site_logger: Logger | None = None): - """ - Initialize the task context logger with the component name. - - :param component_name: the name of the component that will be used to identify the log messages - :param call_site_logger: if provided, message will also be emitted through this logger - """ - self.component_name = component_name - self.task_handler = self._get_task_handler() - self.enabled = self._should_enable() - self.call_site_logger = call_site_logger - - def _should_enable(self) -> bool: - if not conf.getboolean("logging", "enable_task_context_logger"): - return False - if not self.task_handler: - logger.warning("Task handler does not support task context logging") - return False - logger.info("Task context logging is enabled") - return True - - @staticmethod - def _get_task_handler() -> FileTaskHandler | None: - """Return the task handler that supports task context logging.""" - handlers = [ - handler - for handler in logging.getLogger("airflow.task").handlers - if getattr(handler, "supports_task_context_logging", False) - ] - if not handlers: - return None - h = handlers[0] - if TYPE_CHECKING: - assert isinstance(h, FileTaskHandler) - return h - - def _log(self, level: int, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message to the task instance logs. - - :param level: the log level - :param msg: the message to relay to task context log - :param ti: the task instance or the task instance key - """ - if self.call_site_logger and self.call_site_logger.isEnabledFor(level=level): - with suppress(Exception): - self.call_site_logger.log(level, msg, *args) - - if not self.enabled: - return - - if not self.task_handler: - return - - task_handler = copy(self.task_handler) - try: - if isinstance(ti, TaskInstanceKey): - with create_session() as session: - ti = _ensure_ti(ti, session) - task_handler.set_context(ti, identifier=self.component_name) - if hasattr(task_handler, "mark_end_on_close"): - task_handler.mark_end_on_close = False - filename, lineno, func, stackinfo = logger.findCaller(stacklevel=3) - record = logging.LogRecord( - self.component_name, level, filename, lineno, msg, args, None, func=func - ) - task_handler.emit(record) - finally: - task_handler.close() - - def critical(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level CRITICAL to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.CRITICAL, msg, *args, ti=ti) - - def fatal(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level FATAL to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.FATAL, msg, *args, ti=ti) - - def error(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level ERROR to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.ERROR, msg, *args, ti=ti) - - def warn(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level WARN to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.WARNING, msg, *args, ti=ti) - - def warning(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level WARNING to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.WARNING, msg, *args, ti=ti) - - def info(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level INFO to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.INFO, msg, *args, ti=ti) - - def debug(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level DEBUG to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.DEBUG, msg, *args, ti=ti) - - def notset(self, msg: str, *args, ti: TaskInstance | TaskInstanceKey): - """ - Emit a log message with level NOTSET to the task instance logs. - - :param msg: the message to relay to task context log - :param ti: the task instance - """ - self._log(logging.NOTSET, msg, *args, ti=ti) diff --git a/newsfragments/43183.significant.rst b/newsfragments/43183.significant.rst new file mode 100644 index 0000000000000..e363824b6db5f --- /dev/null +++ b/newsfragments/43183.significant.rst @@ -0,0 +1,5 @@ +Remove TaskContextLogger + +We introduced this as a way to inject messages into task logs from places +other than the task execution context. We later realized that we were better off +just using the Log table. diff --git a/providers/src/airflow/providers/amazon/CHANGELOG.rst b/providers/src/airflow/providers/amazon/CHANGELOG.rst index 8099f3943aac9..0da5f22339229 100644 --- a/providers/src/airflow/providers/amazon/CHANGELOG.rst +++ b/providers/src/airflow/providers/amazon/CHANGELOG.rst @@ -788,7 +788,7 @@ Misc .. Below changes are excluded from the changelog. Move them to appropriate section above if needed. Do not delete the lines(!): * ``Use reproducible builds for provider packages (#35693)`` - * ``Update http to s3 system test (#35711)`` + * ``Update http to s3 system test (#35711)`` 8.11.0 ...... diff --git a/tests/utils/log/test_task_context_logger.py b/tests/utils/log/test_task_context_logger.py deleted file mode 100644 index 7c73648535a22..0000000000000 --- a/tests/utils/log/test_task_context_logger.py +++ /dev/null @@ -1,139 +0,0 @@ -# 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 - -import logging -from unittest import mock -from unittest.mock import Mock - -import pytest - -from airflow.models.taskinstancekey import TaskInstanceKey -from airflow.utils.log.task_context_logger import TaskContextLogger - -from tests_common.test_utils.compat import AIRFLOW_V_3_0_PLUS -from tests_common.test_utils.config import conf_vars - -if AIRFLOW_V_3_0_PLUS: - from airflow.utils.types import DagRunTriggeredByType - -logger = logging.getLogger(__name__) - -pytestmark = pytest.mark.skip_if_database_isolation_mode - - -@pytest.fixture -def mock_handler(): - logger = logging.getLogger("airflow.task") - old = logger.handlers[:] - h = Mock() - logger.handlers[:] = [h] - yield h - logger.handlers[:] = old - - -@pytest.fixture -def ti(dag_maker): - with dag_maker() as dag: - - @dag.task() - def nothing(): - return None - - nothing() - - triggered_by_kwargs = {"triggered_by": DagRunTriggeredByType.TEST} if AIRFLOW_V_3_0_PLUS else {} - dr = dag.create_dagrun("running", run_id="abc", **triggered_by_kwargs) - ti = dr.get_task_instances()[0] - return ti - - -def test_task_context_logger_enabled_by_default(): - t = TaskContextLogger(component_name="test_component") - assert t.enabled is True - - -@pytest.mark.parametrize("supported", [True, False]) -def test_task_handler_not_supports_task_context_logging(mock_handler, supported): - mock_handler.supports_task_context_logging = supported - t = TaskContextLogger(component_name="test_component") - assert t.enabled is supported - - -@pytest.mark.skip_if_database_isolation_mode -@pytest.mark.db_test -@pytest.mark.parametrize("supported", [True, False]) -def test_task_context_log_with_correct_arguments(ti, mock_handler, supported): - mock_handler.supports_task_context_logging = supported - t = TaskContextLogger(component_name="test_component") - t.info("test message with args %s, %s", "a", "b", ti=ti) - if supported: - mock_handler.set_context.assert_called_once_with(ti, identifier="test_component") - mock_handler.emit.assert_called_once() - else: - mock_handler.set_context.assert_not_called() - mock_handler.emit.assert_not_called() - - -@pytest.mark.skip_if_database_isolation_mode -@pytest.mark.db_test -@mock.patch("airflow.utils.log.task_context_logger._ensure_ti") -@pytest.mark.parametrize("supported", [True, False]) -def test_task_context_log_with_task_instance_key(mock_ensure_ti, ti, mock_handler, supported): - mock_handler.supports_task_context_logging = supported - mock_ensure_ti.return_value = ti - task_instance_key = TaskInstanceKey(ti.dag_id, ti.task_id, ti.run_id, ti.try_number, ti.map_index) - t = TaskContextLogger(component_name="test_component") - t.info("test message with args %s, %s", "a", "b", ti=task_instance_key) - if supported: - mock_handler.set_context.assert_called_once_with(ti, identifier="test_component") - mock_handler.emit.assert_called_once() - else: - mock_handler.set_context.assert_not_called() - mock_handler.emit.assert_not_called() - - -@pytest.mark.skip_if_database_isolation_mode -@pytest.mark.db_test -def test_task_context_log_closes_task_handler(ti, mock_handler): - t = TaskContextLogger("blah") - t.info("test message", ti=ti) - mock_handler.close.assert_called_once() - - -@pytest.mark.skip_if_database_isolation_mode -@pytest.mark.skip_if_database_isolation_mode -@pytest.mark.db_test -def test_task_context_log_also_emits_to_call_site_logger(ti): - logger = logging.getLogger("abc123567") - logger.setLevel(logging.INFO) - logger.log = Mock() - t = TaskContextLogger("blah", call_site_logger=logger) - t.info("test message", ti=ti) - logger.log.assert_called_once_with(logging.INFO, "test message") - - -@pytest.mark.db_test -@pytest.mark.parametrize("val, expected", [("true", True), ("false", False)]) -def test_task_context_logger_config_works(ti, mock_handler, val, expected): - with conf_vars({("logging", "enable_task_context_logger"): val}): - t = TaskContextLogger("abc") - t.info("test message", ti=ti) - if expected: - mock_handler.emit.assert_called() - else: - mock_handler.emit.assert_not_called() diff --git a/tests_common/test_utils/mock_executor.py b/tests_common/test_utils/mock_executor.py index 6d4791e889150..83197298733ce 100644 --- a/tests_common/test_utils/mock_executor.py +++ b/tests_common/test_utils/mock_executor.py @@ -18,7 +18,7 @@ from __future__ import annotations from collections import defaultdict -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock from airflow.executors.base_executor import BaseExecutor from airflow.executors.executor_utils import ExecutorName @@ -52,8 +52,6 @@ def __init__(self, do_update=True, *args, **kwargs): super().__init__(*args, **kwargs) - self.task_context_logger = Mock() - def success(self): return State.SUCCESS From 899dcbfcb2b0ca2a8eb50cfb807bf96c336a3552 Mon Sep 17 00:00:00 2001 From: Kalyan R Date: Sat, 19 Oct 2024 12:57:31 +0530 Subject: [PATCH 015/258] pin min amqp (#43172) --- hatch_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch_build.py b/hatch_build.py index 2476745730a32..960f818d48f18 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -118,7 +118,7 @@ "flask-bcrypt>=0.7.1", ], "rabbitmq": [ - "amqp", + "amqp>=5.2.0", ], "s3fs": [ # This is required for support of S3 file system which uses aiobotocore From f0740b31050e3b0c3a8d916cdfd497dac2fc51bd Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Sun, 20 Oct 2024 00:49:52 +0100 Subject: [PATCH 016/258] Fix Selective checks for Task SDK (#43185) Task SDK tests were running as part of "DB tests" with other Core files & providers, this PR changes it so they run separately. It also adds separate mypy checks for it. This commit fixes several other issues to allow running Task SDK tests separately. --- .github/workflows/ci.yml | 22 +- .github/workflows/static-checks-mypy-docs.yml | 8 +- .github/workflows/task-sdk-tests.yml | 15 +- .pre-commit-config.yaml | 18 ++ Dockerfile.ci | 1 + contributing-docs/08_static_code_checks.rst | 3 + dev/breeze/doc/ci/04_selective_checks.md | 2 +- dev/breeze/doc/images/output_shell.svg | 4 +- dev/breeze/doc/images/output_shell.txt | 2 +- .../doc/images/output_static-checks.svg | 4 +- .../doc/images/output_static-checks.txt | 2 +- .../doc/images/output_testing_db-tests.svg | 6 +- .../doc/images/output_testing_db-tests.txt | 2 +- .../images/output_testing_non-db-tests.svg | 6 +- .../images/output_testing_non-db-tests.txt | 2 +- .../doc/images/output_testing_tests.svg | 8 +- .../doc/images/output_testing_tests.txt | 2 +- .../src/airflow_breeze/global_constants.py | 1 - .../src/airflow_breeze/pre_commit_ids.py | 1 + .../airflow_breeze/utils/selective_checks.py | 36 +-- dev/breeze/tests/test_selective_checks.py | 249 +++++++++++------- pyproject.toml | 1 + scripts/ci/pre_commit/mypy_folder.py | 15 +- task_sdk/README.md | 4 + task_sdk/pyproject.toml | 2 +- 25 files changed, 261 insertions(+), 155 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a9d716cd8421..fdf63640af9a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: ci-image-build: ${{ steps.selective-checks.outputs.ci-image-build }} prod-image-build: ${{ steps.selective-checks.outputs.prod-image-build }} docs-build: ${{ steps.selective-checks.outputs.docs-build }} - mypy-folders: ${{ steps.selective-checks.outputs.mypy-folders }} + mypy-checks: ${{ steps.selective-checks.outputs.mypy-checks }} needs-mypy: ${{ steps.selective-checks.outputs.needs-mypy }} needs-helm-tests: ${{ steps.selective-checks.outputs.needs-helm-tests }} needs-api-tests: ${{ steps.selective-checks.outputs.needs-api-tests }} @@ -298,7 +298,7 @@ jobs: runs-on-as-json-docs-build: ${{ needs.build-info.outputs.runs-on-as-json-docs-build }} image-tag: ${{ needs.build-info.outputs.image-tag }} needs-mypy: ${{ needs.build-info.outputs.needs-mypy }} - mypy-folders: ${{ needs.build-info.outputs.mypy-folders }} + mypy-checks: ${{ needs.build-info.outputs.mypy-checks }} python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} branch: ${{ needs.build-info.outputs.default-branch }} canary-run: ${{ needs.build-info.outputs.canary-run }} @@ -644,6 +644,24 @@ jobs: ( needs.build-info.outputs.run-kubernetes-tests == 'true' || needs.build-info.outputs.needs-helm-tests == 'true') + tests-task-sdk: + name: "Task SDK tests" + uses: ./.github/workflows/task-sdk-tests.yml + needs: [build-info, wait-for-ci-images] + permissions: + contents: read + packages: read + secrets: inherit + with: + runs-on-as-json-default: ${{ needs.build-info.outputs.runs-on-as-json-default }} + image-tag: ${{ needs.build-info.outputs.image-tag }} + default-python-version: ${{ needs.build-info.outputs.default-python-version }} + python-versions: ${{ needs.build-info.outputs.python-versions }} + run-task-sdk-tests: ${{ needs.build-info.outputs.run-task-sdk-tests }} + if: > + ( needs.build-info.outputs.run-task-sdk-tests == 'true' || + needs.build-info.outputs.run-tests == 'true') + finalize-tests: name: Finalize tests permissions: diff --git a/.github/workflows/static-checks-mypy-docs.yml b/.github/workflows/static-checks-mypy-docs.yml index c875c1667cf9e..7286c3cf9bca2 100644 --- a/.github/workflows/static-checks-mypy-docs.yml +++ b/.github/workflows/static-checks-mypy-docs.yml @@ -36,7 +36,7 @@ on: # yamllint disable-line rule:truthy description: "Whether to run mypy checks (true/false)" required: true type: string - mypy-folders: + mypy-checks: description: "List of folders to run mypy checks on" required: false type: string @@ -148,7 +148,7 @@ jobs: strategy: fail-fast: false matrix: - mypy-folder: ${{ fromJSON(inputs.mypy-folders) }} + mypy-check: ${{ fromJSON(inputs.mypy-checks) }} env: PYTHON_MAJOR_MINOR_VERSION: "${{inputs.default-python-version}}" IMAGE_TAG: "${{ inputs.image-tag }}" @@ -166,10 +166,10 @@ jobs: - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}:${{ inputs.image-tag }}" uses: ./.github/actions/prepare_breeze_and_image id: breeze - - name: "MyPy checks for ${{ matrix.mypy-folder }}" + - name: "MyPy checks for ${{ matrix.mypy-check }}" run: | pip install pre-commit - pre-commit run --color always --verbose --hook-stage manual mypy-${{matrix.mypy-folder}} --all-files + pre-commit run --color always --verbose --hook-stage manual ${{matrix.mypy-check}} --all-files env: VERBOSE: "false" COLUMNS: "250" diff --git a/.github/workflows/task-sdk-tests.yml b/.github/workflows/task-sdk-tests.yml index 14fae903837c2..2d55b108fea1f 100644 --- a/.github/workflows/task-sdk-tests.yml +++ b/.github/workflows/task-sdk-tests.yml @@ -28,10 +28,6 @@ on: # yamllint disable-line rule:truthy description: "Tag to set for the image" required: true type: string - canary-run: - description: "Whether this is a canary run" - required: true - type: string default-python-version: description: "Which version of python should be used by default" required: true @@ -40,6 +36,10 @@ on: # yamllint disable-line rule:truthy description: "JSON-formatted array of Python versions to build images from" required: true type: string + run-task-sdk-tests: + description: "Whether to run Task SDK tests or not (true/false)" + required: true + type: string jobs: task-sdk-tests: timeout-minutes: 80 @@ -58,7 +58,6 @@ jobs: PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}" VERBOSE: "true" CLEAN_AIRFLOW_INSTALLATION: "${{ inputs.canary-run }}" - if: inputs.run-task-sdk-tests == 'true' steps: - name: "Cleanup repo" shell: bash @@ -81,11 +80,7 @@ jobs: pipx uninstall twine || true pipx install twine && twine check dist/*.whl - name: > - Run provider unit tests on - Airflow Task SDK:Python ${{ matrix.python-version }} - if: matrix.run-tests == 'true' + Run unit tests for Airflow Task SDK:Python ${{ matrix.python-version }} run: > breeze testing tests --run-in-parallel --parallel-test-types TaskSDK - --use-packages-from-dist - --package-format wheel diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29762c19136c2..c3c35e4de66ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1291,6 +1291,7 @@ repos: ^.*/.*_vendor/ | ^airflow/migrations | ^providers/ | + ^task_sdk/ | ^dev | ^scripts | ^docs | @@ -1343,6 +1344,23 @@ repos: files: ^.*\.py$ require_serial: true additional_dependencies: ['rich>=12.4.4'] + - id: mypy-task-sdk + name: Run mypy for Task SDK + language: python + entry: ./scripts/ci/pre_commit/mypy.py --namespace-packages + files: ^task_sdk/src/airflow/sdk/.*\.py$|^task_sdk/tests//.*\.py$ + exclude: ^.*/.*_vendor/ + require_serial: true + additional_dependencies: ['rich>=12.4.4'] + - id: mypy-task-sdk + stages: ['manual'] + name: Run mypy for Task SDK (manual) + language: python + entry: ./scripts/ci/pre_commit/mypy_folder.py task_sdk/src/airflow/sdk + pass_filenames: false + files: ^.*\.py$ + require_serial: true + additional_dependencies: ['rich>=12.4.4'] - id: check-provider-yaml-valid name: Validate provider.yaml files entry: ./scripts/ci/pre_commit/check_provider_yaml_files.py diff --git a/Dockerfile.ci b/Dockerfile.ci index 826d7109ed551..3ddba289a807f 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1385,6 +1385,7 @@ RUN bash /scripts/docker/install_packaging_tools.sh; \ COPY pyproject.toml ${AIRFLOW_SOURCES}/pyproject.toml COPY providers/pyproject.toml ${AIRFLOW_SOURCES}/providers/pyproject.toml COPY task_sdk/pyproject.toml ${AIRFLOW_SOURCES}/task_sdk/pyproject.toml +COPY task_sdk/README.md ${AIRFLOW_SOURCES}/task_sdk/README.md COPY airflow/__init__.py ${AIRFLOW_SOURCES}/airflow/ COPY tests_common/ ${AIRFLOW_SOURCES}/tests_common/ COPY generated/* ${AIRFLOW_SOURCES}/generated/ diff --git a/contributing-docs/08_static_code_checks.rst b/contributing-docs/08_static_code_checks.rst index f064a13a773b6..aa9955da1afdd 100644 --- a/contributing-docs/08_static_code_checks.rst +++ b/contributing-docs/08_static_code_checks.rst @@ -325,6 +325,9 @@ require Breeze Docker image to be built locally. | mypy-providers | * Run mypy for providers | * | | | * Run mypy for providers (manual) | | +-----------------------------------------------------------+--------------------------------------------------------+---------+ +| mypy-task-sdk | * Run mypy for Task SDK | * | +| | * Run mypy for Task SDK (manual) | | ++-----------------------------------------------------------+--------------------------------------------------------+---------+ | pretty-format-json | Format JSON files | | +-----------------------------------------------------------+--------------------------------------------------------+---------+ | pylint | pylint | | diff --git a/dev/breeze/doc/ci/04_selective_checks.md b/dev/breeze/doc/ci/04_selective_checks.md index e5894b0296875..5434b1aad4f51 100644 --- a/dev/breeze/doc/ci/04_selective_checks.md +++ b/dev/breeze/doc/ci/04_selective_checks.md @@ -203,7 +203,7 @@ Github Actions to pass the list of parameters to a command to execute | kubernetes-combos-list-as-string | All combinations of Python version and Kubernetes version to use for tests as space-separated string | 3.9-v1.25.2 3.9-v1.26.4 | * | | kubernetes-versions | All Kubernetes versions to use for tests as JSON array | ['v1.25.2'] | | | kubernetes-versions-list-as-string | All Kubernetes versions to use for tests as space-separated string | v1.25.2 | * | -| mypy-folders | List of folders to be considered for mypy | [] | | +| mypy-checks | List of folders to be considered for mypy | [] | | | mysql-exclude | Which versions of MySQL to exclude for tests as JSON array | [] | | | mysql-versions | Which versions of MySQL to use for tests as JSON array | ['5.7'] | | | needs-api-codegen | Whether "api-codegen" are needed to run ("true"/"false") | true | | diff --git a/dev/breeze/doc/images/output_shell.svg b/dev/breeze/doc/images/output_shell.svg index b9c7c480fbd6b..b8eb1713c7f6c 100644 --- a/dev/breeze/doc/images/output_shell.svg +++ b/dev/breeze/doc/images/output_shell.svg @@ -658,8 +658,8 @@ (All | Default | API | Always | BranchExternalPython |   BranchPythonVenv | CLI | Core | ExternalPython |         Operators | Other | PlainAsserts | Providers |           -PythonVenv | Serialization | TaskSDK | WWW |             -All-Postgres | All-MySQL | All-Quarantined)              +PythonVenv | Serialization | WWW | All-Postgres |        +All-MySQL | All-Quarantined)                             [default: Default]                                       --use-airflow-versionUse (reinstall at entry) Airflow version from PyPI. It   can also be version (to install from PyPI), `none`,      diff --git a/dev/breeze/doc/images/output_shell.txt b/dev/breeze/doc/images/output_shell.txt index d7a9da7d92579..051dd34cd10f7 100644 --- a/dev/breeze/doc/images/output_shell.txt +++ b/dev/breeze/doc/images/output_shell.txt @@ -1 +1 @@ -7b382a009f0280b761743a0746739b80 +fd70e0f17940f32fbc0579e8f77fc6c4 diff --git a/dev/breeze/doc/images/output_static-checks.svg b/dev/breeze/doc/images/output_static-checks.svg index e3c89b304289b..96a324e22c427 100644 --- a/dev/breeze/doc/images/output_static-checks.svg +++ b/dev/breeze/doc/images/output_static-checks.svg @@ -366,8 +366,8 @@ identity | insert-license | kubeconform | lint-chart-schema | lint-css |          lint-dockerfile | lint-helm-chart | lint-json-schema | lint-markdown |            lint-openapi | mixed-line-ending | mypy-airflow | mypy-dev | mypy-docs |          -mypy-providers | pretty-format-json | pylint | python-no-log-warn |               -replace-bad-characters | rst-backticks | ruff | ruff-format | shellcheck |        +mypy-providers | mypy-task-sdk | pretty-format-json | pylint | python-no-log-warn +| replace-bad-characters | rst-backticks | ruff | ruff-format | shellcheck |      trailing-whitespace | ts-compile-format-lint-ui | ts-compile-format-lint-www |    update-black-version | update-breeze-cmd-output |                                 update-breeze-readme-config-hash | update-build-dependencies |                    diff --git a/dev/breeze/doc/images/output_static-checks.txt b/dev/breeze/doc/images/output_static-checks.txt index e917996931d60..b0c56ad6640b1 100644 --- a/dev/breeze/doc/images/output_static-checks.txt +++ b/dev/breeze/doc/images/output_static-checks.txt @@ -1 +1 @@ -08a7e37cd651e4d1eb702cb347d9b061 +b4becd0ef113ac04210350ea8f9f98b9 diff --git a/dev/breeze/doc/images/output_testing_db-tests.svg b/dev/breeze/doc/images/output_testing_db-tests.svg index d9d92ed40f050..708665af52de3 100644 --- a/dev/breeze/doc/images/output_testing_db-tests.svg +++ b/dev/breeze/doc/images/output_testing_db-tests.svg @@ -410,15 +410,15 @@ --parallel-test-typesSpace separated list of test types used for testing in parallel                    (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               [default: API Always BranchExternalPython BranchPythonVenv CLI Core ExternalPython Operators Other PlainAsserts Providers[-amazon,google] Providers[amazon]           -Providers[google] PythonVenv Serialization TaskSDK WWW]                            +Providers[google] PythonVenv Serialization WWW]                                    --database-isolationRun airflow in database isolation mode. --excluded-parallel-test-typesSpace separated list of test types that will be excluded from parallel tes runs.   (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Test options ───────────────────────────────────────────────────────────────────────────────────────────────────────╮ --test-timeoutTest timeout in seconds. Set the pytest setup, execution and teardown timeouts to this      diff --git a/dev/breeze/doc/images/output_testing_db-tests.txt b/dev/breeze/doc/images/output_testing_db-tests.txt index 6245a387214f2..6027a2d1e666a 100644 --- a/dev/breeze/doc/images/output_testing_db-tests.txt +++ b/dev/breeze/doc/images/output_testing_db-tests.txt @@ -1 +1 @@ -690396dbea7c9b6e018704e1ee7f727d +7b406b63cd4a75aba6ac38b8c0b7431c diff --git a/dev/breeze/doc/images/output_testing_non-db-tests.svg b/dev/breeze/doc/images/output_testing_non-db-tests.svg index daf8061b13e40..da43621dd6931 100644 --- a/dev/breeze/doc/images/output_testing_non-db-tests.svg +++ b/dev/breeze/doc/images/output_testing_non-db-tests.svg @@ -376,14 +376,14 @@ --parallel-test-typesSpace separated list of test types used for testing in parallel                    (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               [default: API Always BranchExternalPython BranchPythonVenv CLI Core ExternalPython Operators Other PlainAsserts Providers[-amazon,google] Providers[amazon]           -Providers[google] PythonVenv Serialization TaskSDK WWW]                            +Providers[google] PythonVenv Serialization WWW]                                    --excluded-parallel-test-typesSpace separated list of test types that will be excluded from parallel tes runs.   (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Test options ───────────────────────────────────────────────────────────────────────────────────────────────────────╮ --test-timeoutTest timeout in seconds. Set the pytest setup, execution and teardown timeouts to this      diff --git a/dev/breeze/doc/images/output_testing_non-db-tests.txt b/dev/breeze/doc/images/output_testing_non-db-tests.txt index 122c3b1acd145..1080b5a377717 100644 --- a/dev/breeze/doc/images/output_testing_non-db-tests.txt +++ b/dev/breeze/doc/images/output_testing_non-db-tests.txt @@ -1 +1 @@ -dbecb30a3e03c7dffd09d46c16687e62 +2cc222da8b9f31b93b527220b76b48a2 diff --git a/dev/breeze/doc/images/output_testing_tests.svg b/dev/breeze/doc/images/output_testing_tests.svg index 8c8c881915a31..c20e2ef16b243 100644 --- a/dev/breeze/doc/images/output_testing_tests.svg +++ b/dev/breeze/doc/images/output_testing_tests.svg @@ -466,19 +466,19 @@ `Providers[-amazon,google]`                                                        (All | Default | API | Always | BranchExternalPython | BranchPythonVenv | CLI |    Core | ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv  -| Serialization | TaskSDK | WWW | All-Postgres | All-MySQL | All-Quarantined)      +| Serialization | WWW | All-Postgres | All-MySQL | All-Quarantined)                [default: Default]                                                                 --parallel-test-typesSpace separated list of test types used for testing in parallel                    (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               [default: API Always BranchExternalPython BranchPythonVenv CLI Core ExternalPython Operators Other PlainAsserts Providers[-amazon,google] Providers[amazon]           -Providers[google] PythonVenv Serialization TaskSDK WWW]                            +Providers[google] PythonVenv Serialization WWW]                                    --excluded-parallel-test-typesSpace separated list of test types that will be excluded from parallel tes runs.   (API | Always | BranchExternalPython | BranchPythonVenv | CLI | Core |             ExternalPython | Operators | Other | PlainAsserts | Providers | PythonVenv |       -Serialization | TaskSDK | WWW)                                                     +Serialization | WWW)                                                               ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Test options ───────────────────────────────────────────────────────────────────────────────────────────────────────╮ --test-timeoutTest timeout in seconds. Set the pytest setup, execution and teardown timeouts to this    diff --git a/dev/breeze/doc/images/output_testing_tests.txt b/dev/breeze/doc/images/output_testing_tests.txt index ce894444f7f3b..d19f6cf5abdae 100644 --- a/dev/breeze/doc/images/output_testing_tests.txt +++ b/dev/breeze/doc/images/output_testing_tests.txt @@ -1 +1 @@ -c131a2a8ab980041a1a0f5e91fe58ea8 +15002aa129ce25039921f800fb1cf744 diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 00cb24f61051e..35db4d4e530ab 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -194,7 +194,6 @@ class SelectiveUnitTestTypes(Enum): PLAIN_ASSERTS = "PlainAsserts" PROVIDERS = "Providers" PYTHON_VENV = "PythonVenv" - TASK_SDK = "TaskSDK" WWW = "WWW" diff --git a/dev/breeze/src/airflow_breeze/pre_commit_ids.py b/dev/breeze/src/airflow_breeze/pre_commit_ids.py index e684acd24ad2d..91b3ad06330ac 100644 --- a/dev/breeze/src/airflow_breeze/pre_commit_ids.py +++ b/dev/breeze/src/airflow_breeze/pre_commit_ids.py @@ -117,6 +117,7 @@ "mypy-dev", "mypy-docs", "mypy-providers", + "mypy-task-sdk", "pretty-format-json", "pylint", "python-no-log-warn", diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py b/dev/breeze/src/airflow_breeze/utils/selective_checks.py index 3b16b51d59585..90baa5a7c78b2 100644 --- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py +++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py @@ -88,12 +88,12 @@ "API Always BranchExternalPython BranchPythonVenv " "CLI Core ExternalPython Operators Other PlainAsserts " "Providers[-amazon,google] Providers[amazon] Providers[google] " - "PythonVenv Serialization TaskSDK WWW" + "PythonVenv Serialization WWW" ) ALL_CI_SELECTIVE_TEST_TYPES_WITHOUT_PROVIDERS = ( "API Always BranchExternalPython BranchPythonVenv CLI Core " - "ExternalPython Operators Other PlainAsserts PythonVenv Serialization TaskSDK WWW" + "ExternalPython Operators Other PlainAsserts PythonVenv Serialization WWW" ) ALL_PROVIDERS_SELECTIVE_TEST_TYPES = "Providers[-amazon,google] Providers[amazon] Providers[google]" @@ -180,6 +180,7 @@ def __hash__(self): r"^airflow/.*\.py$", r"^chart", r"^providers/src/", + r"^task_sdk/src/", r"^tests/system", r"^CHANGELOG\.txt", r"^airflow/config_templates/config\.yml", @@ -307,10 +308,6 @@ def __hash__(self): r"^airflow/serialization/", r"^tests/serialization/", ], - SelectiveUnitTestTypes.TASK_SDK: [ - r"^task_sdk/src/airflow/sdk/", - r"^task_sdk/tests/", - ], SelectiveUnitTestTypes.PYTHON_VENV: PYTHON_OPERATOR_FILES, SelectiveUnitTestTypes.BRANCH_PYTHON_VENV: PYTHON_OPERATOR_FILES, SelectiveUnitTestTypes.EXTERNAL_PYTHON: PYTHON_OPERATOR_FILES, @@ -646,41 +643,46 @@ def _should_be_run(self, source_area: FileGroupForCi) -> bool: return False @cached_property - def mypy_folders(self) -> list[str]: - folders_to_check: list[str] = [] + def mypy_checks(self) -> list[str]: + checks_to_run: list[str] = [] if ( self._matching_files( FileGroupForCi.ALL_AIRFLOW_PYTHON_FILES, CI_FILE_GROUP_MATCHES, CI_FILE_GROUP_EXCLUDES ) or self.full_tests_needed ): - folders_to_check.append("airflow") + checks_to_run.append("mypy-airflow") if ( self._matching_files( FileGroupForCi.ALL_PROVIDERS_PYTHON_FILES, CI_FILE_GROUP_MATCHES, CI_FILE_GROUP_EXCLUDES ) or self._are_all_providers_affected() ) and self._default_branch == "main": - folders_to_check.append("providers") + checks_to_run.append("mypy-providers") if ( self._matching_files( FileGroupForCi.ALL_DOCS_PYTHON_FILES, CI_FILE_GROUP_MATCHES, CI_FILE_GROUP_EXCLUDES ) or self.full_tests_needed ): - folders_to_check.append("docs") + checks_to_run.append("mypy-docs") if ( self._matching_files( FileGroupForCi.ALL_DEV_PYTHON_FILES, CI_FILE_GROUP_MATCHES, CI_FILE_GROUP_EXCLUDES ) or self.full_tests_needed ): - folders_to_check.append("dev") - return folders_to_check + checks_to_run.append("mypy-dev") + if ( + self._matching_files(FileGroupForCi.TASK_SDK_FILES, CI_FILE_GROUP_MATCHES, CI_FILE_GROUP_EXCLUDES) + or self.full_tests_needed + ): + checks_to_run.append("mypy-task-sdk") + return checks_to_run @cached_property def needs_mypy(self) -> bool: - return self.mypy_folders != [] + return self.mypy_checks != [] @cached_property def needs_python_scans(self) -> bool: @@ -820,6 +822,8 @@ def _get_test_types_to_run(self, split_to_individual_providers: bool = False) -> f"into Core/Other category[/]" ) get_console().print(remaining_files) + if self.run_task_sdk_tests: + candidate_test_types.add("PythonVenv") candidate_test_types.update(all_selective_test_types_except_providers()) else: if "Providers" in candidate_test_types or "API" in candidate_test_types: @@ -1078,7 +1082,9 @@ def skip_pre_commits(self) -> str: # whole package rather than for individual files. That's why we skip those checks in CI # and run them via `mypy-all` command instead and dedicated CI job in matrix # This will also speed up static-checks job usually as the jobs will be running in parallel - pre_commits_to_skip.update({"mypy-providers", "mypy-airflow", "mypy-docs", "mypy-dev"}) + pre_commits_to_skip.update( + {"mypy-providers", "mypy-airflow", "mypy-docs", "mypy-dev", "mypy-task-sdk"} + ) if self._default_branch != "main": # Skip those tests on all "release" branches pre_commits_to_skip.update( diff --git a/dev/breeze/tests/test_selective_checks.py b/dev/breeze/tests/test_selective_checks.py index ec7f924d2d61f..627779ad0ec82 100644 --- a/dev/breeze/tests/test_selective_checks.py +++ b/dev/breeze/tests/test_selective_checks.py @@ -120,13 +120,13 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "false", "skip-pre-commits": "check-provider-yaml-valid,flynt,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": None, "providers-test-types-list-as-string": None, "separate-test-types-list-as-string": None, "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, id="No tests on simple change", ) @@ -156,13 +156,13 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always Providers[common.compat,fab]", "providers-test-types-list-as-string": "Providers[common.compat,fab]", "separate-test-types-list-as-string": "API Always Providers[common.compat] Providers[fab]", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only API tests and DOCS and common.compat, FAB providers should run", ) @@ -182,12 +182,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always", "separate-test-types-list-as-string": "API Always", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only API tests and DOCS should run (no provider tests) when only internal api changed", ) @@ -207,12 +207,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always", "separate-test-types-list-as-string": "API Always", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only API tests and DOCS should run (no provider tests) when only ui api changed", ) @@ -232,12 +232,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "false", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always", "separate-test-types-list-as-string": "API Always", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only API tests should run (no provider tests) and no DOCs build when only test API files changed", ) @@ -258,13 +258,13 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Operators", "providers-test-types-list-as-string": "", "separate-test-types-list-as-string": "Always Operators", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only Operator tests and DOCS should run", ) @@ -285,7 +285,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always BranchExternalPython BranchPythonVenv " "ExternalPython Operators PythonVenv", @@ -293,7 +293,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "separate-test-types-list-as-string": "Always BranchExternalPython BranchPythonVenv " "ExternalPython Operators PythonVenv", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only Python tests", ) @@ -314,13 +314,13 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Serialization", "providers-test-types-list-as-string": "", "separate-test-types-list-as-string": "Always Serialization", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only Serialization tests", ) @@ -344,7 +344,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always Providers[amazon] " @@ -355,7 +355,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "Providers[fab] Providers[google] Providers[openlineage] Providers[pgvector] " "Providers[postgres]", "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers']", + "mypy-checks": "['mypy-airflow', 'mypy-providers']", }, id="API and providers tests and docs should run", ) @@ -375,15 +375,17 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "false", "docs-build": "false", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," - "ts-compile-format-lint-ui,ts-compile-format-lint-www", + "skip-pre-commits": ( + "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs" + ",mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www" + ), "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Providers[apache.beam] Providers[google]", "providers-test-types-list-as-string": "Providers[apache.beam] Providers[google]", "separate-test-types-list-as-string": "Always Providers[apache.beam] Providers[google]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Selected Providers and docs should run", ) @@ -404,17 +406,52 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,flynt,identity,lint-helm-chart,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": None, "providers-test-types-list-as-string": None, "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, id="Only docs builds should run - no tests needed", ) ), + ( + pytest.param( + ("task_sdk/src/airflow/sdk/random.py",), + { + "all-python-versions": "['3.9']", + "all-python-versions-list-as-string": "3.9", + "python-versions": "['3.9']", + "python-versions-list-as-string": "3.9", + "ci-image-build": "true", + "prod-image-build": "false", + "needs-api-tests": "false", + "needs-helm-tests": "false", + "run-kubernetes-tests": "false", + "run-tests": "true", + "run-task-sdk-tests": "true", + "docs-build": "true", + "full-tests-needed": "false", + "skip-pre-commits": ( + "check-provider-yaml-valid,identity,lint-helm-chart" + ",mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk" + ",ts-compile-format-lint-ui,ts-compile-format-lint-www" + ), + "skip-provider-tests": "true", + "upgrade-to-newer-dependencies": "false", + "parallel-test-types-list-as-string": ( + "API Always BranchExternalPython BranchPythonVenv CLI Core ExternalPython " + "Operators Other PlainAsserts PythonVenv Serialization WWW" + ), + "providers-test-types-list-as-string": "", + "needs-mypy": "true", + "mypy-checks": "['mypy-task-sdk']", + }, + id="Task SDK source file changed - Task SDK & Core tests should run", + ) + ), ( pytest.param( ( @@ -434,7 +471,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "true", "upgrade-to-newer-dependencies": "false", @@ -443,7 +480,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "providers-test-types-list-as-string": "Providers[amazon] " "Providers[common.sql,openlineage,pgvector,postgres] Providers[google]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Helm tests, providers (both upstream and downstream)," "kubernetes tests and docs should run", @@ -469,7 +506,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "true", "upgrade-to-newer-dependencies": "false", @@ -480,7 +517,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "Providers[apache.livy] Providers[dbt.cloud] " "Providers[dingding] Providers[discord] Providers[http]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Helm tests, http and all relevant providers, kubernetes tests and " "docs should run even if unimportant files were added", @@ -505,14 +542,14 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "false", "docs-build": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "true", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Providers[airbyte]", "providers-test-types-list-as-string": "Providers[airbyte]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Helm tests, airbyte providers, kubernetes tests and " "docs should run even if unimportant files were added", @@ -537,14 +574,14 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "docs-build": "true", "skip-pre-commits": "check-provider-yaml-valid,identity,mypy-airflow,mypy-dev," - "mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-amazon-tests": "false", "run-kubernetes-tests": "true", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always", "providers-test-types-list-as-string": "", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Docs should run even if unimportant files were added and prod image " "should be build for chart changes", @@ -566,12 +603,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "true", "docs-build": "true", "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run - including all providers and upgrading to " "newer requirements as pyproject.toml changed and all Python versions", @@ -593,12 +630,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "true", "docs-build": "true", "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run and upgrading to newer requirements as dependencies change", ) @@ -618,7 +655,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "needs-helm-tests": "false", "run-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", @@ -627,7 +664,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "Providers[apache.hive,cncf.kubernetes,common.compat,common.sql,exasol,ftp,http," "imap,microsoft.azure,mongo,mysql,openlineage,postgres,salesforce,ssh,teradata] Providers[google]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Providers tests run including amazon tests if amazon provider files changed", ), @@ -645,13 +682,13 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "false", "docs-build": "false", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Providers[airbyte]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Providers tests run without amazon tests if no amazon file changed", ), @@ -671,7 +708,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-tests": "true", "run-amazon-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", @@ -679,7 +716,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "Providers[apache.hive,cncf.kubernetes,common.compat,common.sql,exasol,ftp,http," "imap,microsoft.azure,mongo,mysql,openlineage,postgres,salesforce,ssh,teradata] Providers[google]", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Providers tests run including amazon tests if amazon provider files changed", ), @@ -702,12 +739,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "false", "run-kubernetes-tests": "false", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Providers[common.compat,common.io,openlineage]", "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers']", + "mypy-checks": "['mypy-airflow', 'mypy-providers']", }, id="Only Always and common providers tests should run when only common.io and tests/always changed", ), @@ -726,12 +763,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "true", "run-kubernetes-tests": "false", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Core Providers[standard] Serialization", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Providers standard tests and Serialization tests to run when airflow bash.py changed", ), @@ -750,12 +787,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "false", "docs-build": "false", "run-kubernetes-tests": "false", - "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "Always Core Providers[standard] Serialization", "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, id="Force Core and Serialization tests to run when tests bash changed", ), @@ -775,12 +812,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "true", "docs-build": "true", "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="All tests should be run when tests/utils/ change", ) @@ -801,12 +838,12 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str): "run-amazon-tests": "true", "docs-build": "true", "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="All tests should be run when tests_common/ change", ) @@ -972,12 +1009,12 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including all providers when full tests are needed, " "and all versions are required.", @@ -1006,12 +1043,12 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including all providers when full tests are needed " "but with single python and kubernetes if `default versions only` label is set", @@ -1040,12 +1077,12 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including all providers when full tests are needed " "but with single python and kubernetes if no version label is set", @@ -1075,12 +1112,12 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including all providers when full tests are needed " "but with single python and kubernetes if `latest versions only` label is set", @@ -1110,12 +1147,12 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including full providers when full " "tests are needed even with different label set as well", @@ -1142,16 +1179,16 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "full-tests-needed": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "providers-test-types-list-as-string": ALL_PROVIDERS_SELECTIVE_TEST_TYPES, "separate-test-types-list-as-string": "API Always BranchExternalPython BranchPythonVenv " "CLI Core ExternalPython Operators Other PlainAsserts " + LIST_OF_ALL_PROVIDER_TESTS - + " PythonVenv Serialization TaskSDK WWW", + + " PythonVenv Serialization WWW", "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run including full providers when " "full tests are needed even if no files are changed", @@ -1175,17 +1212,17 @@ def test_full_test_needed_when_scripts_changes(files: tuple[str, ...], expected_ "docs-build": "true", "docs-list-as-string": "apache-airflow docker-stack", "full-tests-needed": "true", - "skip-pre-commits": "check-airflow-provider-compatibility,check-extra-packages-references,check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,validate-operators-init", + "skip-pre-commits": "check-airflow-provider-compatibility,check-extra-packages-references,check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk,validate-operators-init", "skip-provider-tests": "true", "upgrade-to-newer-dependencies": "false", "parallel-test-types-list-as-string": "API Always BranchExternalPython " "BranchPythonVenv CLI Core ExternalPython Operators Other PlainAsserts " - "PythonVenv Serialization TaskSDK WWW", + "PythonVenv Serialization WWW", "separate-test-types-list-as-string": "API Always BranchExternalPython " "BranchPythonVenv CLI Core ExternalPython Operators Other PlainAsserts " - "PythonVenv Serialization TaskSDK WWW", + "PythonVenv Serialization WWW", "needs-mypy": "true", - "mypy-folders": "['airflow', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="Everything should run except Providers and lint pre-commit " "when full tests are needed for non-main branch", @@ -1228,7 +1265,7 @@ def test_expected_output_full_tests_needed( "skip-provider-tests": "true", "parallel-test-types-list-as-string": None, "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, id="Nothing should run if only non-important files changed", ), @@ -1255,7 +1292,7 @@ def test_expected_output_full_tests_needed( "skip-provider-tests": "true", "parallel-test-types-list-as-string": "Always", "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, id="No Helm tests, No providers no lint charts, should run if " "only chart/providers changed in non-main but PROD image should be built", @@ -1285,7 +1322,7 @@ def test_expected_output_full_tests_needed( "skip-provider-tests": "true", "parallel-test-types-list-as-string": "Always CLI", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only CLI tests and Kubernetes tests should run if cli/chart files changed in non-main branch", ), @@ -1309,9 +1346,9 @@ def test_expected_output_full_tests_needed( "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "true", "parallel-test-types-list-as-string": "API Always BranchExternalPython BranchPythonVenv " - "CLI Core ExternalPython Operators Other PlainAsserts PythonVenv Serialization TaskSDK WWW", + "CLI Core ExternalPython Operators Other PlainAsserts PythonVenv Serialization WWW", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="All tests except Providers and helm lint pre-commit " "should run if core file changed in non-main branch", @@ -1348,11 +1385,11 @@ def test_expected_output_pull_request_v2_7( "docs-list-as-string": None, "upgrade-to-newer-dependencies": "false", "skip-pre-commits": "check-provider-yaml-valid,flynt,identity,lint-helm-chart," - "mypy-airflow,mypy-dev,mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "skip-provider-tests": "true", "parallel-test-types-list-as-string": None, "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, id="Nothing should run if only non-important files changed", ), @@ -1368,13 +1405,13 @@ def test_expected_output_pull_request_v2_7( "run-tests": "true", "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, - "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "true", "parallel-test-types-list-as-string": "Always", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Only Always and docs build should run if only system tests changed", ), @@ -1400,7 +1437,7 @@ def test_expected_output_pull_request_v2_7( "cncf.kubernetes common.compat common.sql facebook google hashicorp microsoft.azure " "microsoft.mssql mysql openlineage oracle postgres " "presto salesforce samba sftp ssh trino", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,ts-compile-format-lint-ui,ts-compile-format-lint-www", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk,ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "true", "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "false", @@ -1409,7 +1446,7 @@ def test_expected_output_pull_request_v2_7( "hashicorp,microsoft.azure,microsoft.mssql,mysql,openlineage,oracle,postgres,presto," "salesforce,samba,sftp,ssh,trino] Providers[google]", "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers']", + "mypy-checks": "['mypy-airflow', 'mypy-providers']", }, id="CLI tests and Google-related provider tests should run if cli/chart files changed but " "prod image should be build too and k8s tests too", @@ -1431,14 +1468,14 @@ def test_expected_output_pull_request_v2_7( "run-tests": "true", "docs-build": "true", "docs-list-as-string": "apache-airflow common.compat fab", - "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "false", "parallel-test-types-list-as-string": "API Always CLI Operators Providers[common.compat,fab] WWW", "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="No providers tests except common.compat fab should run if only CLI/API/Operators/WWW file changed", ), @@ -1453,14 +1490,14 @@ def test_expected_output_pull_request_v2_7( "run-tests": "true", "docs-build": "true", "docs-list-as-string": "apache-airflow", - "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES_WITHOUT_PROVIDERS, "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Tests for all airflow core types except providers should run if model file changed", ), @@ -1475,14 +1512,14 @@ def test_expected_output_pull_request_v2_7( "run-tests": "true", "docs-build": "true", "docs-list-as-string": "apache-airflow", - "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers," + "skip-pre-commits": "check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk," "ts-compile-format-lint-ui,ts-compile-format-lint-www", "run-kubernetes-tests": "false", "upgrade-to-newer-dependencies": "false", "skip-provider-tests": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES_WITHOUT_PROVIDERS, "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, id="Tests for all airflow core types except providers should run if " "any other than API/WWW/CLI/Operators file changed.", @@ -1520,11 +1557,11 @@ def test_expected_output_pull_request_target( "run-tests": "true", "docs-build": "true", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="All tests run on push even if unimportant file changed", ), @@ -1541,13 +1578,13 @@ def test_expected_output_pull_request_target( "needs-helm-tests": "false", "run-tests": "true", "docs-build": "true", - "skip-pre-commits": "check-airflow-provider-compatibility,check-extra-packages-references,check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,validate-operators-init", + "skip-pre-commits": "check-airflow-provider-compatibility,check-extra-packages-references,check-provider-yaml-valid,identity,lint-helm-chart,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk,validate-operators-init", "docs-list-as-string": "apache-airflow docker-stack", "upgrade-to-newer-dependencies": "true", "parallel-test-types-list-as-string": "API Always BranchExternalPython BranchPythonVenv " - "CLI Core ExternalPython Operators Other PlainAsserts PythonVenv Serialization TaskSDK WWW", + "CLI Core ExternalPython Operators Other PlainAsserts PythonVenv Serialization WWW", "needs-mypy": "true", - "mypy-folders": "['airflow', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="All tests except Providers and Helm run on push" " even if unimportant file changed in non-main branch", @@ -1565,12 +1602,12 @@ def test_expected_output_pull_request_target( "needs-helm-tests": "true", "run-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "docs-list-as-string": ALL_DOCS_SELECTED_FOR_BUILD, "upgrade-to-newer-dependencies": "true", "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, id="All tests run on push if core file changed", ), @@ -1619,13 +1656,13 @@ def test_no_commit_provided_trigger_full_build_for_any_event_type(github_event): "needs-helm-tests": "true", "run-tests": "true", "docs-build": "true", - "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers", + "skip-pre-commits": "identity,mypy-airflow,mypy-dev,mypy-docs,mypy-providers,mypy-task-sdk", "upgrade-to-newer-dependencies": ( "true" if github_event in [GithubEvents.PUSH, GithubEvents.SCHEDULE] else "false" ), "parallel-test-types-list-as-string": ALL_CI_SELECTIVE_TEST_TYPES, "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, str(stderr), ) @@ -2203,7 +2240,7 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("README.md",), { "needs-mypy": "false", - "mypy-folders": "[]", + "mypy-checks": "[]", }, "main", (), @@ -2213,7 +2250,7 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("airflow/cli/file.py",), { "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, "main", (), @@ -2223,7 +2260,7 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("airflow/models/file.py",), { "needs-mypy": "true", - "mypy-folders": "['airflow']", + "mypy-checks": "['mypy-airflow']", }, "main", (), @@ -2233,17 +2270,27 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("providers/src/airflow/providers/a_file.py",), { "needs-mypy": "true", - "mypy-folders": "['providers']", + "mypy-checks": "['mypy-providers']", }, "main", (), id="Airflow mypy checks on provider files", ), + pytest.param( + ("task_sdk/src/airflow/sdk/a_file.py",), + { + "needs-mypy": "true", + "mypy-checks": "['mypy-task-sdk']", + }, + "main", + (), + id="Airflow mypy checks on Task SDK files", + ), pytest.param( ("docs/a_file.py",), { "needs-mypy": "true", - "mypy-folders": "['docs']", + "mypy-checks": "['mypy-docs']", }, "main", (), @@ -2253,7 +2300,7 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("dev/a_package/a_file.py",), { "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, "main", (), @@ -2263,7 +2310,7 @@ def test_provider_compatibility_checks(labels: tuple[str, ...], expected_outputs ("readme.md",), { "needs-mypy": "true", - "mypy-folders": "['airflow', 'providers', 'docs', 'dev']", + "mypy-checks": "['mypy-airflow', 'mypy-providers', 'mypy-docs', 'mypy-dev', 'mypy-task-sdk']", }, "main", ("full tests needed",), diff --git a/pyproject.toml b/pyproject.toml index f4512a382621f..f23c0afb6749c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -592,6 +592,7 @@ explicit_package_bases = true mypy_path = [ "$MYPY_CONFIG_FILE_DIR", "$MYPY_CONFIG_FILE_DIR/providers/src", + "$MYPY_CONFIG_FILE_DIR/task_sdk/src", ] [[tool.mypy.overrides]] diff --git a/scripts/ci/pre_commit/mypy_folder.py b/scripts/ci/pre_commit/mypy_folder.py index b2d7c76580276..36a7840fff9b8 100755 --- a/scripts/ci/pre_commit/mypy_folder.py +++ b/scripts/ci/pre_commit/mypy_folder.py @@ -31,7 +31,13 @@ initialize_breeze_precommit(__name__, __file__) -ALLOWED_FOLDERS = ["airflow", "providers/src/airflow/providers", "dev", "docs"] +ALLOWED_FOLDERS = [ + "airflow", + "providers/src/airflow/providers", + "dev", + "docs", + "task_sdk/src/airflow/sdk", +] if len(sys.argv) < 2: console.print(f"[yellow]You need to specify the folder to test as parameter: {ALLOWED_FOLDERS}\n") @@ -50,6 +56,13 @@ "--namespace-packages", ] ) +if mypy_folder == "task_sdk/src/airflow/sdk": + arguments.extend( + [ + "task_sdk/tests", + "--namespace-packages", + ] + ) if mypy_folder == "airflow": arguments.extend( diff --git a/task_sdk/README.md b/task_sdk/README.md index ef14affc68c62..d74c50d4d740b 100644 --- a/task_sdk/README.md +++ b/task_sdk/README.md @@ -16,3 +16,7 @@ specific language governing permissions and limitations under the License. --> + +# Apache Airflow Task SDK + +The Apache Airflow Task SDK includes interfaces for DAG authors. diff --git a/task_sdk/pyproject.toml b/task_sdk/pyproject.toml index 149a9731ce994..be2be98baf86e 100644 --- a/task_sdk/pyproject.toml +++ b/task_sdk/pyproject.toml @@ -19,7 +19,7 @@ name = "apache-airflow-task-sdk" version = "0.1.0.dev0" description = "Python Task SDK for Apache Airflow DAG Authors" -#readme = "README.md" +readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.9, <3.13" dependencies = [] From 57500b6608fcbd3bfaa1ddbe6364899ac3f8c251 Mon Sep 17 00:00:00 2001 From: Pavan Sharma Date: Sun, 20 Oct 2024 09:51:18 +0530 Subject: [PATCH 017/258] (fix): HybridExecutor tasks of other executor rescheduled in kubernetes executor (#43003) --- .../executors/kubernetes_executor.py | 22 +- .../executors/test_kubernetes_executor.py | 213 ++++++++++++++++++ 2 files changed, 233 insertions(+), 2 deletions(-) diff --git a/providers/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py b/providers/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py index 4301a54f02922..ab18394f4125d 100644 --- a/providers/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +++ b/providers/src/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py @@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, Any, Sequence from kubernetes.dynamic import DynamicClient -from sqlalchemy import select, update +from sqlalchemy import or_, select, update from airflow.cli.cli_config import ( ARG_DAG_ID, @@ -52,6 +52,7 @@ ) from airflow.configuration import conf from airflow.executors.base_executor import BaseExecutor +from airflow.executors.executor_constants import KUBERNETES_EXECUTOR from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import ( ADOPTED, POD_EXECUTOR_DONE_KEY, @@ -229,13 +230,30 @@ def clear_not_launched_queued_tasks(self, session: Session = NEW_SESSION) -> Non assert self.kube_client from airflow.models.taskinstance import TaskInstance + hybrid_executor_enabled = hasattr(TaskInstance, "executor") + default_executor = None + if hybrid_executor_enabled: + from airflow.executors.executor_loader import ExecutorLoader + + default_executor = str(ExecutorLoader.get_default_executor_name()) + with Stats.timer("kubernetes_executor.clear_not_launched_queued_tasks.duration"): self.log.debug("Clearing tasks that have not been launched") query = select(TaskInstance).where( - TaskInstance.state == TaskInstanceState.QUEUED, TaskInstance.queued_by_job_id == self.job_id + TaskInstance.state == TaskInstanceState.QUEUED, + TaskInstance.queued_by_job_id == self.job_id, ) if self.kubernetes_queue: query = query.where(TaskInstance.queue == self.kubernetes_queue) + elif hybrid_executor_enabled and KUBERNETES_EXECUTOR == default_executor: + query = query.where( + or_( + TaskInstance.executor == KUBERNETES_EXECUTOR, + TaskInstance.executor.is_(None), + ), + ) + elif hybrid_executor_enabled: + query = query.where(TaskInstance.executor == KUBERNETES_EXECUTOR) queued_tis: list[TaskInstance] = session.scalars(query).all() self.log.info("Found %s queued task instances", len(queued_tis)) diff --git a/providers/tests/cncf/kubernetes/executors/test_kubernetes_executor.py b/providers/tests/cncf/kubernetes/executors/test_kubernetes_executor.py index 9d425edcb9d06..13ca0ed828c65 100644 --- a/providers/tests/cncf/kubernetes/executors/test_kubernetes_executor.py +++ b/providers/tests/cncf/kubernetes/executors/test_kubernetes_executor.py @@ -29,6 +29,12 @@ from urllib3 import HTTPResponse from airflow.exceptions import AirflowException, AirflowProviderDeprecationWarning +from airflow.executors.executor_constants import ( + CELERY_EXECUTOR, + CELERY_KUBERNETES_EXECUTOR, + KUBERNETES_EXECUTOR, +) +from airflow.models.taskinstance import TaskInstance from airflow.models.taskinstancekey import TaskInstanceKey from airflow.operators.empty import EmptyOperator from airflow.providers.cncf.kubernetes import pod_generator @@ -1277,6 +1283,7 @@ def test_kube_config_get_namespace_list( @pytest.mark.db_test @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) def test_clear_not_launched_queued_tasks_not_launched( self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session ): @@ -1287,6 +1294,13 @@ def test_clear_not_launched_queued_tasks_not_launched( mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource mock_kube_dynamic_client.return_value.get.return_value.items = [] + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) dag_run = dag_maker.create_dagrun() @@ -1320,6 +1334,7 @@ def test_clear_not_launched_queued_tasks_not_launched( ], ) @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) def test_clear_not_launched_queued_tasks_launched( self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session, task_queue, kubernetes_queue ): @@ -1350,6 +1365,13 @@ def test_clear_not_launched_queued_tasks_launched( ] ) + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) dag_run = dag_maker.create_dagrun() @@ -1376,6 +1398,7 @@ def test_clear_not_launched_queued_tasks_launched( @pytest.mark.db_test @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) def test_clear_not_launched_queued_tasks_mapped_task(self, mock_kube_dynamic_client, dag_maker, session): """One mapped task has a launched pod - other does not.""" @@ -1410,6 +1433,13 @@ def get(*args, **kwargs): mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource mock_kube_dynamic_client.return_value.get.side_effect = get + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) with dag_maker(dag_id="test_clear"): op = BashOperator.partial(task_id="bash").expand(bash_command=["echo 0", "echo 1"]) @@ -1443,6 +1473,7 @@ def get(*args, **kwargs): ) @pytest.mark.db_test + @conf_vars({("core", "executor"): CELERY_KUBERNETES_EXECUTOR}) def test_clear_not_launched_queued_tasks_not_launched_other_queue( self, dag_maker, create_dummy_dag, session ): @@ -1450,6 +1481,13 @@ def test_clear_not_launched_queued_tasks_not_launched_other_queue( mock_kube_client = mock.MagicMock() mock_kube_client.list_namespaced_pod.return_value = k8s.V1PodList(items=[]) + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) dag_run = dag_maker.create_dagrun() @@ -1470,7 +1508,175 @@ def test_clear_not_launched_queued_tasks_not_launched_other_queue( assert mock_kube_client.list_namespaced_pod.call_count == 0 @pytest.mark.db_test + @pytest.mark.skipif( + not hasattr(TaskInstance, "executor"), reason="Hybrid executor added in later version" + ) + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) + def test_clear_not_launched_queued_tasks_not_launched_other_executor( + self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session + ): + """Queued TI has no pod, but it is not queued for the k8s executor""" + mock_kube_client = mock.MagicMock() + mock_kube_dynamic_client.return_value = mock.MagicMock() + mock_pod_resource = mock.MagicMock() + mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource + mock_kube_dynamic_client.return_value.get.return_value.items = [] + + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) + create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) + dag_run = dag_maker.create_dagrun() + + ti = dag_run.task_instances[0] + ti.state = State.QUEUED + ti.queued_by_job_id = 1 + ti.executor = "CeleryExecutor" + session.flush() + + executor = self.kubernetes_executor + executor.job_id = 1 + + executor.kube_client = mock_kube_client + executor.clear_not_launched_queued_tasks(session=session) + + ti.refresh_from_db() + assert ti.executor == "CeleryExecutor" + assert ti.state == State.QUEUED + assert mock_kube_client.list_namespaced_pod.call_count == 0 + + @pytest.mark.db_test + @pytest.mark.skipif( + not hasattr(TaskInstance, "executor"), reason="Hybrid executor added in later version" + ) @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): CELERY_EXECUTOR}) + def test_clear_not_launched_queued_tasks_not_launched_other_default_executor( + self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session + ): + """Queued TI has no pod, but it is not queued for the k8s executor""" + mock_kube_client = mock.MagicMock() + mock_kube_dynamic_client.return_value = mock.MagicMock() + mock_pod_resource = mock.MagicMock() + mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource + mock_kube_dynamic_client.return_value.get.return_value.items = [] + + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) + create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) + dag_run = dag_maker.create_dagrun() + + ti = dag_run.task_instances[0] + ti.state = State.QUEUED + ti.queued_by_job_id = 1 + session.flush() + + executor = self.kubernetes_executor + executor.job_id = 1 + + executor.kube_client = mock_kube_client + executor.clear_not_launched_queued_tasks(session=session) + + ti.refresh_from_db() + assert ti.state == State.QUEUED + assert mock_kube_client.list_namespaced_pod.call_count == 0 + + @pytest.mark.db_test + @pytest.mark.skipif( + not hasattr(TaskInstance, "executor"), reason="Hybrid executor added in later version" + ) + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) + def test_clear_not_launched_queued_tasks_launched_none_executor( + self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session + ): + """Queued TI has no pod, but it is not queued for the k8s executor""" + mock_kube_client = mock.MagicMock() + mock_kube_dynamic_client.return_value = mock.MagicMock() + mock_pod_resource = mock.MagicMock() + mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource + mock_kube_dynamic_client.return_value.get.return_value.items = [] + + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) + create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) + dag_run = dag_maker.create_dagrun() + + ti = dag_run.task_instances[0] + ti.state = State.QUEUED + ti.queued_by_job_id = 1 + session.flush() + + executor = self.kubernetes_executor + executor.job_id = 1 + + executor.kube_client = mock_kube_client + executor.clear_not_launched_queued_tasks(session=session) + + ti.refresh_from_db() + assert ti.state == State.SCHEDULED + assert mock_kube_dynamic_client.return_value.get.call_count == 1 + + @pytest.mark.db_test + @pytest.mark.skipif( + not hasattr(TaskInstance, "executor"), reason="Hybrid executor added in later version" + ) + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) + def test_clear_not_launched_queued_tasks_launched_kubernetes_executor( + self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session + ): + """Queued TI has no pod, but it is not queued for the k8s executor""" + mock_kube_client = mock.MagicMock() + mock_kube_dynamic_client.return_value = mock.MagicMock() + mock_pod_resource = mock.MagicMock() + mock_kube_dynamic_client.return_value.resources.get.return_value = mock_pod_resource + mock_kube_dynamic_client.return_value.get.return_value.items = [] + + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) + create_dummy_dag(dag_id="test_clear", task_id="task1", with_dagrun_type=None) + dag_run = dag_maker.create_dagrun() + + ti = dag_run.task_instances[0] + ti.state = State.QUEUED + ti.queued_by_job_id = 1 + ti.executor = KUBERNETES_EXECUTOR + session.flush() + + executor = self.kubernetes_executor + executor.job_id = 1 + + executor.kube_client = mock_kube_client + executor.clear_not_launched_queued_tasks(session=session) + + ti.refresh_from_db() + assert ti.state == State.SCHEDULED + assert mock_kube_dynamic_client.return_value.get.call_count == 1 + + @pytest.mark.db_test + @mock.patch("airflow.providers.cncf.kubernetes.executors.kubernetes_executor.DynamicClient") + @conf_vars({("core", "executor"): KUBERNETES_EXECUTOR}) def test_clear_not_launched_queued_tasks_clear_only_by_job_id( self, mock_kube_dynamic_client, dag_maker, create_dummy_dag, session ): @@ -1479,6 +1685,13 @@ def test_clear_not_launched_queued_tasks_clear_only_by_job_id( mock_kube_dynamic_client.return_value = mock.MagicMock() mock_kube_dynamic_client.return_value.get.return_value = k8s.V1PodList(items=[]) + # This is hack to use overridden conf vars as it seems executors loaded before conf override. + if hasattr(TaskInstance, "executor"): + import importlib + + from airflow.executors import executor_loader + + importlib.reload(executor_loader) create_dummy_dag(dag_id="test_clear_0", task_id="task0", with_dagrun_type=None) dag_run = dag_maker.create_dagrun() From 0da9235ccb336dbdef3e9252f08692dec782881e Mon Sep 17 00:00:00 2001 From: Indrale Dnyaneshwar <118615488+Dnyanu76@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:20:58 +0530 Subject: [PATCH 018/258] Docs: Typo Fix (#43199) Corrected "customising" to "customizing" in [README.md]. This pull request addresses a minor typo found in repository. The typo has been corrected to improve clarity and maintain the quality of the documentation. This change is purely cosmetic and does not affect functionality. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0419ae0456070..01408d1dbb7ab 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Those are - in the order of most common ways people install Airflow: - [PyPI releases](https://pypi.org/project/apache-airflow/) to install Airflow using standard `pip` tool - [Docker Images](https://hub.docker.com/r/apache/airflow) to install airflow via `docker` tool, use them in Kubernetes, Helm Charts, `docker-compose`, `docker swarm`, etc. You can - read more about using, customising, and extending the images in the + read more about using, customizing, and extending the images in the [Latest docs](https://airflow.apache.org/docs/docker-stack/index.html), and learn details on the internals in the [images](https://airflow.apache.org/docs/docker-stack/index.html) document. - [Tags in GitHub](https://github.com/apache/airflow/tags) to retrieve the git project sources that From 014808b3622d8791ea0b28a1e59a8b2d2a610bfc Mon Sep 17 00:00:00 2001 From: p13rr0m <16443611+p13rr0m@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:37:41 +0200 Subject: [PATCH 019/258] Fix outdated CloudRunExecuteJobOperator docs (#43195) --- .../providers/google/cloud/operators/cloud_run.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/providers/src/airflow/providers/google/cloud/operators/cloud_run.py b/providers/src/airflow/providers/google/cloud/operators/cloud_run.py index a461f84827694..01b7f2ff704ca 100644 --- a/providers/src/airflow/providers/google/cloud/operators/cloud_run.py +++ b/providers/src/airflow/providers/google/cloud/operators/cloud_run.py @@ -243,18 +243,16 @@ def execute(self, context: Context): class CloudRunExecuteJobOperator(GoogleCloudBaseOperator): """ - Executes a job and wait for the operation to be completed. Pushes the executed job to xcom. + Executes a job and waits for the operation to be completed. Pushes the executed job to xcom. :param project_id: Required. The ID of the Google Cloud project that the service belongs to. :param region: Required. The ID of the Google Cloud region that the service belongs to. :param job_name: Required. The name of the job to update. - :param job: Required. The job descriptor containing the new configuration of the job to update. - The name field will be replaced by job_name :param overrides: Optional map of override values. :param gcp_conn_id: The connection ID used to connect to Google Cloud. - :param polling_period_seconds: Optional: Control the rate of the poll for the result of deferrable run. + :param polling_period_seconds: Optional. Control the rate of the poll for the result of deferrable run. By default, the trigger will poll every 10 seconds. - :param timeout: The timeout for this request. + :param timeout_seconds: Optional. The timeout for this request, in seconds. :param impersonation_chain: Optional service account to impersonate using short-term credentials, or chained list of accounts required to get the access_token of the last account in the list, which will be impersonated in the request. @@ -263,7 +261,7 @@ class CloudRunExecuteJobOperator(GoogleCloudBaseOperator): If set as a sequence, the identities from the list must grant Service Account Token Creator IAM role to the directly preceding identity, with first account from the list granting this role to the originating account (templated). - :param deferrable: Run operator in the deferrable mode + :param deferrable: Run the operator in deferrable mode. """ template_fields = ("project_id", "region", "gcp_conn_id", "impersonation_chain", "job_name", "overrides") From 930c0d70481668023acb0f2a64b5bd225c1a45b1 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Sun, 20 Oct 2024 18:42:34 +0100 Subject: [PATCH 020/258] Bump ``pip`` to ``24.2`` (#43197) --- dev/breeze/doc/ci/02_images.md | 2 +- .../src/airflow_breeze/commands/release_management_commands.py | 2 +- dev/breeze/src/airflow_breeze/global_constants.py | 2 +- scripts/ci/install_breeze.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index d0554b74fd73d..fb17928a97f0a 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -447,7 +447,7 @@ can be used for CI images: | `DEV_APT_DEPS` | Empty - install default dependencies (see `install_os_dependencies.sh`) | Dev APT dependencies installed in the first part of the image | | `ADDITIONAL_DEV_APT_DEPS` | | Additional apt dev dependencies installed in the first part of the image | | `ADDITIONAL_DEV_APT_ENV` | | Additional env variables defined when installing dev deps | -| `AIRFLOW_PIP_VERSION` | `24.0` | PIP version used. | +| `AIRFLOW_PIP_VERSION` | `24.2` | PIP version used. | | `AIRFLOW_UV_VERSION` | `0.4.24` | UV version used. | | `AIRFLOW_USE_UV` | `true` | Whether to use UV for installation. | | `PIP_PROGRESS_BAR` | `on` | Progress bar for PIP installation | diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index d92a27381154e..b5d3d5ab04812 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -229,7 +229,7 @@ class VersionedFile(NamedTuple): file_name: str -AIRFLOW_PIP_VERSION = "24.0" +AIRFLOW_PIP_VERSION = "24.2" AIRFLOW_UV_VERSION = "0.4.24" AIRFLOW_USE_UV = False WHEEL_VERSION = "0.36.2" diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 35db4d4e530ab..97001746409ac 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -156,7 +156,7 @@ ALLOWED_INSTALL_MYSQL_CLIENT_TYPES = ["mariadb", "mysql"] -PIP_VERSION = "24.0" +PIP_VERSION = "24.2" DEFAULT_UV_HTTP_TIMEOUT = 300 DEFAULT_WSL2_HTTP_TIMEOUT = 900 diff --git a/scripts/ci/install_breeze.sh b/scripts/ci/install_breeze.sh index 5ffd604670b0a..695739a5e3281 100755 --- a/scripts/ci/install_breeze.sh +++ b/scripts/ci/install_breeze.sh @@ -25,7 +25,7 @@ if [[ ${PYTHON_VERSION=} != "" ]]; then PYTHON_ARG="--python=$(which python"${PYTHON_VERSION}") " fi -python -m pip install --upgrade pip==24.0 +python -m pip install --upgrade pip==24.2 python -m pip install "pipx>=1.4.1" python -m pipx uninstall apache-airflow-breeze >/dev/null 2>&1 || true # shellcheck disable=SC2086 From 04368b7500fbf5f44d377b0d48e2252ffe6ccb43 Mon Sep 17 00:00:00 2001 From: Amogh Desai Date: Sun, 20 Oct 2024 23:51:05 +0530 Subject: [PATCH 021/258] Consider both commit authors and PR authors in status of testing issue (#43192) * Consider both commit authors and PR authors in status of testing issue * Consider both commit authors and PR authors in status of testing issue --- .../commands/release_management_commands.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py index b5d3d5ab04812..bc7014c8c8553 100644 --- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py @@ -3476,6 +3476,22 @@ def generate_issue_content( continue pull_requests[pr_number] = pr + + # retrieve and append commit authors (to handle cherry picks) + if hasattr(pr, "get_commits"): + try: + commits = pr.get_commits() + for commit in commits: + author = commit.author + if author: + users[pr_number].add(author.login) + progress.console.print(f"Added commit author {author.login} for PR#{pr_number}") + + except Exception as e: + progress.console.print( + f"[warn]Could not retrieve commits for PR#{pr_number}: {e}, skipping[/]" + ) + # GitHub does not have linked issues in PR - but we quite rigorously add Fixes/Closes # Relate so we can find those from the body if pr.body: From f76d3b21f9789db2542da0355cbcf5b077e65e30 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Mon, 21 Oct 2024 15:56:53 +0800 Subject: [PATCH 022/258] AIP-84 Delete Pool (#43165) --- .../api_connexion/endpoints/pool_endpoint.py | 10 ++- .../core_api/openapi/v1-generated.yaml | 47 +++++++++++ .../core_api/routes/public/__init__.py | 2 + .../core_api/routes/public/pools.py | 48 ++++++++++++ airflow/ui/openapi-gen/queries/common.ts | 4 + airflow/ui/openapi-gen/queries/queries.ts | 38 +++++++++ .../ui/openapi-gen/requests/services.gen.ts | 31 ++++++++ airflow/ui/openapi-gen/requests/types.gen.ts | 37 +++++++++ .../core_api/routes/public/test_pools.py | 78 +++++++++++++++++++ 9 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 airflow/api_fastapi/core_api/routes/public/pools.py create mode 100644 tests/api_fastapi/core_api/routes/public/test_pools.py diff --git a/airflow/api_connexion/endpoints/pool_endpoint.py b/airflow/api_connexion/endpoints/pool_endpoint.py index 553d50c7464b7..6ea28f7457ee6 100644 --- a/airflow/api_connexion/endpoints/pool_endpoint.py +++ b/airflow/api_connexion/endpoints/pool_endpoint.py @@ -30,6 +30,7 @@ from airflow.api_connexion.parameters import apply_sorting, check_limit, format_parameters from airflow.api_connexion.schemas.pool_schema import PoolCollection, pool_collection_schema, pool_schema from airflow.models.pool import Pool +from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.session import NEW_SESSION, provide_session from airflow.www.decorators import action_logging @@ -39,6 +40,7 @@ from airflow.api_connexion.types import APIResponse, UpdateMask +@mark_fastapi_migration_done @security.requires_access_pool("DELETE") @action_logging @provide_session @@ -118,9 +120,11 @@ def patch_pool( # there is no way field is None here (UpdateMask is a List[str]) # so if pool_schema.declared_fields[field].attribute is None file is returned update_mask = [ - pool_schema.declared_fields[field].attribute # type: ignore[misc] - if pool_schema.declared_fields[field].attribute - else field + ( + pool_schema.declared_fields[field].attribute # type: ignore[misc] + if pool_schema.declared_fields[field].attribute + else field + ) for field in update_mask ] except KeyError as err: diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 88e292364767f..97aac85644a8c 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1010,6 +1010,53 @@ paths: application/json: schema: $ref: '#/components/schemas/HealthInfoSchema' + /public/pools/{pool_name}: + delete: + tags: + - Pool + summary: Delete Pool + description: Delete a pool entry. + operationId: delete_pool + parameters: + - name: pool_name + in: path + required: true + schema: + type: string + title: Pool Name + responses: + '204': + description: Successful Response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Bad Request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Not Found + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' components: schemas: BaseInfoSchema: diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py b/airflow/api_fastapi/core_api/routes/public/__init__.py index 3d43a7bbb0efb..12d98e44199da 100644 --- a/airflow/api_fastapi/core_api/routes/public/__init__.py +++ b/airflow/api_fastapi/core_api/routes/public/__init__.py @@ -22,6 +22,7 @@ from airflow.api_fastapi.core_api.routes.public.dag_run import dag_run_router from airflow.api_fastapi.core_api.routes.public.dags import dags_router from airflow.api_fastapi.core_api.routes.public.monitor import monitor_router +from airflow.api_fastapi.core_api.routes.public.pools import pools_router from airflow.api_fastapi.core_api.routes.public.variables import variables_router public_router = AirflowRouter(prefix="/public") @@ -32,3 +33,4 @@ public_router.include_router(variables_router) public_router.include_router(dag_run_router) public_router.include_router(monitor_router) +public_router.include_router(pools_router) diff --git a/airflow/api_fastapi/core_api/routes/public/pools.py b/airflow/api_fastapi/core_api/routes/public/pools.py new file mode 100644 index 0000000000000..2b4ffc02632c3 --- /dev/null +++ b/airflow/api_fastapi/core_api/routes/public/pools.py @@ -0,0 +1,48 @@ +# 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 fastapi import Depends, HTTPException +from sqlalchemy import delete +from sqlalchemy.orm import Session +from typing_extensions import Annotated + +from airflow.api_fastapi.common.db.common import get_session +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc +from airflow.models.pool import Pool + +pools_router = AirflowRouter(tags=["Pool"], prefix="/pools") + + +@pools_router.delete( + "/{pool_name}", + status_code=204, + responses=create_openapi_http_exception_doc([400, 401, 403, 404]), +) +async def delete_pool( + pool_name: str, + session: Annotated[Session, Depends(get_session)], +): + """Delete a pool entry.""" + if pool_name == "default_pool": + raise HTTPException(400, "Default Pool can't be deleted") + + affected_count = session.execute(delete(Pool).where(Pool.pool == pool_name)).rowcount + + if affected_count == 0: + raise HTTPException(404, f"The Pool with name: `{pool_name}` was not found") diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 7c0ab1c646163..3a31b4c0ad993 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -8,6 +8,7 @@ import { DagService, DashboardService, MonitorService, + PoolService, VariableService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; @@ -269,3 +270,6 @@ export type VariableServiceDeleteVariableMutationResult = Awaited< export type DagRunServiceDeleteDagRunMutationResult = Awaited< ReturnType >; +export type PoolServiceDeletePoolMutationResult = Awaited< + ReturnType +>; diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index 9d3e640fbf98d..6d358a7008a50 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -13,6 +13,7 @@ import { DagService, DashboardService, MonitorService, + PoolService, VariableService, } from "../requests/services.gen"; import { DAGPatchBody, DagRunState, VariableBody } from "../requests/types.gen"; @@ -763,3 +764,40 @@ export const useDagRunServiceDeleteDagRun = < }) as unknown as Promise, ...options, }); +/** + * Delete Pool + * Delete a pool entry. + * @param data The data for the request. + * @param data.poolName + * @returns void Successful Response + * @throws ApiError + */ +export const usePoolServiceDeletePool = < + TData = Common.PoolServiceDeletePoolMutationResult, + TError = unknown, + TContext = unknown, +>( + options?: Omit< + UseMutationOptions< + TData, + TError, + { + poolName: string; + }, + TContext + >, + "mutationFn" + >, +) => + useMutation< + TData, + TError, + { + poolName: string; + }, + TContext + >({ + mutationFn: ({ poolName }) => + PoolService.deletePool({ poolName }) as unknown as Promise, + ...options, + }); diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index 1561c024a83d1..64bf06c4d6f6f 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -40,6 +40,8 @@ import type { DeleteDagRunData, DeleteDagRunResponse, GetHealthResponse, + DeletePoolData, + DeletePoolResponse, } from "./types.gen"; export class AssetService { @@ -592,3 +594,32 @@ export class MonitorService { }); } } + +export class PoolService { + /** + * Delete Pool + * Delete a pool entry. + * @param data The data for the request. + * @param data.poolName + * @returns void Successful Response + * @throws ApiError + */ + public static deletePool( + data: DeletePoolData, + ): CancelablePromise { + return __request(OpenAPI, { + method: "DELETE", + url: "/public/pools/{pool_name}", + path: { + pool_name: data.poolName, + }, + errors: { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 422: "Validation Error", + }, + }); + } +} diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index aae457b13afd1..ef3847b23f2e0 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -466,6 +466,12 @@ export type DeleteDagRunResponse = void; export type GetHealthResponse = HealthInfoSchema; +export type DeletePoolData = { + poolName: string; +}; + +export type DeletePoolResponse = void; + export type $OpenApiTs = { "/ui/next_run_assets/{dag_id}": { get: { @@ -937,4 +943,35 @@ export type $OpenApiTs = { }; }; }; + "/public/pools/{pool_name}": { + delete: { + req: DeletePoolData; + res: { + /** + * Successful Response + */ + 204: void; + /** + * Bad Request + */ + 400: HTTPExceptionResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Not Found + */ + 404: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; }; diff --git a/tests/api_fastapi/core_api/routes/public/test_pools.py b/tests/api_fastapi/core_api/routes/public/test_pools.py new file mode 100644 index 0000000000000..4a5c511b8dd50 --- /dev/null +++ b/tests/api_fastapi/core_api/routes/public/test_pools.py @@ -0,0 +1,78 @@ +# 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 + +import pytest + +from airflow.models.pool import Pool +from airflow.utils.session import provide_session + +from tests_common.test_utils.db import clear_db_pools + +pytestmark = [pytest.mark.db_test, pytest.mark.skip_if_database_isolation_mode] + +POOL1_NAME = "pool1" +POOL1_SLOT = 3 +POOL1_INCLUDE_DEFERRED = True + + +POOL2_NAME = "pool2" +POOL2_SLOT = 10 +POOL2_INCLUDE_DEFERRED = False +POOL2_DESCRIPTION = "Some Description" + + +@provide_session +def _create_pools(session) -> None: + pool1 = Pool(pool=POOL1_NAME, slots=POOL1_SLOT, include_deferred=POOL1_INCLUDE_DEFERRED) + pool2 = Pool(pool=POOL2_NAME, slots=POOL2_SLOT, include_deferred=POOL2_INCLUDE_DEFERRED) + session.add_all([pool1, pool2]) + + +class TestPools: + @pytest.fixture(autouse=True) + def setup(self) -> None: + clear_db_pools() + + def teardown_method(self) -> None: + clear_db_pools() + + def create_pools(self): + _create_pools() + + +class TestDeletePool(TestPools): + def test_delete_should_respond_204(self, test_client, session): + self.create_pools() + pools = session.query(Pool).all() + assert len(pools) == 3 + response = test_client.delete(f"/public/pools/{POOL1_NAME}") + assert response.status_code == 204 + pools = session.query(Pool).all() + assert len(pools) == 2 + + def test_delete_should_respond_400(self, test_client): + response = test_client.delete("/public/pools/default_pool") + assert response.status_code == 400 + body = response.json() + assert "Default Pool can't be deleted" == body["detail"] + + def test_delete_should_respond_404(self, test_client): + response = test_client.delete(f"/public/pools/{POOL1_NAME}") + assert response.status_code == 404 + body = response.json() + assert f"The Pool with name: `{POOL1_NAME}` was not found" == body["detail"] From e5f55377d29ae67315cf59ff3bf4895241a644a0 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Mon, 21 Oct 2024 10:08:13 +0100 Subject: [PATCH 023/258] Run Task SDK tests without DB (#43181) --- .github/workflows/task-sdk-tests.yml | 3 +- dev/breeze/doc/05_test_commands.rst | 21 ++ ...utput_setup_check-all-params-in-groups.svg | 2 +- ...utput_setup_check-all-params-in-groups.txt | 2 +- ...output_setup_regenerate-command-images.svg | 2 +- ...output_setup_regenerate-command-images.txt | 2 +- dev/breeze/doc/images/output_testing.svg | 26 +- dev/breeze/doc/images/output_testing.txt | 2 +- .../images/output_testing_task-sdk-tests.svg | 296 ++++++++++++++++++ .../images/output_testing_task-sdk-tests.txt | 1 + .../commands/testing_commands.py | 62 ++++ .../commands/testing_commands_config.py | 48 +++ .../src/airflow_breeze/utils/run_tests.py | 11 +- task_sdk/tests/conftest.py | 5 + 14 files changed, 460 insertions(+), 23 deletions(-) create mode 100644 dev/breeze/doc/images/output_testing_task-sdk-tests.svg create mode 100644 dev/breeze/doc/images/output_testing_task-sdk-tests.txt diff --git a/.github/workflows/task-sdk-tests.yml b/.github/workflows/task-sdk-tests.yml index 2d55b108fea1f..d1d152648cb8b 100644 --- a/.github/workflows/task-sdk-tests.yml +++ b/.github/workflows/task-sdk-tests.yml @@ -82,5 +82,4 @@ jobs: - name: > Run unit tests for Airflow Task SDK:Python ${{ matrix.python-version }} run: > - breeze testing tests --run-in-parallel - --parallel-test-types TaskSDK + breeze testing task-sdk-tests --python "${{ matrix.python-version }}" diff --git a/dev/breeze/doc/05_test_commands.rst b/dev/breeze/doc/05_test_commands.rst index 79aa206921d21..e210017088ac3 100644 --- a/dev/breeze/doc/05_test_commands.rst +++ b/dev/breeze/doc/05_test_commands.rst @@ -209,6 +209,27 @@ Here is the detailed set of options for the ``breeze testing non-db-tests`` comm :alt: Breeze testing non-db-tests +Using ``breeze testing task-sdk-tests`` command +............................................ + +The ``breeze testing task-sdk-tests`` command is simplified version of the ``breeze testing tests`` command +that allows you to run tests for Task SDK without initializing database. The Task SDK should not need +database to be started so this acts as a good check to see if the Task SDK tests are working properly. + +Run all Task SDK tests: + +.. code-block:: bash + + breeze testing task-sdk-tests + +Here is the detailed set of options for the ``breeze testing task-sdk-tests`` command. + +.. image:: ./images/output_testing_task-sdk-tests.svg + :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_testing_task-sdk-tests.svg + :width: 100% + :alt: Breeze testing task-sdk-tests + + Running integration tests ......................... diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg index a5f797e9cf211..836aa93edd3c8 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg @@ -199,7 +199,7 @@ setup:check-all-params-in-groups | setup:config | setup:regenerate-command-images | setup:self-upgrade  | setup:synchronize-local-mounts | setup:version | shell | start-airflow | static-checks | testing |    testing:db-tests | testing:docker-compose-tests | testing:helm-tests | testing:integration-tests |      -testing:non-db-tests | testing:tests)                                                                   +testing:non-db-tests | testing:task-sdk-tests | testing:tests)                                          ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ --verbose-vPrint verbose information about performed steps. diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt index d2cab78ff8c5b..a467d9b754f1b 100644 --- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt +++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt @@ -1 +1 @@ -852bdb14696f768b8a22551ba88bf061 +e12840c7b1262cfcb539fddec1079941 diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg index c107a1843b4e5..9691c2ced1158 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg @@ -213,7 +213,7 @@ setup:check-all-params-in-groups | setup:config | setup:regenerate-command-images |                  setup:self-upgrade | setup:synchronize-local-mounts | setup:version | shell | start-airflow |        static-checks | testing | testing:db-tests | testing:docker-compose-tests | testing:helm-tests |     -testing:integration-tests | testing:non-db-tests | testing:tests)                                    +testing:integration-tests | testing:non-db-tests | testing:task-sdk-tests | testing:tests)           --check-onlyOnly check if some images need to be regenerated. Return 0 if no need or 1 if needed. Cannot be used together with --command flag or --force.                                                             ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt index 751b0329d71fc..2ab847da1bb10 100644 --- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt +++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt @@ -1 +1 @@ -326695396d27b77b860202ebe9267746 +6139c005ad900ae2ae1c9776a693469b diff --git a/dev/breeze/doc/images/output_testing.svg b/dev/breeze/doc/images/output_testing.svg index c85e5a60979a7..6042ff509acac 100644 --- a/dev/breeze/doc/images/output_testing.svg +++ b/dev/breeze/doc/images/output_testing.svg @@ -1,4 +1,4 @@ - + docker-compose-testsRun docker-compose tests.                                                                      ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -db-tests    Run all (default) or specified DB-bound unit tests. This is a dedicated command that only runs DB      -tests and it runs them in parallel via splitting tests by test types into separate containers with     -separate database started for each container.                                                          -non-db-testsRun all (default) or specified Non-DB unit tests. This is a dedicated command that only runs Non-DB    -tests and it runs them in parallel via pytest-xdist in single container, with `none` backend set.      -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +db-tests      Run all (default) or specified DB-bound unit tests. This is a dedicated command that only runs DB    +tests and it runs them in parallel via splitting tests by test types into separate containers with   +separate database started for each container.                                                        +non-db-tests  Run all (default) or specified Non-DB unit tests. This is a dedicated command that only runs Non-DB  +tests and it runs them in parallel via pytest-xdist in single container, with `none` backend set.    +task-sdk-testsRun task-sdk tests. This is a dedicated command that only runs Task SDk tests & don't need DB and it +runs them in parallel via pytest-xdist in single container, with `none` backend set.                 +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/dev/breeze/doc/images/output_testing.txt b/dev/breeze/doc/images/output_testing.txt index 5f088aae9e13e..8052011626153 100644 --- a/dev/breeze/doc/images/output_testing.txt +++ b/dev/breeze/doc/images/output_testing.txt @@ -1 +1 @@ -1447b50a7f4c843b01e92358a6b2e072 +dab71fe178a36e68a33d247f45886387 diff --git a/dev/breeze/doc/images/output_testing_task-sdk-tests.svg b/dev/breeze/doc/images/output_testing_task-sdk-tests.svg new file mode 100644 index 0000000000000..ac5c8e76a62f7 --- /dev/null +++ b/dev/breeze/doc/images/output_testing_task-sdk-tests.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Command: testing task-sdk-tests + + + + + + + + + + +Usage:breeze testing task-sdk-tests[OPTIONS] [EXTRA_PYTEST_ARGS]... + +Run task-sdk tests. This is a dedicated command that only runs Task SDk tests & don't need DB and it runs them in  +parallel via pytest-xdist in single container, with `none` backend set. + +╭─ Test options ───────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--test-timeoutTest timeout in seconds. Set the pytest setup, execution and teardown timeouts to this value +(INTEGER RANGE)                                                                              +[default: 60; x>=0]                                                                          +--enable-coverageEnable coverage capturing for tests in the form of XML files +--collect-onlyCollect tests only, do not run them. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Test environment ───────────────────────────────────────────────────────────────────────────────────────────────────╮ +--python-pPython major/minor version used in Airflow image for images. +(>3.9< | 3.10 | 3.11 | 3.12)                                 +[default: 3.9]                                               +--forward-credentials-fForward local credentials to container when running. +--force-sa-warnings/--no-force-sa-warningsEnable `sqlalchemy.exc.MovedIn20Warning` during the tests runs. +[default: force-sa-warnings]                                    +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options for parallel test commands ─────────────────────────────────────────────────────────────────────────────────╮ +--parallelismMaximum number of processes to use while running the operation in parallel. +(INTEGER RANGE)                                                             +[default: 4; 1<=x<=8]                                                       +--skip-cleanupSkip cleanup of temporary files created during parallel run. +--debug-resourcesWhether to show resource information while running in parallel. +--include-success-outputsWhether to include outputs of successful parallel runs (skipped by default). +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Upgrading/downgrading/removing selected packages ───────────────────────────────────────────────────────────────────╮ +--downgrade-sqlalchemyDowngrade SQLAlchemy to minimum supported version. +--downgrade-pendulumDowngrade Pendulum to minimum supported version. +--remove-arm-packagesRemoves arm packages from the image to test if ARM collection works +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Advanced flag for tests command ────────────────────────────────────────────────────────────────────────────────────╮ +--airflow-constraints-referenceConstraint reference to use for airflow installation (used in calculated        +constraints URL).                                                               +(TEXT)                                                                          +--clean-airflow-installationClean the airflow installation before installing version specified by           +--use-airflow-version.                                                          +--github-repository-gGitHub repository used to pull, push run images.(TEXT) +[default: apache/airflow]                        +--image-tagTag of the image which is used to run the image (implies --mount-sources=skip). +(TEXT)                                                                          +[default: latest]                                                               +--package-formatFormat of packages.(wheel | sdist | both)[default: wheel] +--mount-sourcesChoose scope of local sources that should be mounted, skipped, or removed       +(default = selected).                                                           +(selected | all | skip | remove | tests | providers-and-tests)                  +[default: selected]                                                             +--skip-docker-compose-downSkips running docker-compose down after tests +--keep-env-variablesDo not clear environment variables that might have side effect while running    +tests                                                                           +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Common options ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--dry-run-DIf dry-run is set, commands are only printed, not executed. +--verbose-vPrint verbose information about performed steps. +--help-hShow this message and exit. +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/dev/breeze/doc/images/output_testing_task-sdk-tests.txt b/dev/breeze/doc/images/output_testing_task-sdk-tests.txt new file mode 100644 index 0000000000000..f432c3ded6dd1 --- /dev/null +++ b/dev/breeze/doc/images/output_testing_task-sdk-tests.txt @@ -0,0 +1 @@ +c02f626caa20d7bff91f92a198234cf5 diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py b/dev/breeze/src/airflow_breeze/commands/testing_commands.py index c3bb4e9193204..3c3015cd7fe12 100644 --- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py @@ -684,6 +684,68 @@ def command_for_non_db_tests(**kwargs): ) +@group_for_testing.command( + name="task-sdk-tests", + help="Run task-sdk tests. This is a dedicated command that only " + "runs Task SDk tests & don't need DB and it runs them in parallel via pytest-xdist in single container, " + "with `none` backend set.", + context_settings=dict( + ignore_unknown_options=False, + allow_extra_args=False, + ), +) +@option_airflow_constraints_reference +@option_clean_airflow_installation +@option_collect_only +@option_debug_resources +@option_downgrade_pendulum +@option_downgrade_sqlalchemy +@option_dry_run +@option_enable_coverage +@option_force_sa_warnings +@option_forward_credentials +@option_github_repository +@option_image_tag_for_running +@option_include_success_outputs +@option_keep_env_variables +@option_mount_sources +@option_package_format +@option_parallelism +@option_python +@option_remove_arm_packages +@option_skip_cleanup +@option_skip_docker_compose_down +@option_test_timeout +@option_verbose +@click.argument("extra_pytest_args", nargs=-1, type=click.UNPROCESSED) +def command_for_task_sdk_tests(**kwargs): + _run_test_command( + backend="none", + database_isolation=False, + db_reset=False, + integration=(), + run_db_tests_only=False, + run_in_parallel=False, + skip_db_tests=True, + test_type="TaskSDK", + use_xdist=True, + excluded_parallel_test_types="", + excluded_providers="", + force_lowest_dependencies=False, + install_airflow_with_constraints=False, + no_db_cleanup=True, + parallel_test_types="", + providers_constraints_location="", + providers_skip_constraints=False, + skip_provider_tests=True, + skip_providers="", + upgrade_boto=False, + use_airflow_version=None, + use_packages_from_dist=False, + **kwargs, + ) + + def _run_test_command( *, airflow_constraints_reference: str, diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py index 34c0dbff00934..2732bcb13b007 100644 --- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py +++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py @@ -169,6 +169,54 @@ ], }, ], + "breeze testing task-sdk-tests": [ + { + "name": "Test options", + "options": [ + "--test-timeout", + "--enable-coverage", + "--collect-only", + ], + }, + { + "name": "Test environment", + "options": [ + "--python", + "--forward-credentials", + "--force-sa-warnings", + ], + }, + { + "name": "Options for parallel test commands", + "options": [ + "--parallelism", + "--skip-cleanup", + "--debug-resources", + "--include-success-outputs", + ], + }, + { + "name": "Upgrading/downgrading/removing selected packages", + "options": [ + "--downgrade-sqlalchemy", + "--downgrade-pendulum", + "--remove-arm-packages", + ], + }, + { + "name": "Advanced flag for tests command", + "options": [ + "--airflow-constraints-reference", + "--clean-airflow-installation", + "--github-repository", + "--image-tag", + "--package-format", + "--mount-sources", + "--skip-docker-compose-down", + "--keep-env-variables", + ], + }, + ], "breeze testing db-tests": [ { "name": "Select tests to run", diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py b/dev/breeze/src/airflow_breeze/utils/run_tests.py index acde9d0cc4d42..99763c1196bf1 100644 --- a/dev/breeze/src/airflow_breeze/utils/run_tests.py +++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py @@ -300,13 +300,10 @@ def generate_args_for_pytest( no_db_cleanup: bool, ): result_log_file, warnings_file, coverage_file = test_paths(test_type, backend, helm_test_package) - if skip_db_tests: - if parallel_test_types_list: - args = convert_parallel_types_to_folders( - parallel_test_types_list, skip_provider_tests, python_version=python_version - ) - else: - args = ["tests"] if test_type != "None" else [] + if skip_db_tests and parallel_test_types_list: + args = convert_parallel_types_to_folders( + parallel_test_types_list, skip_provider_tests, python_version=python_version + ) else: args = convert_test_type_to_pytest_args( test_type=test_type, diff --git a/task_sdk/tests/conftest.py b/task_sdk/tests/conftest.py index a410a1217c2f0..ddc7c61656a09 100644 --- a/task_sdk/tests/conftest.py +++ b/task_sdk/tests/conftest.py @@ -16,10 +16,15 @@ # under the License. from __future__ import annotations +import os + import pytest pytest_plugins = "tests_common.pytest_plugin" +# Task SDK does not need access to the Airflow database +os.environ["_AIRFLOW_SKIP_DB_TESTS"] = "true" + @pytest.hookimpl(tryfirst=True) def pytest_configure(config: pytest.Config) -> None: From f37f29bc7ed50f9709606e4f6fed66136e8a05c7 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Mon, 21 Oct 2024 17:22:14 +0800 Subject: [PATCH 024/258] AIP-84 Get Providers (#43159) --- .../endpoints/provider_endpoint.py | 2 + .../core_api/openapi/v1-generated.yaml | 69 +++++++++++++++++ .../core_api/routes/public/__init__.py | 2 + .../core_api/routes/public/providers.py | 55 ++++++++++++++ .../core_api/serializers/providers.py | 35 +++++++++ airflow/ui/openapi-gen/queries/common.ts | 19 +++++ airflow/ui/openapi-gen/queries/prefetch.ts | 24 ++++++ airflow/ui/openapi-gen/queries/queries.ts | 33 ++++++++ airflow/ui/openapi-gen/queries/suspense.ts | 33 ++++++++ .../ui/openapi-gen/requests/schemas.gen.ts | 41 ++++++++++ .../ui/openapi-gen/requests/services.gen.ts | 29 +++++++ airflow/ui/openapi-gen/requests/types.gen.ts | 39 ++++++++++ .../core_api/routes/public/test_providers.py | 75 +++++++++++++++++++ 13 files changed, 456 insertions(+) create mode 100644 airflow/api_fastapi/core_api/routes/public/providers.py create mode 100644 airflow/api_fastapi/core_api/serializers/providers.py create mode 100644 tests/api_fastapi/core_api/routes/public/test_providers.py diff --git a/airflow/api_connexion/endpoints/provider_endpoint.py b/airflow/api_connexion/endpoints/provider_endpoint.py index d9ba0c819b702..1eb032dee030e 100644 --- a/airflow/api_connexion/endpoints/provider_endpoint.py +++ b/airflow/api_connexion/endpoints/provider_endpoint.py @@ -28,6 +28,7 @@ ) from airflow.auth.managers.models.resource_details import AccessView from airflow.providers_manager import ProvidersManager +from airflow.utils.api_migration import mark_fastapi_migration_done if TYPE_CHECKING: from airflow.api_connexion.types import APIResponse @@ -46,6 +47,7 @@ def _provider_mapper(provider: ProviderInfo) -> Provider: ) +@mark_fastapi_migration_done @security.requires_access_view(AccessView.PROVIDERS) def get_providers() -> APIResponse: """Get providers.""" diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 97aac85644a8c..88dc7428bed6b 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1057,6 +1057,41 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /public/providers/: + get: + tags: + - Provider + summary: Get Providers + description: Get providers. + operationId: get_providers + parameters: + - name: limit + in: query + required: false + schema: + type: integer + default: 100 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ProviderCollectionResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' components: schemas: BaseInfoSchema: @@ -1803,6 +1838,40 @@ components: - task_instance_states title: HistoricalMetricDataResponse description: Historical Metric Data serializer for responses. + ProviderCollectionResponse: + properties: + providers: + items: + $ref: '#/components/schemas/ProviderResponse' + type: array + title: Providers + total_entries: + type: integer + title: Total Entries + type: object + required: + - providers + - total_entries + title: ProviderCollectionResponse + description: Provider Collection serializer for responses. + ProviderResponse: + properties: + package_name: + type: string + title: Package Name + description: + type: string + title: Description + version: + type: string + title: Version + type: object + required: + - package_name + - description + - version + title: ProviderResponse + description: Provider serializer for responses. SchedulerInfoSchema: properties: status: diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py b/airflow/api_fastapi/core_api/routes/public/__init__.py index 12d98e44199da..0778d66cc4137 100644 --- a/airflow/api_fastapi/core_api/routes/public/__init__.py +++ b/airflow/api_fastapi/core_api/routes/public/__init__.py @@ -23,6 +23,7 @@ from airflow.api_fastapi.core_api.routes.public.dags import dags_router from airflow.api_fastapi.core_api.routes.public.monitor import monitor_router from airflow.api_fastapi.core_api.routes.public.pools import pools_router +from airflow.api_fastapi.core_api.routes.public.providers import providers_router from airflow.api_fastapi.core_api.routes.public.variables import variables_router public_router = AirflowRouter(prefix="/public") @@ -34,3 +35,4 @@ public_router.include_router(dag_run_router) public_router.include_router(monitor_router) public_router.include_router(pools_router) +public_router.include_router(providers_router) diff --git a/airflow/api_fastapi/core_api/routes/public/providers.py b/airflow/api_fastapi/core_api/routes/public/providers.py new file mode 100644 index 0000000000000..6c01578dd5f69 --- /dev/null +++ b/airflow/api_fastapi/core_api/routes/public/providers.py @@ -0,0 +1,55 @@ +# 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 + +import re2 + +from airflow.api_fastapi.common.parameters import QueryLimit, QueryOffset +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.serializers.providers import ProviderCollectionResponse, ProviderResponse +from airflow.providers_manager import ProviderInfo, ProvidersManager + +providers_router = AirflowRouter(tags=["Provider"], prefix="/providers") + + +def _remove_rst_syntax(value: str) -> str: + return re2.sub("[`_<>]", "", value.strip(" \n.")) + + +def _provider_mapper(provider: ProviderInfo) -> ProviderResponse: + return ProviderResponse( + package_name=provider.data["package-name"], + description=_remove_rst_syntax(provider.data["description"]), + version=provider.version, + ) + + +@providers_router.get("/") +async def get_providers( + limit: QueryLimit, + offset: QueryOffset, +) -> ProviderCollectionResponse: + """Get providers.""" + providers = sorted( + [_provider_mapper(d) for d in ProvidersManager().providers.values()], key=lambda x: x.package_name + ) + total_entries = len(providers) + + if limit.value is not None and offset.value is not None: + providers = providers[offset.value : offset.value + limit.value] + return ProviderCollectionResponse(providers=providers, total_entries=total_entries) diff --git a/airflow/api_fastapi/core_api/serializers/providers.py b/airflow/api_fastapi/core_api/serializers/providers.py new file mode 100644 index 0000000000000..4e542f19f9f8e --- /dev/null +++ b/airflow/api_fastapi/core_api/serializers/providers.py @@ -0,0 +1,35 @@ +# 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 ProviderResponse(BaseModel): + """Provider serializer for responses.""" + + package_name: str + description: str + version: str + + +class ProviderCollectionResponse(BaseModel): + """Provider Collection serializer for responses.""" + + providers: list[ProviderResponse] + total_entries: int diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 3a31b4c0ad993..98317d58ee4f3 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -9,6 +9,7 @@ import { DashboardService, MonitorService, PoolService, + ProviderService, VariableService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; @@ -246,6 +247,24 @@ export const UseMonitorServiceGetHealthKeyFn = (queryKey?: Array) => [ useMonitorServiceGetHealthKey, ...(queryKey ?? []), ]; +export type ProviderServiceGetProvidersDefaultResponse = Awaited< + ReturnType +>; +export type ProviderServiceGetProvidersQueryResult< + TData = ProviderServiceGetProvidersDefaultResponse, + TError = unknown, +> = UseQueryResult; +export const useProviderServiceGetProvidersKey = "ProviderServiceGetProviders"; +export const UseProviderServiceGetProvidersKeyFn = ( + { + limit, + offset, + }: { + limit?: number; + offset?: number; + } = {}, + queryKey?: Array, +) => [useProviderServiceGetProvidersKey, ...(queryKey ?? [{ limit, offset }])]; export type VariableServicePostVariableMutationResult = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index 98c706a63fc12..af05a26fc2618 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -8,6 +8,7 @@ import { DagService, DashboardService, MonitorService, + ProviderService, VariableService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; @@ -300,3 +301,26 @@ export const prefetchUseMonitorServiceGetHealth = (queryClient: QueryClient) => queryKey: Common.UseMonitorServiceGetHealthKeyFn(), queryFn: () => MonitorService.getHealth(), }); +/** + * Get Providers + * Get providers. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @returns ProviderCollectionResponse Successful Response + * @throws ApiError + */ +export const prefetchUseProviderServiceGetProviders = ( + queryClient: QueryClient, + { + limit, + offset, + }: { + limit?: number; + offset?: number; + } = {}, +) => + queryClient.prefetchQuery({ + queryKey: Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }), + queryFn: () => ProviderService.getProviders({ limit, offset }), + }); diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index 6d358a7008a50..bf9a744f6cb53 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -14,6 +14,7 @@ import { DashboardService, MonitorService, PoolService, + ProviderService, VariableService, } from "../requests/services.gen"; import { DAGPatchBody, DagRunState, VariableBody } from "../requests/types.gen"; @@ -387,6 +388,38 @@ export const useMonitorServiceGetHealth = < queryFn: () => MonitorService.getHealth() as TData, ...options, }); +/** + * Get Providers + * Get providers. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @returns ProviderCollectionResponse Successful Response + * @throws ApiError + */ +export const useProviderServiceGetProviders = < + TData = Common.ProviderServiceGetProvidersDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + }: { + limit?: number; + offset?: number; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useQuery({ + queryKey: Common.UseProviderServiceGetProvidersKeyFn( + { limit, offset }, + queryKey, + ), + queryFn: () => ProviderService.getProviders({ limit, offset }) as TData, + ...options, + }); /** * Post Variable * Create a variable. diff --git a/airflow/ui/openapi-gen/queries/suspense.ts b/airflow/ui/openapi-gen/queries/suspense.ts index d3d8b7b3441a4..fad0d5b7a5a8e 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -8,6 +8,7 @@ import { DagService, DashboardService, MonitorService, + ProviderService, VariableService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; @@ -381,3 +382,35 @@ export const useMonitorServiceGetHealthSuspense = < queryFn: () => MonitorService.getHealth() as TData, ...options, }); +/** + * Get Providers + * Get providers. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @returns ProviderCollectionResponse Successful Response + * @throws ApiError + */ +export const useProviderServiceGetProvidersSuspense = < + TData = Common.ProviderServiceGetProvidersDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + limit, + offset, + }: { + limit?: number; + offset?: number; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useSuspenseQuery({ + queryKey: Common.UseProviderServiceGetProvidersKeyFn( + { limit, offset }, + queryKey, + ), + queryFn: () => ProviderService.getProviders({ limit, offset }) as TData, + ...options, + }); diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index f2214e2e30483..23458a325883d 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1169,6 +1169,47 @@ export const $HistoricalMetricDataResponse = { description: "Historical Metric Data serializer for responses.", } as const; +export const $ProviderCollectionResponse = { + properties: { + providers: { + items: { + $ref: "#/components/schemas/ProviderResponse", + }, + type: "array", + title: "Providers", + }, + total_entries: { + type: "integer", + title: "Total Entries", + }, + }, + type: "object", + required: ["providers", "total_entries"], + title: "ProviderCollectionResponse", + description: "Provider Collection serializer for responses.", +} as const; + +export const $ProviderResponse = { + properties: { + package_name: { + type: "string", + title: "Package Name", + }, + description: { + type: "string", + title: "Description", + }, + version: { + type: "string", + title: "Version", + }, + }, + type: "object", + required: ["package_name", "description", "version"], + title: "ProviderResponse", + description: "Provider serializer for responses.", +} as const; + export const $SchedulerInfoSchema = { properties: { status: { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index 64bf06c4d6f6f..0fa959e9725fa 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -42,6 +42,8 @@ import type { GetHealthResponse, DeletePoolData, DeletePoolResponse, + GetProvidersData, + GetProvidersResponse, } from "./types.gen"; export class AssetService { @@ -623,3 +625,30 @@ export class PoolService { }); } } + +export class ProviderService { + /** + * Get Providers + * Get providers. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @returns ProviderCollectionResponse Successful Response + * @throws ApiError + */ + public static getProviders( + data: GetProvidersData = {}, + ): CancelablePromise { + return __request(OpenAPI, { + method: "GET", + url: "/public/providers/", + query: { + limit: data.limit, + offset: data.offset, + }, + errors: { + 422: "Validation Error", + }, + }); + } +} diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index ef3847b23f2e0..8cbc0b7e0dabd 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -258,6 +258,23 @@ export type HistoricalMetricDataResponse = { task_instance_states: TaskInstanceState; }; +/** + * Provider Collection serializer for responses. + */ +export type ProviderCollectionResponse = { + providers: Array; + total_entries: number; +}; + +/** + * Provider serializer for responses. + */ +export type ProviderResponse = { + package_name: string; + description: string; + version: string; +}; + /** * Schema for Scheduler info. */ @@ -472,6 +489,13 @@ export type DeletePoolData = { export type DeletePoolResponse = void; +export type GetProvidersData = { + limit?: number; + offset?: number; +}; + +export type GetProvidersResponse = ProviderCollectionResponse; + export type $OpenApiTs = { "/ui/next_run_assets/{dag_id}": { get: { @@ -974,4 +998,19 @@ export type $OpenApiTs = { }; }; }; + "/public/providers/": { + get: { + req: GetProvidersData; + res: { + /** + * Successful Response + */ + 200: ProviderCollectionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; }; diff --git a/tests/api_fastapi/core_api/routes/public/test_providers.py b/tests/api_fastapi/core_api/routes/public/test_providers.py new file mode 100644 index 0000000000000..500913b7f6173 --- /dev/null +++ b/tests/api_fastapi/core_api/routes/public/test_providers.py @@ -0,0 +1,75 @@ +# 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 unittest import mock + +import pytest + +from airflow.providers_manager import ProviderInfo + +pytestmark = [pytest.mark.db_test, pytest.mark.skip_if_database_isolation_mode] + +MOCK_PROVIDERS = { + "apache-airflow-providers-amazon": ProviderInfo( + "1.0.0", + { + "package-name": "apache-airflow-providers-amazon", + "name": "Amazon", + "description": "`Amazon Web Services (AWS) `__.\n", + "versions": ["1.0.0"], + }, + "package", + ), + "apache-airflow-providers-apache-cassandra": ProviderInfo( + "1.0.0", + { + "package-name": "apache-airflow-providers-apache-cassandra", + "name": "Apache Cassandra", + "description": "`Apache Cassandra `__.\n", + "versions": ["1.0.0"], + }, + "package", + ), +} + + +class TestGetProviders: + @pytest.mark.parametrize( + "query_params, expected_total_entries, expected_package_name", + [ + # Filters + ({}, 2, ["apache-airflow-providers-amazon", "apache-airflow-providers-apache-cassandra"]), + ({"limit": 1}, 2, ["apache-airflow-providers-amazon"]), + ({"offset": 1}, 2, ["apache-airflow-providers-apache-cassandra"]), + ], + ) + @mock.patch( + "airflow.providers_manager.ProvidersManager.providers", + new_callable=mock.PropertyMock, + return_value=MOCK_PROVIDERS, + ) + def test_get_dags( + self, mock_provider, test_client, query_params, expected_total_entries, expected_package_name + ): + response = test_client.get("/public/providers", params=query_params) + + assert response.status_code == 200 + body = response.json() + + assert body["total_entries"] == expected_total_entries + assert [provider["package_name"] for provider in body["providers"]] == expected_package_name From f3bd2c27bd83d1141cfb92ed5c88d1e2d2a388d1 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU <68415893+jason810496@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:23:13 +0800 Subject: [PATCH 025/258] AIP-84 | Public list tags API (#42959) * AIP-84 | Public list tags API * refactor: upd resp schema, use paginated_select * refactor(test): move test to routers/public folder * refactor: remove OrderBy param, use SortParm --- airflow/api_fastapi/common/parameters.py | 13 ++ .../core_api/openapi/v1-generated.yaml | 78 +++++++++++ .../core_api/routes/public/dags.py | 39 +++++- .../api_fastapi/core_api/serializers/dags.py | 7 + airflow/ui/openapi-gen/queries/common.ts | 25 ++++ airflow/ui/openapi-gen/queries/prefetch.ts | 35 +++++ airflow/ui/openapi-gen/queries/queries.ts | 44 +++++++ airflow/ui/openapi-gen/queries/suspense.ts | 44 +++++++ .../ui/openapi-gen/requests/schemas.gen.ts | 20 +++ .../ui/openapi-gen/requests/services.gen.ts | 33 +++++ airflow/ui/openapi-gen/requests/types.gen.ts | 40 ++++++ .../core_api/routes/public/test_dags.py | 122 +++++++++++++++++- 12 files changed, 497 insertions(+), 3 deletions(-) diff --git a/airflow/api_fastapi/common/parameters.py b/airflow/api_fastapi/common/parameters.py index 4aa8335905ca0..9b265c7583a7e 100644 --- a/airflow/api_fastapi/common/parameters.py +++ b/airflow/api_fastapi/common/parameters.py @@ -265,6 +265,17 @@ def depends(self, last_dag_run_state: DagRunState | None = None) -> _LastDagRunS return self.set_value(last_dag_run_state) +class _DagTagNamePatternSearch(_SearchParam): + """Search on dag_tag.name.""" + + def __init__(self, skip_none: bool = True) -> None: + super().__init__(DagTag.name, skip_none) + + def depends(self, tag_name_pattern: str | None = None) -> _DagTagNamePatternSearch: + tag_name_pattern = super().transform_aliases(tag_name_pattern) + return self.set_value(tag_name_pattern) + + def _safe_parse_datetime(date_to_check: str) -> datetime: """ Parse datetime and raise error for invalid dates. @@ -299,3 +310,5 @@ def _safe_parse_datetime(date_to_check: str) -> datetime: QueryOwnersFilter = Annotated[_OwnersFilter, Depends(_OwnersFilter().depends)] # DagRun QueryLastDagRunStateFilter = Annotated[_LastDagRunStateFilter, Depends(_LastDagRunStateFilter().depends)] +# DAGTags +QueryDagTagPatternSearch = Annotated[_DagTagNamePatternSearch, Depends(_DagTagNamePatternSearch().depends)] diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 88dc7428bed6b..325a6354de2b2 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -291,6 +291,68 @@ 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 + default: 100 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + 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: @@ -1713,6 +1775,22 @@ components: - dataset_triggered title: DAGRunTypes description: DAG Run Types for responses. + DAGTagCollectionResponse: + properties: + tags: + items: + type: string + type: array + title: Tags + total_entries: + type: integer + title: Total Entries + type: object + required: + - tags + - total_entries + title: DAGTagCollectionResponse + description: DAG Tags Collection serializer for responses. DagProcessorInfoSchema: properties: status: diff --git a/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow/api_fastapi/core_api/routes/public/dags.py index 81293211fc92e..c7b753b5cdbd9 100644 --- a/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow/api_fastapi/core_api/routes/public/dags.py @@ -18,7 +18,7 @@ from __future__ import annotations from fastapi import Depends, HTTPException, Query, Request, Response -from sqlalchemy import update +from sqlalchemy import select, update from sqlalchemy.orm import Session from typing_extensions import Annotated @@ -32,6 +32,7 @@ QueryDagDisplayNamePatternSearch, QueryDagIdPatternSearch, QueryDagIdPatternSearchWithNone, + QueryDagTagPatternSearch, QueryLastDagRunStateFilter, QueryLimit, QueryOffset, @@ -48,9 +49,10 @@ DAGDetailsResponse, DAGPatchBody, DAGResponse, + DAGTagCollectionResponse, ) from airflow.exceptions import AirflowException, DagNotFound -from airflow.models import DAG, DagModel +from airflow.models import DAG, DagModel, DagTag dags_router = AirflowRouter(tags=["DAG"], prefix="/dags") @@ -95,6 +97,39 @@ async def get_dags( ) +@dags_router.get( + "/tags", + responses=create_openapi_http_exception_doc([401, 403]), +) +async def get_dag_tags( + limit: QueryLimit, + offset: QueryOffset, + order_by: Annotated[ + SortParam, + Depends( + SortParam( + ["name"], + DagTag, + ).dynamic_depends() + ), + ], + tag_name_pattern: QueryDagTagPatternSearch, + session: Annotated[Session, Depends(get_session)], +) -> DAGTagCollectionResponse: + """Get all DAG tags.""" + base_select = select(DagTag.name).group_by(DagTag.name) + dag_tags_select, total_entries = paginated_select( + base_select=base_select, + 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=[dag_tag for dag_tag in dag_tags], total_entries=total_entries) + + @dags_router.get("/{dag_id}", responses=create_openapi_http_exception_doc([400, 401, 403, 404, 422])) async def get_dag( dag_id: str, session: Annotated[Session, Depends(get_session)], request: Request diff --git a/airflow/api_fastapi/core_api/serializers/dags.py b/airflow/api_fastapi/core_api/serializers/dags.py index c9d48aac222eb..39e85ea8c6f0e 100644 --- a/airflow/api_fastapi/core_api/serializers/dags.py +++ b/airflow/api_fastapi/core_api/serializers/dags.py @@ -156,3 +156,10 @@ 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/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 98317d58ee4f3..5e950de8447e9 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -102,6 +102,31 @@ 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 >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index af05a26fc2618..8807201d42992 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -129,6 +129,41 @@ 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. diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index bf9a744f6cb53..ac6939d9f6f7a 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -157,6 +157,50 @@ 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. diff --git a/airflow/ui/openapi-gen/queries/suspense.ts b/airflow/ui/openapi-gen/queries/suspense.ts index fad0d5b7a5a8e..1e497418893e8 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -151,6 +151,50 @@ 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. diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index 23458a325883d..aa6437118eeea 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1013,6 +1013,26 @@ export const $DAGRunTypes = { description: "DAG Run Types for responses.", } as const; +export const $DAGTagCollectionResponse = { + properties: { + tags: { + items: { + type: "string", + }, + type: "array", + title: "Tags", + }, + total_entries: { + type: "integer", + title: "Total Entries", + }, + }, + type: "object", + required: ["tags", "total_entries"], + title: "DAGTagCollectionResponse", + description: "DAG Tags Collection serializer for responses.", +} as const; + export const $DagProcessorInfoSchema = { properties: { status: { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index 0fa959e9725fa..d6f46b283faa1 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -11,6 +11,8 @@ import type { GetDagsResponse, PatchDagsData, PatchDagsResponse, + GetDagTagsData, + GetDagTagsResponse, GetDagData, GetDagResponse, PatchDagData, @@ -186,6 +188,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/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. diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 8cbc0b7e0dabd..c795fce3e540d 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -177,6 +177,14 @@ export type DAGRunTypes = { dataset_triggered: number; }; +/** + * DAG Tags Collection serializer for responses. + */ +export type DAGTagCollectionResponse = { + tags: Array; + total_entries: number; +}; + /** * Schema for DagProcessor info. */ @@ -387,6 +395,15 @@ 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; }; @@ -577,6 +594,29 @@ 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; 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 edc350c27b84a..a48040482023e 100644 --- a/tests/api_fastapi/core_api/routes/public/test_dags.py +++ b/tests/api_fastapi/core_api/routes/public/test_dags.py @@ -21,7 +21,7 @@ import pendulum import pytest -from airflow.models.dag import DagModel +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 @@ -88,6 +88,11 @@ def _create_deactivated_paused_dag(self, session=None): 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: @@ -118,6 +123,7 @@ def setup(self, dag_maker, session=None) -> None: 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 @@ -386,6 +392,120 @@ 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.""" From 7ede73c85a3e5815b061f9b520e999cd4b5efd52 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 21 Oct 2024 11:55:32 +0200 Subject: [PATCH 026/258] Complete automation of version replacement pre-commit for pip and uv (#43205) The scripts to update pip and uv version were not complete - they did not replace a few of our scripts and documentation. This was especially troublesome for doc replacement, because updating versions manually led to misalignments of tables in markdown. Lack of completeness of the upgrade caused #43197 and #43135 manual PRs to bump all references. Also an earlier upgrade caused the markdown table to be broken - with UV row table offset by 1. This PR fixes it: * all the scripts and docs are updated now * when markdown is updated, the table structure is not broken --- dev/breeze/doc/ci/02_images.md | 63 ++++++++-------- scripts/ci/pre_commit/update_installers.py | 86 ++++++++++++++++++++-- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md index fb17928a97f0a..7565cc41c9ee0 100644 --- a/dev/breeze/doc/ci/02_images.md +++ b/dev/breeze/doc/ci/02_images.md @@ -421,36 +421,39 @@ DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \ The following build arguments (`--build-arg` in docker build command) can be used for CI images: -| Build argument | Default value | Description | -|-----------------------------------|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| -| `PYTHON_BASE_IMAGE` | `python:3.9-slim-bookworm` | Base Python image | -| `PYTHON_MAJOR_MINOR_VERSION` | `3.9` | major/minor version of Python (should match base image) | -| `DEPENDENCIES_EPOCH_NUMBER` | `2` | increasing this number will reinstall all apt dependencies | -| `ADDITIONAL_PIP_INSTALL_FLAGS` | | additional `pip` flags passed to the installation commands (except when reinstalling `pip` itself) | -| `PIP_NO_CACHE_DIR` | `true` | if true, then no pip cache will be stored | -| `UV_NO_CACHE` | `true` | if true, then no uv cache will be stored | -| `HOME` | `/root` | Home directory of the root user (CI image has root user as default) | -| `AIRFLOW_HOME` | `/root/airflow` | Airflow's HOME (that's where logs and sqlite databases are stored) | -| `AIRFLOW_SOURCES` | `/opt/airflow` | Mounted sources of Airflow | -| `AIRFLOW_REPO` | `apache/airflow` | the repository from which PIP dependencies are pre-installed | -| `AIRFLOW_BRANCH` | `main` | the branch from which PIP dependencies are pre-installed | -| `AIRFLOW_CI_BUILD_EPOCH` | `1` | increasing this value will reinstall PIP dependencies from the repository from scratch | -| `AIRFLOW_CONSTRAINTS_LOCATION` | | If not empty, it will override the source of the constraints with the specified URL or file. | -| `AIRFLOW_CONSTRAINTS_REFERENCE` | | reference (branch or tag) from GitHub repository from which constraints are used. By default it is set to `constraints-main` but can be `constraints-2-X`. | -| `AIRFLOW_EXTRAS` | `all` | extras to install | -| `UPGRADE_INVALIDATION_STRING` | | If set to any random value the dependencies are upgraded to newer versions. In CI it is set to build id. | -| `AIRFLOW_PRE_CACHED_PIP_PACKAGES` | `true` | Allows to pre-cache airflow PIP packages from the GitHub of Apache Airflow This allows to optimize iterations for Image builds and speeds up CI jobs. | -| `ADDITIONAL_AIRFLOW_EXTRAS` | | additional extras to install | -| `ADDITIONAL_PYTHON_DEPS` | | additional Python dependencies to install | -| `DEV_APT_COMMAND` | | Dev apt command executed before dev deps are installed in the first part of image | -| `ADDITIONAL_DEV_APT_COMMAND` | | Additional Dev apt command executed before dev dep are installed in the first part of the image | -| `DEV_APT_DEPS` | Empty - install default dependencies (see `install_os_dependencies.sh`) | Dev APT dependencies installed in the first part of the image | -| `ADDITIONAL_DEV_APT_DEPS` | | Additional apt dev dependencies installed in the first part of the image | -| `ADDITIONAL_DEV_APT_ENV` | | Additional env variables defined when installing dev deps | -| `AIRFLOW_PIP_VERSION` | `24.2` | PIP version used. | -| `AIRFLOW_UV_VERSION` | `0.4.24` | UV version used. | -| `AIRFLOW_USE_UV` | `true` | Whether to use UV for installation. | -| `PIP_PROGRESS_BAR` | `on` | Progress bar for PIP installation | +| Build argument | Default value | Description | +|-----------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `PYTHON_BASE_IMAGE` | `python:3.9-slim-bookworm` | Base Python image | +| `PYTHON_MAJOR_MINOR_VERSION` | `3.9` | major/minor version of Python (should match base image) | +| `DEPENDENCIES_EPOCH_NUMBER` | `2` | increasing this number will reinstall all apt dependencies | +| `ADDITIONAL_PIP_INSTALL_FLAGS` | | additional `pip` flags passed to the installation commands (except when reinstalling `pip` itself) | +| `PIP_NO_CACHE_DIR` | `true` | if true, then no pip cache will be stored | +| `UV_NO_CACHE` | `true` | if true, then no uv cache will be stored | +| `HOME` | `/root` | Home directory of the root user (CI image has root user as default) | +| `AIRFLOW_HOME` | `/root/airflow` | Airflow's HOME (that's where logs and sqlite databases are stored) | +| `AIRFLOW_SOURCES` | `/opt/airflow` | Mounted sources of Airflow | +| `AIRFLOW_REPO` | `apache/airflow` | the repository from which PIP dependencies are pre-installed | +| `AIRFLOW_BRANCH` | `main` | the branch from which PIP dependencies are pre-installed | +| `AIRFLOW_CI_BUILD_EPOCH` | `1` | increasing this value will reinstall PIP dependencies from the repository from scratch | +| `AIRFLOW_CONSTRAINTS_LOCATION` | | If not empty, it will override the source of the constraints with the specified URL or file. | +| `AIRFLOW_CONSTRAINTS_REFERENCE` | | reference (branch or tag) from GitHub repository from which constraints are used. By default it is set to `constraints-main` but can be `constraints-2-X`. | +| `AIRFLOW_EXTRAS` | `all` | extras to install | +| `UPGRADE_INVALIDATION_STRING` | | If set to any random value the dependencies are upgraded to newer versions. In CI it is set to build id. | +| `AIRFLOW_PRE_CACHED_PIP_PACKAGES` | `true` | Allows to pre-cache airflow PIP packages from the GitHub of Apache Airflow This allows to optimize iterations for Image builds and speeds up CI jobs. | +| `ADDITIONAL_AIRFLOW_EXTRAS` | | additional extras to install | +| `ADDITIONAL_PYTHON_DEPS` | | additional Python dependencies to install | +| `DEV_APT_COMMAND` | | Dev apt command executed before dev deps are installed in the first part of image | +| `ADDITIONAL_DEV_APT_COMMAND` | | Additional Dev apt command executed before dev dep are installed in the first part of the image | +| `DEV_APT_DEPS` | | Dev APT dependencies installed in the first part of the image (default empty means default dependencies are used) | +| `ADDITIONAL_DEV_APT_DEPS` | | Additional apt dev dependencies installed in the first part of the image | +| `ADDITIONAL_DEV_APT_ENV` | | Additional env variables defined when installing dev deps | +| `AIRFLOW_PIP_VERSION` | `24.2` | PIP version used. | +| `AIRFLOW_UV_VERSION` | `0.4.24` | UV version used. | +| `AIRFLOW_USE_UV` | `true` | Whether to use UV for installation. | +| `PIP_PROGRESS_BAR` | `on` | Progress bar for PIP installation | + + +The" Here are some examples of how CI images can built manually. CI is always built from local sources. diff --git a/scripts/ci/pre_commit/update_installers.py b/scripts/ci/pre_commit/update_installers.py index 1cbd38c8333a2..a90e07d38c9f3 100755 --- a/scripts/ci/pre_commit/update_installers.py +++ b/scripts/ci/pre_commit/update_installers.py @@ -30,8 +30,22 @@ FILES_TO_UPDATE = [ AIRFLOW_SOURCES_ROOT_PATH / "Dockerfile", AIRFLOW_SOURCES_ROOT_PATH / "Dockerfile.ci", + AIRFLOW_SOURCES_ROOT_PATH / "scripts" / "ci" / "install_breeze.sh", AIRFLOW_SOURCES_ROOT_PATH / "scripts" / "docker" / "common.sh", AIRFLOW_SOURCES_ROOT_PATH / "pyproject.toml", + AIRFLOW_SOURCES_ROOT_PATH / "dev" / "breeze" / "src" / "airflow_breeze" / "global_constants.py", + AIRFLOW_SOURCES_ROOT_PATH + / "dev" + / "breeze" + / "src" + / "airflow_breeze" + / "commands" + / "release_management_commands.py", +] + + +DOC_FILES_TO_UPDATE: list[Path] = [ + AIRFLOW_SOURCES_ROOT_PATH / "dev/" / "breeze" / "doc" / "ci" / "02_images.md" ] @@ -43,13 +57,39 @@ def get_latest_pypi_version(package_name: str) -> str: return latest_version -PIP_PATTERN = re.compile(r"AIRFLOW_PIP_VERSION=[0-9.]+") -UV_PATTERN = re.compile(r"AIRFLOW_UV_VERSION=[0-9.]+") -UV_GREATER_PATTERN = re.compile(r'"uv>=[0-9]+[0-9.]+"') +AIRFLOW_PIP_PATTERN = re.compile(r"(AIRFLOW_PIP_VERSION=)([0-9.]+)") +AIRFLOW_PIP_QUOTED_PATTERN = re.compile(r"(AIRFLOW_PIP_VERSION = )(\"[0-9.]+\")") +PIP_QUOTED_PATTERN = re.compile(r"(PIP_VERSION = )(\"[0-9.]+\")") +AIRFLOW_PIP_DOC_PATTERN = re.compile(r"(\| *`AIRFLOW_PIP_VERSION` *\| *)(`[0-9.]+`)( *\|)") +AIRFLOW_PIP_UPGRADE_PATTERN = re.compile(r"(python -m pip install --upgrade pip==)([0-9.]+)") + +AIRFLOW_UV_PATTERN = re.compile(r"(AIRFLOW_UV_VERSION=)([0-9.]+)") +AIRFLOW_UV_QUOTED_PATTERN = re.compile(r"(AIRFLOW_UV_VERSION = )(\"[0-9.]+\")") +AIRFLOW_UV_DOC_PATTERN = re.compile(r"(\| *`AIRFLOW_UV_VERSION` *\| *)(`[0-9.]+`)( *\|)") +UV_GREATER_PATTERN = re.compile(r'"(uv>=)([0-9]+)"') UPGRADE_UV: bool = os.environ.get("UPGRADE_UV", "true").lower() == "true" UPGRADE_PIP: bool = os.environ.get("UPGRADE_PIP", "true").lower() == "true" + +def replace_group_2_while_keeping_total_length(pattern: re.Pattern[str], replacement: str, text: str) -> str: + def replacer(match): + original_length = len(match.group(2)) + padding = "" + if len(match.groups()) > 2: + padding = match.group(3) + new_length = len(replacement) + diff = new_length - original_length + if diff <= 0: + padding = " " * -diff + padding + else: + padding = padding[diff:] + padded_replacement = match.group(1) + replacement + padding + return padded_replacement.strip() + + return re.sub(pattern, replacer, text) + + if __name__ == "__main__": pip_version = get_latest_pypi_version("pip") console.print(f"[bright_blue]Latest pip version: {pip_version}") @@ -62,10 +102,44 @@ def get_latest_pypi_version(package_name: str) -> str: file_content = file.read_text() new_content = file_content if UPGRADE_PIP: - new_content = re.sub(PIP_PATTERN, f"AIRFLOW_PIP_VERSION={pip_version}", new_content, re.MULTILINE) + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_PIP_PATTERN, pip_version, new_content + ) + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_PIP_UPGRADE_PATTERN, pip_version, new_content + ) + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_PIP_QUOTED_PATTERN, f'"{pip_version}"', new_content + ) + new_content = replace_group_2_while_keeping_total_length( + PIP_QUOTED_PATTERN, f'"{pip_version}"', new_content + ) + if UPGRADE_UV: + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_UV_PATTERN, uv_version, new_content + ) + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_UV_QUOTED_PATTERN, f'"{uv_version}"', new_content + ) + new_content = replace_group_2_while_keeping_total_length( + UV_GREATER_PATTERN, uv_version, new_content + ) + if new_content != file_content: + file.write_text(new_content) + console.print(f"[bright_blue]Updated {file}") + changed = True + for file in DOC_FILES_TO_UPDATE: + console.print(f"[bright_blue]Updating {file}") + file_content = file.read_text() + new_content = file_content + if UPGRADE_PIP: + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_PIP_DOC_PATTERN, f"`{pip_version}`", new_content + ) if UPGRADE_UV: - new_content = re.sub(UV_PATTERN, f"AIRFLOW_UV_VERSION={uv_version}", new_content, re.MULTILINE) - new_content = re.sub(UV_GREATER_PATTERN, f'"uv>={uv_version}"', new_content, re.MULTILINE) + new_content = replace_group_2_while_keeping_total_length( + AIRFLOW_UV_DOC_PATTERN, f"`{uv_version}`", new_content + ) if new_content != file_content: file.write_text(new_content) console.print(f"[bright_blue]Updated {file}") From a7b1aa48334a3308623034acd693167e0cd6e227 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 21 Oct 2024 11:56:46 +0200 Subject: [PATCH 027/258] Make sure all GitHub activity is archived (#43208) While following a discussion at board@a.o I realized that not all GitHub activity of ours is archived - we use the commits@a.a.o to archive all the commits automatically, and this list is not supposed to be subscribed to in general as it is already far too busy for that, however we can also forward all issues, prs and discussions there, which will make us follow the requirement of the ASF that all the activity in the project should be archived in foundation-managed archives. It also has the nice benefit that there is single place (ponymail commits@a.a.o list) that has great search capabilities and is really nice as a single place where we will be able to find all the activity - regardles if they were in commits, prs, issues or discussions. --- .asf.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.asf.yaml b/.asf.yaml index 7c6efaf928f85..50dea7951556e 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -134,3 +134,7 @@ github: notifications: jobs: jobs@airflow.apache.org + commits: commits@airflow.apache.org + issues: commits@airflow.apache.org + pullrequests: commits@airflow.apache.org + discussions: commits@airflow.apache.org From dc4def7c87441554ff106e65ec2b7894cdba7b0e Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 21 Oct 2024 18:44:13 +0800 Subject: [PATCH 028/258] Rename Dataset database tables as Asset (#42023) * feat(models/asset): rename index idx_dataset_alias_dataset_event_alias_id as idx_asset_alias_asset_event_alias_id * feat(models/asset): rename index idx_dataset_alias_dataset_event_event_id as idx_asset_alias_asset_event_event_id * feat(models/asset): rename table dataset_alias_dataset_event as asset_alias_asset_event * feat(models): rename dataset_alias as asset_alias * rename table "dataset_alias" as "asset_alias" * rename table "dataset_alias_dataset" as "asset_alias_dataset" * rename index "idx_dataset_alias_dataset_alias_id" as "idx_asset_alias_asset_alias_id" * rename index "idx_dataset_alias_dataset_alias_dataset_id" as "idx_asset_alias_asset_dataset_id" * rename fk constraint "ds_dsa_alias_id" as "a_aa_alias_id"" * change reference column from "dataset_alias.id" to "asset_alias.id" * rename table "dag_schedule_dataset_alias_reference" as "dag_schedule_asset_alias_reference" * rename column "dataset_alias" as "asset_alias" * rename fk constraint "dsdar_dataset_alias_fkey" as "dsaar_asset_alias_fkey" * change reference column from "dataset_alias.id" to "asset_alias.id" * rename index "idx_dag_schedule_dataset_alias_reference_dag_id" as "idx_dag_schedule_asset_alias_reference_dag_id" * in table "asset_alias_asset_event" * change fk alias_id reference from "dataset_alias.id" to "asset_alias.id" * in fk constraint "dss_de_alias_id" * change reference column from "dataset_alias.id" to "asset_alias.id" * feat(models/asset): rename table "dag_schedule_dataset_reference" as "dag_schedule_asset_reference" * rename fk constraint "dsdr_dataset_fkey" as "dsar_dataset_fkey" * rename fk constraint "dsdr_dag_id_fkey" as "dsar_dag_id_fkey" * rename index "idx_dag_schedule_dataset_reference_dag_id" as "idx_dag_schedule_asset_reference_dag_id" * feat(models): rename table "task_outlet_dataset_references" as "task_outlet_asset_references" * rename fk constraint "todr_dataset_fkey" as "todr_asset_fkey" * rename index "idx_task_outlet_dataset_reference_dag_id" as "todr_dataset_fkey" * feat(models/asset): rename table "dagrun_dataset_event" as "dagrun_asset_event" * rename index "idx_dagrun_dataset_events_dag_run_id" as "idx_dagrun_asset_events_dag_run_id" * rename index "idx_dagrun_dataset_events_event_id" as "idx_dagrun_asset_events_event_id" * feat(models/dag): rename column "schedule_dataset_references" as "schedule_asset_references" in table "dag" * feat(models/asset): rename table "dataset_dag_run_queue" as "asset_dag_run_queue" * rename index "idx_dataset_dag_run_queue_target_dag_id" as "idx_asset_dag_run_queue_target_dag_id" * feat(models/dag): rename consumed_dataset_events as consumed_asset_events * feat(models/asset): rename table "dataset_event" as "asset_event" * feat(models/asset): rename index idx_dataset_id_timestamp as idx_asset_id_timestamp * feat(models/asset): rename idx_asset_alias_asset_dataset_id as idx_asset_alias_asset_asset_id * feat(models/asset): rename dataset as asset in fks * feat(models/asset): rename asset_alias_dataset as asset_alias_asset * feat(models/asset): rename index idx_dataset_alias_name_unique as idx_asset_alias_name_unique * feat(models/asset): rename idx_dataset_name_uri_unique as idx_asset_name_uri_unique * feat(models/asset): rename dsar_dataset_fkey as dsar_asset_fkey * feat(models/asset): rename datasetdagrunqueue_pkey as assetdagrunqueue_pkey * feat(models/asset): rename ddrq_dataset_fkey as ddrq_asset_fkey * feat(models/asset): rename ddrq fks as adrq fks * feat(models/asset): rename dsdr keys as dsar keys * feat(models/asset): rename todr keys as toar keys * feat(migrations): initial migration files * feat(migrations): add utility functions _rename_index and _rename_fk_constraint * feat(migrations): add _rename_pk_constraint utility function * feat(migrations): update migration files to reflect current model change (except for fk reference table update) * feat: rename dataset_id as asset_id * feat(api_connexion): rename dataset_id as asset_id * feat: rename dataset.uri as asset.uri * feat: rename dataset_expression as asset_expression * feat(listeners): rename argument dataset_alias as asset_alias * feat(utils/types): rename DagRunType.DATASET_TRIGGERED as DagRunType.ASSET_TRIGGERED * feat: rename dataset_triggered_dag_info as asset_triggered_dag_info * feat: rename dataset as asset * feat(models/asset): rename dss_de as aa_ae * feat(models/asset): rename dsdar as dsaar * feat(migrations): wrap up the upgrade part (except for fk upgrade) * feat(migrations): rename migration file * docs(newsfragment): add place holder newsfragment * build: generate migration related files * feat(migrations): wrap up upgrade script * feat(migrations): wrap up downgrade script * fix: fix missing frontend change * fix(migrations): fix fk_constraint rename for association tables * fix: fix missing dataset related changes due to rebasing * feat(migrations): reorder migrations * feat(migrations): fix error for sqlite * feat(models/asset): remove redundant fk constraint aa_ae_alias_id and aa_ae_event_id * feat(migrations): remove redundant fk constraint a_aa_asset_id and a_aa_alias_id * build: generate db migration side files * feat(migrations): add postgresql support * feat(migrations): add mysql support for downgrade * fix: rename reference_table as referent_table * refactor: reorganize 0038 migration * docs: improve endpoint asset_triggered change description * docs: add description to endpoint renaming and note it in newsfragments --- .../api_connexion/endpoints/asset_endpoint.py | 10 +- .../endpoints/dag_run_endpoint.py | 2 +- airflow/api_connexion/openapi/v1.yaml | 76 +- airflow/api_connexion/schemas/asset_schema.py | 4 +- airflow/api_connexion/schemas/dag_schema.py | 2 +- .../core_api/openapi/v1-generated.yaml | 14 +- .../api_fastapi/core_api/routes/ui/assets.py | 8 +- .../api_fastapi/core_api/serializers/dags.py | 2 +- .../core_api/serializers/dashboard.py | 2 +- airflow/assets/__init__.py | 2 +- airflow/assets/manager.py | 8 +- airflow/dag_processing/collection.py | 28 +- airflow/jobs/scheduler_job_runner.py | 28 +- airflow/listeners/spec/asset.py | 4 +- .../0040_3_0_0_rename_dataset_as_asset.py | 693 ++++ airflow/models/asset.py | 194 +- airflow/models/dag.py | 50 +- airflow/models/dagrun.py | 2 +- airflow/models/taskinstance.py | 26 +- airflow/serialization/pydantic/asset.py | 8 +- airflow/serialization/pydantic/dag_run.py | 2 +- airflow/serialization/serialized_objects.py | 6 +- airflow/timetables/assets.py | 2 +- .../ui/openapi-gen/requests/schemas.gen.ts | 14 +- airflow/ui/openapi-gen/requests/types.gen.ts | 6 +- airflow/utils/context.py | 2 +- airflow/utils/db.py | 2 +- airflow/utils/db_cleanup.py | 2 +- airflow/utils/types.py | 4 +- airflow/www/jest-setup.js | 2 +- .../static/js/cluster-activity/index.test.tsx | 2 +- .../static/js/components/DatasetEventCard.tsx | 10 +- .../www/static/js/components/RunTypeIcon.tsx | 2 +- airflow/www/static/js/dag/details/Header.tsx | 2 +- airflow/www/static/js/dag/details/dag/Dag.tsx | 4 +- .../details/dagRun/DatasetTriggerEvents.tsx | 2 +- .../static/js/dag/details/dagRun/index.tsx | 4 +- .../www/static/js/dag/details/graph/index.tsx | 30 +- .../www/static/js/dag/details/graph/utils.ts | 2 +- .../taskInstance/DatasetUpdateEvents.tsx | 2 +- airflow/www/static/js/datasetUtils.js | 6 +- .../www/static/js/datasets/AssetEvents.tsx | 2 +- airflow/www/static/js/types/api-generated.ts | 165 +- airflow/www/static/js/types/index.ts | 2 +- ...n_modal.html => asset_next_run_modal.html} | 2 +- .../airflow/{datasets.html => assets.html} | 0 airflow/www/templates/airflow/dag.html | 29 +- airflow/www/templates/airflow/dags.html | 8 +- airflow/www/views.py | 62 +- docs/apache-airflow/img/airflow_erd.sha256 | 2 +- docs/apache-airflow/img/airflow_erd.svg | 3404 ++++++++--------- docs/apache-airflow/migrations-ref.rst | 4 +- newsfragments/42023.significant.rst | 9 + .../api_endpoints/test_asset_endpoint.py | 56 +- .../api_endpoints/test_dag_endpoint.py | 4 +- .../endpoints/test_asset_endpoint.py | 46 +- .../endpoints/test_dag_endpoint.py | 20 +- .../endpoints/test_dag_parsing.py | 2 +- .../endpoints/test_dag_run_endpoint.py | 10 +- .../schemas/test_dataset_schema.py | 30 +- .../core_api/routes/public/test_dag_run.py | 2 +- .../core_api/routes/public/test_dags.py | 2 +- .../core_api/routes/ui/test_assets.py | 2 +- .../core_api/routes/ui/test_dashboard.py | 6 +- tests/assets/test_asset.py | 6 +- tests/assets/test_manager.py | 6 +- tests/dags/test_assets.py | 12 +- tests/jobs/test_scheduler_job.py | 22 +- tests/models/test_dag.py | 46 +- tests/models/test_dagrun.py | 6 +- tests/models/test_taskinstance.py | 62 +- tests/serialization/test_pydantic_models.py | 40 +- tests/serialization/test_serde.py | 14 +- tests/timetables/test_assets_timetable.py | 6 +- tests/utils/test_db_cleanup.py | 14 +- .../www/views/test_views_cluster_activity.py | 6 +- tests/www/views/test_views_dataset.py | 104 +- tests/www/views/test_views_grid.py | 34 +- tests/www/views/test_views_trigger_dag.py | 2 +- 79 files changed, 3176 insertions(+), 2344 deletions(-) create mode 100644 airflow/migrations/versions/0040_3_0_0_rename_dataset_as_asset.py rename airflow/www/templates/airflow/{dataset_next_run_modal.html => asset_next_run_modal.html} (97%) rename airflow/www/templates/airflow/{datasets.html => assets.html} (100%) create mode 100644 newsfragments/42023.significant.rst diff --git a/airflow/api_connexion/endpoints/asset_endpoint.py b/airflow/api_connexion/endpoints/asset_endpoint.py index cbbe542ea7987..1ea1db2b3bbb8 100644 --- a/airflow/api_connexion/endpoints/asset_endpoint.py +++ b/airflow/api_connexion/endpoints/asset_endpoint.py @@ -133,7 +133,7 @@ def get_asset_events( query = select(AssetEvent) if asset_id: - query = query.where(AssetEvent.dataset_id == asset_id) + query = query.where(AssetEvent.asset_id == asset_id) if source_dag_id: query = query.where(AssetEvent.source_dag_id == source_dag_id) if source_task_id: @@ -166,7 +166,7 @@ def _generate_queued_event_where_clause( where_clause.append(AssetDagRunQueue.target_dag_id == dag_id) if uri is not None: where_clause.append( - AssetDagRunQueue.dataset_id.in_( + AssetDagRunQueue.asset_id.in_( select(AssetModel.id).where(AssetModel.uri == uri), ), ) @@ -187,7 +187,7 @@ def get_dag_asset_queued_event( where_clause = _generate_queued_event_where_clause(dag_id=dag_id, uri=uri, before=before) adrq = session.scalar( select(AssetDagRunQueue) - .join(AssetModel, AssetDagRunQueue.dataset_id == AssetModel.id) + .join(AssetModel, AssetDagRunQueue.asset_id == AssetModel.id) .where(*where_clause) ) if adrq is None: @@ -228,7 +228,7 @@ def get_dag_asset_queued_events( where_clause = _generate_queued_event_where_clause(dag_id=dag_id, before=before) query = ( select(AssetDagRunQueue, AssetModel.uri) - .join(AssetModel, AssetDagRunQueue.dataset_id == AssetModel.id) + .join(AssetModel, AssetDagRunQueue.asset_id == AssetModel.id) .where(*where_clause) ) result = session.execute(query).all() @@ -278,7 +278,7 @@ def get_asset_queued_events( ) query = ( select(AssetDagRunQueue, AssetModel.uri) - .join(AssetModel, AssetDagRunQueue.dataset_id == AssetModel.id) + .join(AssetModel, AssetDagRunQueue.asset_id == AssetModel.id) .where(*where_clause) ) total_entries = get_query_count(query, session=session) diff --git a/airflow/api_connexion/endpoints/dag_run_endpoint.py b/airflow/api_connexion/endpoints/dag_run_endpoint.py index 74eae13ddd4d0..e0d83575611b0 100644 --- a/airflow/api_connexion/endpoints/dag_run_endpoint.py +++ b/airflow/api_connexion/endpoints/dag_run_endpoint.py @@ -130,7 +130,7 @@ def get_upstream_asset_events(*, dag_id: str, dag_run_id: str, session: Session "DAGRun not found", detail=f"DAGRun with DAG ID: '{dag_id}' and DagRun ID: '{dag_run_id}' not found", ) - events = dag_run.consumed_dataset_events + events = dag_run.consumed_asset_events return asset_event_collection_schema.dump( AssetEventCollection(asset_events=events, total_entries=len(events)) ) diff --git a/airflow/api_connexion/openapi/v1.yaml b/airflow/api_connexion/openapi/v1.yaml index e99f91639c49e..af38326489a4a 100644 --- a/airflow/api_connexion/openapi/v1.yaml +++ b/airflow/api_connexion/openapi/v1.yaml @@ -1146,6 +1146,7 @@ paths: Get asset for a dag run. *New in version 2.4.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamDatasetEvents" x-openapi-router-controller: airflow.api_connexion.endpoints.dag_run_endpoint operationId: get_upstream_asset_events tags: [DAGRun, Asset] @@ -1211,6 +1212,7 @@ paths: Get a queued asset event for a DAG. *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_dag_asset_queued_event parameters: @@ -1236,6 +1238,7 @@ paths: Delete a queued Asset event for a DAG. *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: delete_dag_asset_queued_event parameters: @@ -1263,6 +1266,7 @@ paths: Get queued Asset events for a DAG. *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_dag_asset_queued_events parameters: @@ -1288,6 +1292,7 @@ paths: Delete queued Asset events for a DAG. *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: delete_dag_asset_queued_events parameters: @@ -1336,6 +1341,7 @@ paths: Get queued Asset events for an Asset *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_asset_queued_events parameters: @@ -1361,6 +1367,7 @@ paths: Delete queued Asset events for a Asset. *New in version 2.9.0* + *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: delete_asset_queued_events parameters: @@ -2480,6 +2487,8 @@ paths: x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_assets tags: [Asset] + description: | + *Changed in 3.0.0*: The endpoint value was renamed from "/datasets" parameters: - $ref: "#/components/parameters/PageLimit" - $ref: "#/components/parameters/PageOffset" @@ -2517,7 +2526,10 @@ paths: - $ref: "#/components/parameters/AssetURI" get: summary: Get an asset - description: Get an asset by uri. + description: | + Get an asset by uri. + + *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/{uri}" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_asset tags: [Asset] @@ -2538,7 +2550,10 @@ paths: /assets/events: get: summary: Get asset events - description: Get asset events + description: | + Get asset events + + *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: get_asset_events tags: [Asset] @@ -2566,7 +2581,10 @@ paths: $ref: "#/components/responses/NotFound" post: summary: Create asset event - description: Create asset event + description: | + Create asset event + + *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events" x-openapi-router-controller: airflow.api_connexion.endpoints.asset_endpoint operationId: create_asset_event tags: [Asset] @@ -3299,7 +3317,9 @@ components: - backfill - manual - scheduled - - dataset_triggered + - asset_triggered + description: | + *Changed in 3.0.0*: The asset_triggered value was renamed from dataset_triggered. state: $ref: "#/components/schemas/DagState" external_trigger: @@ -4088,9 +4108,12 @@ components: dag_run_timeout: $ref: "#/components/schemas/TimeDelta" nullable: true - dataset_expression: + asset_expression: type: object - description: Nested asset any/all conditions + description: | + Nested asset any/all conditions + + *Changed in 3.0.0*: The asset_expression value was renamed from dataset_expression. nullable: true doc_md: type: string @@ -4475,6 +4498,7 @@ components: An asset item. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from Dataset. type: object properties: id: @@ -4510,6 +4534,7 @@ components: An asset reference to an upstream task. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from TaskOutletDatasetReference. type: object properties: dag_id: @@ -4534,6 +4559,7 @@ components: An asset reference to a downstream DAG. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from DagScheduleDatasetReference. type: object properties: dag_id: @@ -4554,6 +4580,7 @@ components: A collection of assets. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from DatasetCollection. type: object allOf: - type: object @@ -4562,6 +4589,8 @@ components: type: array items: $ref: "#/components/schemas/Asset" + description: | + *Changed in 3.0.0*: This was renamed from datasets. - $ref: "#/components/schemas/CollectionInfo" AssetEvent: @@ -4569,14 +4598,21 @@ components: An asset event. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from DatasetEvent. type: object properties: - dataset_id: + asset_id: type: integer - description: The asset id - dataset_uri: + description: | + The asset id + + *Changed in 3.0.0*: This was renamed from dataset_id. + asset_uri: type: string - description: The URI of the asset + description: | + The URI of the asset + + *Changed in 3.0.0*: This was renamed from dataset_uri. nullable: false extra: type: object @@ -4611,10 +4647,15 @@ components: type: object required: - asset_uri + description: | + *Changed in 3.0.0*: This was renamed from CreateDatasetEvent. properties: asset_uri: type: string - description: The URI of the asset + description: | + The URI of the asset + + *Changed in 3.0.0*: This was renamed from dataset_uri. nullable: false extra: type: object @@ -4705,12 +4746,15 @@ components: A collection of asset events. *New in version 2.4.0* + *Changed in 3.0.0*: This was renamed from DatasetEventCollection. type: object allOf: - type: object properties: asset_events: type: array + description: | + *Changed in 3.0.0*: This was renamed from dataset_events. items: $ref: "#/components/schemas/AssetEvent" - $ref: "#/components/schemas/CollectionInfo" @@ -5515,7 +5559,10 @@ components: type: string format: path required: true - description: The encoded Asset URI + description: | + The encoded Asset URI + + *Changed in 3.0.0*: This was renamed from DatasetURI. PoolName: in: path @@ -5701,7 +5748,10 @@ components: name: asset_id schema: type: integer - description: The Asset ID that updated the asset. + description: | + The Asset ID that updated the asset. + + *Changed in 3.0.0*: This was renamed from FilterDatasetID. FilterSourceDAGID: in: query diff --git a/airflow/api_connexion/schemas/asset_schema.py b/airflow/api_connexion/schemas/asset_schema.py index 662f73a50d8b9..7f84b799d1a77 100644 --- a/airflow/api_connexion/schemas/asset_schema.py +++ b/airflow/api_connexion/schemas/asset_schema.py @@ -136,8 +136,8 @@ class Meta: model = AssetEvent id = auto_field() - dataset_id = auto_field() - dataset_uri = fields.String(attribute="dataset.uri", dump_only=True) + asset_id = auto_field() + asset_uri = fields.String(attribute="asset.uri", dump_only=True) extra = JsonObjectField() source_task_id = auto_field() source_dag_id = auto_field() diff --git a/airflow/api_connexion/schemas/dag_schema.py b/airflow/api_connexion/schemas/dag_schema.py index 6c7ff6fdc30e0..3ca650ac9a5e9 100644 --- a/airflow/api_connexion/schemas/dag_schema.py +++ b/airflow/api_connexion/schemas/dag_schema.py @@ -98,7 +98,7 @@ class DAGDetailSchema(DAGSchema): catchup = fields.Boolean(dump_only=True) orientation = fields.String(dump_only=True) max_active_tasks = fields.Integer(dump_only=True) - dataset_expression = fields.Dict(allow_none=True) + asset_expression = fields.Dict(allow_none=True) start_date = fields.DateTime(dump_only=True) dag_run_timeout = fields.Nested(TimeDeltaSchema, attribute="dagrun_timeout", dump_only=True) doc_md = fields.String(dump_only=True) diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 325a6354de2b2..ebe209334d5ce 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1377,11 +1377,11 @@ components: format: duration - type: 'null' title: Dag Run Timeout - dataset_expression: + asset_expression: anyOf: - type: object - type: 'null' - title: Dataset Expression + title: Asset Expression doc_md: anyOf: - type: string @@ -1472,7 +1472,7 @@ components: - owners - catchup - dag_run_timeout - - dataset_expression + - asset_expression - doc_md - start_date - end_date @@ -1764,15 +1764,15 @@ components: manual: type: integer title: Manual - dataset_triggered: + asset_triggered: type: integer - title: Dataset Triggered + title: Asset Triggered type: object required: - backfill - scheduled - manual - - dataset_triggered + - asset_triggered title: DAGRunTypes description: DAG Run Types for responses. DAGTagCollectionResponse: @@ -1844,7 +1844,7 @@ components: - backfill - scheduled - manual - - dataset_triggered + - asset_triggered title: DagRunType description: Class with DagRun types. DagTagPydantic: diff --git a/airflow/api_fastapi/core_api/routes/ui/assets.py b/airflow/api_fastapi/core_api/routes/ui/assets.py index b8a17c7398424..6786bc30ae680 100644 --- a/airflow/api_fastapi/core_api/routes/ui/assets.py +++ b/airflow/api_fastapi/core_api/routes/ui/assets.py @@ -56,11 +56,11 @@ async def next_run_assets( AssetModel.uri, func.max(AssetEvent.timestamp).label("lastUpdate"), ) - .join(DagScheduleAssetReference, DagScheduleAssetReference.dataset_id == AssetModel.id) + .join(DagScheduleAssetReference, DagScheduleAssetReference.asset_id == AssetModel.id) .join( AssetDagRunQueue, and_( - AssetDagRunQueue.dataset_id == AssetModel.id, + AssetDagRunQueue.asset_id == AssetModel.id, AssetDagRunQueue.target_dag_id == DagScheduleAssetReference.dag_id, ), isouter=True, @@ -68,7 +68,7 @@ async def next_run_assets( .join( AssetEvent, and_( - AssetEvent.dataset_id == AssetModel.id, + AssetEvent.asset_id == AssetModel.id, ( AssetEvent.timestamp >= latest_run.execution_date if latest_run and latest_run.execution_date @@ -82,5 +82,5 @@ async def next_run_assets( .order_by(AssetModel.uri) ) ] - data = {"dataset_expression": dag_model.dataset_expression, "events": events} + data = {"asset_expression": dag_model.asset_expression, "events": events} return data diff --git a/airflow/api_fastapi/core_api/serializers/dags.py b/airflow/api_fastapi/core_api/serializers/dags.py index 39e85ea8c6f0e..a3e11de36039b 100644 --- a/airflow/api_fastapi/core_api/serializers/dags.py +++ b/airflow/api_fastapi/core_api/serializers/dags.py @@ -112,7 +112,7 @@ class DAGDetailsResponse(DAGResponse): catchup: bool dag_run_timeout: timedelta | None - dataset_expression: dict | None + asset_expression: dict | None doc_md: str | None start_date: datetime | None end_date: datetime | None diff --git a/airflow/api_fastapi/core_api/serializers/dashboard.py b/airflow/api_fastapi/core_api/serializers/dashboard.py index ee31a812945ec..66adc8ed3df92 100644 --- a/airflow/api_fastapi/core_api/serializers/dashboard.py +++ b/airflow/api_fastapi/core_api/serializers/dashboard.py @@ -25,7 +25,7 @@ class DAGRunTypes(BaseModel): backfill: int scheduled: int manual: int - dataset_triggered: int + asset_triggered: int class DAGRunStates(BaseModel): diff --git a/airflow/assets/__init__.py b/airflow/assets/__init__.py index 4dbc35fb95397..dcc5484656eb7 100644 --- a/airflow/assets/__init__.py +++ b/airflow/assets/__init__.py @@ -169,7 +169,7 @@ def expand_alias_to_assets(alias: str | AssetAlias, *, session: Session = NEW_SE select(AssetAliasModel).where(AssetAliasModel.name == alias_name).limit(1) ) if asset_alias_obj: - return [asset.to_public() for asset in asset_alias_obj.datasets] + return [asset.to_public() for asset in asset_alias_obj.assets] return [] diff --git a/airflow/assets/manager.py b/airflow/assets/manager.py index cd4d72e633a8e..a06c7c31786f5 100644 --- a/airflow/assets/manager.py +++ b/airflow/assets/manager.py @@ -138,7 +138,7 @@ def register_asset_change( cls._add_asset_alias_association({alias.name for alias in aliases}, asset_model, session=session) event_kwargs = { - "dataset_id": asset_model.id, + "asset_id": asset_model.id, "extra": extra, } if task_instance: @@ -167,7 +167,7 @@ def register_asset_change( ).unique() for asset_alias_model in asset_alias_models: - asset_alias_model.dataset_events.append(asset_event) + asset_alias_model.asset_events.append(asset_event) session.add(asset_alias_model) dags_to_queue_from_asset_alias |= { @@ -224,7 +224,7 @@ def _queue_dagruns(cls, asset_id: int, dags_to_queue: set[DagModel], session: Se @classmethod def _slow_path_queue_dagruns(cls, asset_id: int, dags_to_queue: set[DagModel], session: Session) -> None: def _queue_dagrun_if_needed(dag: DagModel) -> str | None: - item = AssetDagRunQueue(target_dag_id=dag.dag_id, dataset_id=asset_id) + item = AssetDagRunQueue(target_dag_id=dag.dag_id, asset_id=asset_id) # Don't error whole transaction when a single RunQueue item conflicts. # https://docs.sqlalchemy.org/en/14/orm/session_transaction.html#using-savepoint try: @@ -243,7 +243,7 @@ def _postgres_queue_dagruns(cls, asset_id: int, dags_to_queue: set[DagModel], se from sqlalchemy.dialects.postgresql import insert values = [{"target_dag_id": dag.dag_id} for dag in dags_to_queue] - stmt = insert(AssetDagRunQueue).values(dataset_id=asset_id).on_conflict_do_nothing() + stmt = insert(AssetDagRunQueue).values(asset_id=asset_id).on_conflict_do_nothing() session.execute(stmt, values) @classmethod diff --git a/airflow/dag_processing/collection.py b/airflow/dag_processing/collection.py index 068a3727d04c0..034c9c0540125 100644 --- a/airflow/dag_processing/collection.py +++ b/airflow/dag_processing/collection.py @@ -67,9 +67,9 @@ def _find_orm_dags(dag_ids: Iterable[str], *, session: Session) -> dict[str, Dag select(DagModel) .options(joinedload(DagModel.tags, innerjoin=False)) .where(DagModel.dag_id.in_(dag_ids)) - .options(joinedload(DagModel.schedule_dataset_references)) - .options(joinedload(DagModel.schedule_dataset_alias_references)) - .options(joinedload(DagModel.task_outlet_dataset_references)) + .options(joinedload(DagModel.schedule_asset_references)) + .options(joinedload(DagModel.schedule_asset_alias_references)) + .options(joinedload(DagModel.task_outlet_asset_references)) ) stmt = with_row_locks(stmt, of=DagModel, session=session) return {dm.dag_id: dm for dm in session.scalars(stmt).unique()} @@ -223,7 +223,7 @@ def update_dags( ) dm.timetable_summary = dag.timetable.summary dm.timetable_description = dag.timetable.description - dm.dataset_expression = dag.timetable.asset_condition.as_expression() + dm.asset_expression = dag.timetable.asset_condition.as_expression() dm.processor_subdir = processor_subdir last_automated_run: DagRun | None = run_info.latest_runs.get(dag.dag_id) @@ -237,8 +237,8 @@ def update_dags( dm.calculate_dagrun_date_fields(dag, last_automated_data_interval) if not dag.timetable.asset_condition: - dm.schedule_dataset_references = [] - dm.schedule_dataset_alias_references = [] + dm.schedule_asset_references = [] + dm.schedule_asset_alias_references = [] # FIXME: STORE NEW REFERENCES. if dag.tags: @@ -367,15 +367,15 @@ def add_dag_asset_references( for dag_id, references in self.schedule_asset_references.items(): # Optimization: no references at all; this is faster than repeated delete(). if not references: - dags[dag_id].schedule_dataset_references = [] + dags[dag_id].schedule_asset_references = [] continue referenced_asset_ids = {asset.id for asset in (assets[r.uri] for r in references)} - orm_refs = {r.dataset_id: r for r in dags[dag_id].schedule_dataset_references} + orm_refs = {r.asset_id: r for r in dags[dag_id].schedule_asset_references} for asset_id, ref in orm_refs.items(): if asset_id not in referenced_asset_ids: session.delete(ref) session.bulk_save_objects( - DagScheduleAssetReference(dataset_id=asset_id, dag_id=dag_id) + DagScheduleAssetReference(asset_id=asset_id, dag_id=dag_id) for asset_id in referenced_asset_ids if asset_id not in orm_refs ) @@ -393,10 +393,10 @@ def add_dag_asset_alias_references( for dag_id, references in self.schedule_asset_alias_references.items(): # Optimization: no references at all; this is faster than repeated delete(). if not references: - dags[dag_id].schedule_dataset_alias_references = [] + dags[dag_id].schedule_asset_alias_references = [] continue referenced_alias_ids = {alias.id for alias in (aliases[r.name] for r in references)} - orm_refs = {a.alias_id: a for a in dags[dag_id].schedule_dataset_alias_references} + orm_refs = {a.alias_id: a for a in dags[dag_id].schedule_asset_alias_references} for alias_id, ref in orm_refs.items(): if alias_id not in referenced_alias_ids: session.delete(ref) @@ -419,18 +419,18 @@ def add_task_asset_references( for dag_id, references in self.outlet_references.items(): # Optimization: no references at all; this is faster than repeated delete(). if not references: - dags[dag_id].task_outlet_dataset_references = [] + dags[dag_id].task_outlet_asset_references = [] continue referenced_outlets = { (task_id, asset.id) for task_id, asset in ((task_id, assets[d.uri]) for task_id, d in references) } - orm_refs = {(r.task_id, r.dataset_id): r for r in dags[dag_id].task_outlet_dataset_references} + orm_refs = {(r.task_id, r.asset_id): r for r in dags[dag_id].task_outlet_asset_references} for key, ref in orm_refs.items(): if key not in referenced_outlets: session.delete(ref) session.bulk_save_objects( - TaskOutletAssetReference(dataset_id=asset_id, dag_id=dag_id, task_id=task_id) + TaskOutletAssetReference(asset_id=asset_id, dag_id=dag_id, task_id=task_id) for task_id, asset_id in referenced_outlets if (task_id, asset_id) not in orm_refs ) diff --git a/airflow/jobs/scheduler_job_runner.py b/airflow/jobs/scheduler_job_runner.py index a052bf700db7d..233b687d1069f 100644 --- a/airflow/jobs/scheduler_job_runner.py +++ b/airflow/jobs/scheduler_job_runner.py @@ -1272,15 +1272,15 @@ def _do_scheduling(self, session: Session) -> int: @retry_db_transaction def _create_dagruns_for_dags(self, guard: CommitProhibitorGuard, session: Session) -> None: """Find Dag Models needing DagRuns and Create Dag Runs with retries in case of OperationalError.""" - query, dataset_triggered_dag_info = DagModel.dags_needing_dagruns(session) + query, asset_triggered_dag_info = DagModel.dags_needing_dagruns(session) all_dags_needing_dag_runs = set(query.all()) - dataset_triggered_dags = [ - dag for dag in all_dags_needing_dag_runs if dag.dag_id in dataset_triggered_dag_info + asset_triggered_dags = [ + dag for dag in all_dags_needing_dag_runs if dag.dag_id in asset_triggered_dag_info ] - non_dataset_dags = all_dags_needing_dag_runs.difference(dataset_triggered_dags) - self._create_dag_runs(non_dataset_dags, session) - if dataset_triggered_dags: - self._create_dag_runs_asset_triggered(dataset_triggered_dags, dataset_triggered_dag_info, session) + non_asset_dags = all_dags_needing_dag_runs.difference(asset_triggered_dags) + self._create_dag_runs(non_asset_dags, session) + if asset_triggered_dags: + self._create_dag_runs_asset_triggered(asset_triggered_dags, asset_triggered_dag_info, session) # commit the session - Release the write lock on DagModel table. guard.commit() @@ -1391,7 +1391,7 @@ def _create_dag_runs(self, dag_models: Collection[DagModel], session: Session) - def _create_dag_runs_asset_triggered( self, dag_models: Collection[DagModel], - dataset_triggered_dag_info: dict[str, tuple[datetime, datetime]], + asset_triggered_dag_info: dict[str, tuple[datetime, datetime]], session: Session, ) -> None: """For DAGs that are triggered by assets, create dag runs.""" @@ -1401,7 +1401,7 @@ def _create_dag_runs_asset_triggered( # duplicate dag runs exec_dates = { dag_id: timezone.coerce_datetime(last_time) - for dag_id, (_, last_time) in dataset_triggered_dag_info.items() + for dag_id, (_, last_time) in asset_triggered_dag_info.items() } existing_dagruns: set[tuple[str, timezone.DateTime]] = set( session.execute( @@ -1441,7 +1441,7 @@ def _create_dag_runs_asset_triggered( .where( DagRun.dag_id == dag.dag_id, DagRun.execution_date < exec_date, - DagRun.run_type == DagRunType.DATASET_TRIGGERED, + DagRun.run_type == DagRunType.ASSET_TRIGGERED, ) .order_by(DagRun.execution_date.desc()) .limit(1) @@ -1457,14 +1457,14 @@ def _create_dag_runs_asset_triggered( select(AssetEvent) .join( DagScheduleAssetReference, - AssetEvent.dataset_id == DagScheduleAssetReference.dataset_id, + AssetEvent.asset_id == DagScheduleAssetReference.asset_id, ) .where(*asset_event_filters) ).all() data_interval = dag.timetable.data_interval_for_events(exec_date, asset_events) run_id = dag.timetable.generate_run_id( - run_type=DagRunType.DATASET_TRIGGERED, + run_type=DagRunType.ASSET_TRIGGERED, logical_date=exec_date, data_interval=data_interval, session=session, @@ -1473,7 +1473,7 @@ def _create_dag_runs_asset_triggered( dag_run = dag.create_dagrun( run_id=run_id, - run_type=DagRunType.DATASET_TRIGGERED, + run_type=DagRunType.ASSET_TRIGGERED, execution_date=exec_date, data_interval=data_interval, state=DagRunState.QUEUED, @@ -1484,7 +1484,7 @@ def _create_dag_runs_asset_triggered( triggered_by=DagRunTriggeredByType.DATASET, ) Stats.incr("asset.triggered_dagruns") - dag_run.consumed_dataset_events.extend(asset_events) + dag_run.consumed_asset_events.extend(asset_events) session.execute( delete(AssetDagRunQueue).where(AssetDagRunQueue.target_dag_id == dag_run.dag_id) ) diff --git a/airflow/listeners/spec/asset.py b/airflow/listeners/spec/asset.py index 78b14c8b10aeb..dba9ac700e415 100644 --- a/airflow/listeners/spec/asset.py +++ b/airflow/listeners/spec/asset.py @@ -33,8 +33,8 @@ def on_asset_created(asset: Asset): @hookspec -def on_asset_alias_created(dataset_alias: AssetAlias): - """Execute when a new dataset alias is created.""" +def on_asset_alias_created(asset_alias: AssetAlias): + """Execute when a new asset alias is created.""" @hookspec diff --git a/airflow/migrations/versions/0040_3_0_0_rename_dataset_as_asset.py b/airflow/migrations/versions/0040_3_0_0_rename_dataset_as_asset.py new file mode 100644 index 0000000000000..70261b75c7b56 --- /dev/null +++ b/airflow/migrations/versions/0040_3_0_0_rename_dataset_as_asset.py @@ -0,0 +1,693 @@ +# +# 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. + +""" +Rename dataset as asset. + +Revision ID: 05234396c6fc +Revises: 3a8972ecb8f9 +Create Date: 2024-10-02 08:10:01.697128 +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import sqlalchemy as sa +import sqlalchemy_jsonfield +from alembic import op + +from airflow.settings import json + +# revision identifiers, used by Alembic. +revision = "05234396c6fc" +down_revision = "3a8972ecb8f9" +branch_labels = None +depends_on = None +airflow_version = "3.0.0" + +if TYPE_CHECKING: + from alembic.operations.base import BatchOperations + from sqlalchemy.sql.elements import conv + + +def _rename_index( + *, batch_op: BatchOperations, original_name: str, new_name: str, columns: list[str], unique: bool +) -> None: + batch_op.drop_index(original_name) + batch_op.create_index(new_name, columns, unique=unique) + + +def _rename_fk_constraint( + *, + batch_op: BatchOperations, + original_name: str | conv, + new_name: str | conv, + referent_table: str, + local_cols: list[str], + remote_cols: list[str], + ondelete: str, +) -> None: + batch_op.drop_constraint(original_name, type_="foreignkey") + batch_op.create_foreign_key( + constraint_name=new_name, + referent_table=referent_table, + local_cols=local_cols, + remote_cols=remote_cols, + ondelete=ondelete, + ) + + +def _rename_pk_constraint( + *, batch_op: BatchOperations, original_name: str, new_name: str, columns: list[str] +) -> None: + if batch_op.get_bind().dialect.name in ("postgresql", "mysql"): + batch_op.drop_constraint(original_name, type_="primary") + batch_op.create_primary_key(constraint_name=new_name, columns=columns) + + +# original table name to new table name +table_name_mappings = ( + ("dataset_alias_dataset", "asset_alias_asset"), + ("dataset_alias_dataset_event", "asset_alias_asset_event"), + ("dataset_alias", "asset_alias"), + ("dataset", "asset"), + ("dag_schedule_dataset_alias_reference", "dag_schedule_asset_alias_reference"), + ("dag_schedule_dataset_reference", "dag_schedule_asset_reference"), + ("task_outlet_dataset_reference", "task_outlet_asset_reference"), + ("dataset_dag_run_queue", "asset_dag_run_queue"), + ("dagrun_dataset_event", "dagrun_asset_event"), + ("dataset_event", "asset_event"), +) + + +def upgrade(): + """Rename dataset as asset.""" + # Rename tables + for original_name, new_name in table_name_mappings: + op.rename_table(original_name, new_name) + + with op.batch_alter_table("asset_active", schema=None) as batch_op: + batch_op.drop_constraint("asset_active_asset_name_uri_fkey", type_="foreignkey") + + with op.batch_alter_table("asset", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_name_uri_unique", + new_name="idx_asset_name_uri_unique", + columns=["name", "uri"], + unique=True, + ) + + with op.batch_alter_table("asset_active", schema=None) as batch_op: + batch_op.create_foreign_key( + constraint_name="asset_active_asset_name_uri_fkey", + referent_table="asset", + local_cols=["name", "uri"], + remote_cols=["name", "uri"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("asset_alias_asset", schema=None) as batch_op: + batch_op.alter_column("dataset_id", new_column_name="asset_id", type_=sa.Integer(), nullable=False) + + with op.batch_alter_table("asset_alias_asset", schema=None) as batch_op: + batch_op.drop_constraint(op.f("dataset_alias_dataset_alias_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_alias_dataset_alias_id", + new_name="idx_asset_alias_asset_alias_id", + columns=["alias_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="asset_alias_asset_alias_id_fk_key", + referent_table="asset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint(op.f("dataset_alias_dataset_dataset_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_alias_dataset_alias_dataset_id", + new_name="idx_asset_alias_asset_asset_id", + columns=["asset_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="asset_alias_asset_asset_id_fk_key", + referent_table="asset", + local_cols=["asset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("asset_alias_asset_event", schema=None) as batch_op: + batch_op.drop_constraint(op.f("dataset_alias_dataset_event_alias_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_alias_dataset_event_alias_id", + new_name="idx_asset_alias_asset_event_alias_id", + columns=["alias_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("asset_alias_asset_event_asset_id_fkey"), + referent_table="asset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint(op.f("dataset_alias_dataset_event_event_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_alias_dataset_event_event_id", + new_name="idx_asset_alias_asset_event_event_id", + columns=["event_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("asset_alias_asset_event_event_id_fk_key"), + referent_table="asset_event", + local_cols=["event_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dag_schedule_asset_alias_reference", schema=None) as batch_op: + batch_op.drop_constraint("dsdar_dataset_fkey", type_="foreignkey") + batch_op.drop_constraint("dsdar_dag_id_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="dsdar_pkey", + new_name="asaar_pkey", + columns=["alias_id", "dag_id"], + ) + _rename_index( + batch_op=batch_op, + original_name="idx_dag_schedule_dataset_alias_reference_dag_id", + new_name="idx_dag_schedule_asset_alias_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="dsaar_asset_alias_fkey", + referent_table="asset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="dsaar_dag_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dag_schedule_asset_reference", schema=None) as batch_op: + batch_op.alter_column("dataset_id", new_column_name="asset_id", type_=sa.Integer(), nullable=False) + + with op.batch_alter_table("dag_schedule_asset_reference", schema=None) as batch_op: + batch_op.drop_constraint("dsdr_dag_id_fkey", type_="foreignkey") + if op.get_bind().dialect.name in ("postgres", "mysql"): + batch_op.drop_constraint("dsdr_dataset_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="dsdr_pkey", + new_name="dsar_pkey", + columns=["asset_id", "dag_id"], + ) + _rename_index( + batch_op=batch_op, + original_name="idx_dag_schedule_dataset_reference_dag_id", + new_name="idx_dag_schedule_asset_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="dsar_dag_id_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="dsar_asset_fkey", + referent_table="asset", + local_cols=["asset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("task_outlet_asset_reference", schema=None) as batch_op: + batch_op.alter_column("dataset_id", new_column_name="asset_id", type_=sa.Integer(), nullable=False) + + batch_op.drop_constraint("todr_dag_id_fkey", type_="foreignkey") + if op.get_bind().dialect.name in ("postgres", "mysql"): + batch_op.drop_constraint("todr_dataset_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="todr_pkey", + new_name="toar_pkey", + columns=["asset_id", "dag_id", "task_id"], + ) + + _rename_index( + batch_op=batch_op, + original_name="idx_task_outlet_dataset_reference_dag_id", + new_name="idx_task_outlet_asset_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="toar_asset_fkey", + referent_table="asset", + local_cols=["asset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="toar_dag_id_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("asset_dag_run_queue", schema=None) as batch_op: + batch_op.alter_column("dataset_id", new_column_name="asset_id", type_=sa.Integer(), nullable=False) + + batch_op.drop_constraint("ddrq_dag_fkey", type_="foreignkey") + if op.get_bind().dialect.name in ("postgres", "mysql"): + batch_op.drop_constraint("ddrq_dataset_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="datasetdagrunqueue_pkey", + new_name="assetdagrunqueue_pkey", + columns=["asset_id", "target_dag_id"], + ) + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_dag_run_queue_target_dag_id", + new_name="idx_asset_dag_run_queue_target_dag_id", + columns=["target_dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="adrq_asset_fkey", + referent_table="asset", + local_cols=["asset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="adrq_dag_fkey", + referent_table="dag", + local_cols=["target_dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dagrun_asset_event", schema=None) as batch_op: + batch_op.drop_constraint("dagrun_dataset_event_event_id_fkey", type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dagrun_dataset_events_dag_run_id", + new_name="idx_dagrun_asset_events_dag_run_id", + columns=["dag_run_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="dagrun_asset_event_dag_run_id_fkey", + referent_table="dag_run", + local_cols=["dag_run_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint("dagrun_dataset_event_dag_run_id_fkey", type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dagrun_dataset_events_event_id", + new_name="idx_dagrun_asset_events_event_id", + columns=["event_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="dagrun_asset_event_event_id_fkey", + referent_table="asset_event", + local_cols=["event_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("asset_event", schema=None) as batch_op: + batch_op.alter_column("dataset_id", new_column_name="asset_id", type_=sa.Integer(), nullable=False) + + with op.batch_alter_table("asset_event", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_id_timestamp", + new_name="idx_asset_id_timestamp", + columns=["asset_id", "timestamp"], + unique=False, + ) + + with op.batch_alter_table("asset_alias", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_dataset_alias_name_unique", + new_name="idx_asset_alias_name_unique", + columns=["name"], + unique=True, + ) + + with op.batch_alter_table("dag", schema=None) as batch_op: + batch_op.alter_column( + "dataset_expression", + new_column_name="asset_expression", + type_=sqlalchemy_jsonfield.JSONField(json=json), + ) + + +def downgrade(): + """Unapply Rename dataset as asset.""" + # Rename tables + for original_name, new_name in table_name_mappings: + op.rename_table(new_name, original_name) + + with op.batch_alter_table("asset_active", schema=None) as batch_op: + batch_op.drop_constraint("asset_active_asset_name_uri_fkey", type_="foreignkey") + + with op.batch_alter_table("dataset", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_asset_name_uri_unique", + new_name="idx_dataset_name_uri_unique", + columns=["name", "uri"], + unique=True, + ) + + with op.batch_alter_table("asset_active", schema=None) as batch_op: + batch_op.create_foreign_key( + constraint_name="asset_active_asset_name_uri_fkey", + referent_table="dataset", + local_cols=["name", "uri"], + remote_cols=["name", "uri"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dataset_alias_dataset", schema=None) as batch_op: + batch_op.alter_column("asset_id", new_column_name="dataset_id", type_=sa.Integer(), nullable=False) + + with op.batch_alter_table("dataset_alias_dataset", schema=None) as batch_op: + batch_op.drop_constraint(op.f("asset_alias_asset_alias_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_asset_alias_asset_alias_id", + new_name="idx_dataset_alias_dataset_alias_id", + columns=["alias_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("dataset_alias_dataset_alias_id_fkey"), + referent_table="dataset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint(op.f("asset_alias_asset_asset_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_asset_alias_asset_asset_id", + new_name="idx_dataset_alias_dataset_alias_dataset_id", + columns=["dataset_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("dataset_alias_dataset_dataset_id_fkey"), + referent_table="dataset", + local_cols=["dataset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dataset_alias_dataset_event", schema=None) as batch_op: + batch_op.drop_constraint(op.f("asset_alias_asset_event_alias_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_asset_alias_asset_event_alias_id", + new_name="idx_dataset_alias_dataset_event_alias_id", + columns=["alias_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("dataset_alias_dataset_event_alias_id_fkey"), + referent_table="dataset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint(op.f("asset_alias_asset_event_event_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_asset_alias_asset_event_event_id", + new_name="idx_dataset_alias_dataset_event_event_id", + columns=["event_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name=op.f("dataset_alias_dataset_event_event_id_fkey"), + referent_table="dataset_event", + local_cols=["event_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dag_schedule_dataset_alias_reference", schema=None) as batch_op: + batch_op.drop_constraint("dsaar_asset_alias_fkey", type_="foreignkey") + batch_op.drop_constraint("dsaar_dag_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="asaar_pkey", + new_name="dsdar_pkey", + columns=["alias_id", "dag_id"], + ) + _rename_index( + batch_op=batch_op, + original_name="idx_dag_schedule_asset_alias_reference_dag_id", + new_name="idx_dag_schedule_dataset_alias_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="dsdar_dataset_fkey", + referent_table="dataset_alias", + local_cols=["alias_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="dsdar_dag_id_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dag_schedule_dataset_reference", schema=None) as batch_op: + batch_op.alter_column("asset_id", new_column_name="dataset_id", type_=sa.Integer(), nullable=False) + + batch_op.drop_constraint("dsar_dag_id_fkey", type_="foreignkey") + batch_op.drop_constraint("dsar_asset_fkey", type_="foreignkey") + + _rename_index( + batch_op=batch_op, + original_name="idx_dag_schedule_asset_reference_dag_id", + new_name="idx_dag_schedule_dataset_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + _rename_pk_constraint( + batch_op=batch_op, + original_name="dsar_pkey", + new_name="dsdr_pkey", + columns=["dataset_id", "dag_id"], + ) + + batch_op.create_foreign_key( + constraint_name="dsdr_dag_id_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="dsdr_dataset_fkey", + referent_table="dataset", + local_cols=["dataset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("task_outlet_dataset_reference", schema=None) as batch_op: + batch_op.alter_column("asset_id", new_column_name="dataset_id", type_=sa.Integer(), nullable=False) + + batch_op.drop_constraint("toar_asset_fkey", type_="foreignkey") + batch_op.drop_constraint("toar_dag_id_fkey", type_="foreignkey") + + _rename_index( + batch_op=batch_op, + original_name="idx_task_outlet_asset_reference_dag_id", + new_name="idx_task_outlet_dataset_reference_dag_id", + columns=["dag_id"], + unique=False, + ) + _rename_pk_constraint( + batch_op=batch_op, + original_name="toar_pkey", + new_name="todr_pkey", + columns=["dataset_id", "dag_id", "task_id"], + ) + + batch_op.create_foreign_key( + constraint_name="todr_dataset_fkey", + referent_table="dataset", + local_cols=["dataset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="todr_dag_id_fkey", + referent_table="dag", + local_cols=["dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dataset_dag_run_queue", schema=None) as batch_op: + batch_op.alter_column("asset_id", new_column_name="dataset_id", type_=sa.Integer(), nullable=False) + + batch_op.drop_constraint("adrq_asset_fkey", type_="foreignkey") + batch_op.drop_constraint("adrq_dag_fkey", type_="foreignkey") + + _rename_pk_constraint( + batch_op=batch_op, + original_name="assetdagrunqueue_pkey", + new_name="datasetdagrunqueue_pkey", + columns=["dataset_id", "target_dag_id"], + ) + _rename_index( + batch_op=batch_op, + original_name="idx_asset_dag_run_queue_target_dag_id", + new_name="idx_dataset_dag_run_queue_target_dag_id", + columns=["target_dag_id"], + unique=False, + ) + + batch_op.create_foreign_key( + constraint_name="ddrq_dataset_fkey", + referent_table="dataset", + local_cols=["dataset_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + batch_op.create_foreign_key( + constraint_name="ddrq_dag_fkey", + referent_table="dag", + local_cols=["target_dag_id"], + remote_cols=["dag_id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dagrun_dataset_event", schema=None) as batch_op: + batch_op.drop_constraint(op.f("dagrun_asset_event_event_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dagrun_asset_events_event_id", + new_name="idx_dagrun_dataset_events_event_id", + columns=["event_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="dagrun_dataset_event_event_id_fkey", + referent_table="dataset_event", + local_cols=["event_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + batch_op.drop_constraint(op.f("dagrun_asset_event_dag_run_id_fkey"), type_="foreignkey") + _rename_index( + batch_op=batch_op, + original_name="idx_dagrun_asset_events_dag_run_id", + new_name="idx_dagrun_dataset_events_dag_run_id", + columns=["dag_run_id"], + unique=False, + ) + batch_op.create_foreign_key( + constraint_name="dagrun_dataset_event_dag_run_id_fkey", + referent_table="dag_run", + local_cols=["dag_run_id"], + remote_cols=["id"], + ondelete="CASCADE", + ) + + with op.batch_alter_table("dataset_event", schema=None) as batch_op: + batch_op.alter_column("asset_id", new_column_name="dataset_id", type_=sa.Integer(), nullable=False) + + with op.batch_alter_table("dataset_event", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_asset_id_timestamp", + new_name="idx_dataset_id_timestamp", + columns=["dataset_id", "timestamp"], + unique=False, + ) + + with op.batch_alter_table("dataset_alias", schema=None) as batch_op: + _rename_index( + batch_op=batch_op, + original_name="idx_asset_alias_name_unique", + new_name="idx_dataset_alias_name_unique", + columns=["name"], + unique=True, + ) + + with op.batch_alter_table("dag", schema=None) as batch_op: + batch_op.alter_column( + "asset_expression", + new_column_name="dataset_expression", + type_=sqlalchemy_jsonfield.JSONField(json=json), + ) diff --git a/airflow/models/asset.py b/airflow/models/asset.py index 79f9b7389439d..2b810e3d0f641 100644 --- a/airflow/models/asset.py +++ b/airflow/models/asset.py @@ -40,45 +40,21 @@ from airflow.utils.sqlalchemy import UtcDateTime alias_association_table = Table( - "dataset_alias_dataset", + "asset_alias_asset", Base.metadata, - Column("alias_id", ForeignKey("dataset_alias.id", ondelete="CASCADE"), primary_key=True), - Column("dataset_id", ForeignKey("dataset.id", ondelete="CASCADE"), primary_key=True), - Index("idx_dataset_alias_dataset_alias_id", "alias_id"), - Index("idx_dataset_alias_dataset_alias_dataset_id", "dataset_id"), - ForeignKeyConstraint( - ("alias_id",), - ["dataset_alias.id"], - name="ds_dsa_alias_id", - ondelete="CASCADE", - ), - ForeignKeyConstraint( - ("dataset_id",), - ["dataset.id"], - name="ds_dsa_dataset_id", - ondelete="CASCADE", - ), + Column("alias_id", ForeignKey("asset_alias.id", ondelete="CASCADE"), primary_key=True), + Column("asset_id", ForeignKey("asset.id", ondelete="CASCADE"), primary_key=True), + Index("idx_asset_alias_asset_alias_id", "alias_id"), + Index("idx_asset_alias_asset_asset_id", "asset_id"), ) -dataset_alias_dataset_event_assocation_table = Table( - "dataset_alias_dataset_event", +asset_alias_asset_event_assocation_table = Table( + "asset_alias_asset_event", Base.metadata, - Column("alias_id", ForeignKey("dataset_alias.id", ondelete="CASCADE"), primary_key=True), - Column("event_id", ForeignKey("dataset_event.id", ondelete="CASCADE"), primary_key=True), - Index("idx_dataset_alias_dataset_event_alias_id", "alias_id"), - Index("idx_dataset_alias_dataset_event_event_id", "event_id"), - ForeignKeyConstraint( - ("alias_id",), - ["dataset_alias.id"], - name="dss_de_alias_id", - ondelete="CASCADE", - ), - ForeignKeyConstraint( - ("event_id",), - ["dataset_event.id"], - name="dss_de_event_id", - ondelete="CASCADE", - ), + Column("alias_id", ForeignKey("asset_alias.id", ondelete="CASCADE"), primary_key=True), + Column("event_id", ForeignKey("asset_event.id", ondelete="CASCADE"), primary_key=True), + Index("idx_asset_alias_asset_event_alias_id", "alias_id"), + Index("idx_asset_alias_asset_event_event_id", "event_id"), ) @@ -116,23 +92,23 @@ class AssetAliasModel(Base): nullable=False, ) - __tablename__ = "dataset_alias" + __tablename__ = "asset_alias" __table_args__ = ( - Index("idx_dataset_alias_name_unique", name, unique=True), + Index("idx_asset_alias_name_unique", name, unique=True), {"sqlite_autoincrement": True}, # ensures PK values not reused ) - datasets = relationship( + assets = relationship( "AssetModel", secondary=alias_association_table, backref="aliases", ) - dataset_events = relationship( + asset_events = relationship( "AssetEvent", - secondary=dataset_alias_dataset_event_assocation_table, + secondary=asset_alias_asset_event_assocation_table, back_populates="source_aliases", ) - consuming_dags = relationship("DagScheduleAssetAliasReference", back_populates="dataset_alias") + consuming_dags = relationship("DagScheduleAssetAliasReference", back_populates="asset_alias") @classmethod def from_public(cls, obj: AssetAlias) -> AssetAliasModel: @@ -207,12 +183,12 @@ class AssetModel(Base): active = relationship("AssetActive", uselist=False, viewonly=True) - consuming_dags = relationship("DagScheduleAssetReference", back_populates="dataset") - producing_tasks = relationship("TaskOutletAssetReference", back_populates="dataset") + consuming_dags = relationship("DagScheduleAssetReference", back_populates="asset") + producing_tasks = relationship("TaskOutletAssetReference", back_populates="asset") - __tablename__ = "dataset" + __tablename__ = "asset" __table_args__ = ( - Index("idx_dataset_name_uri_unique", name, uri, unique=True), + Index("idx_asset_name_uri_unique", name, uri, unique=True), {"sqlite_autoincrement": True}, # ensures PK values not reused ) @@ -293,7 +269,7 @@ class AssetActive(Base): PrimaryKeyConstraint(name, uri, name="asset_active_pkey"), ForeignKeyConstraint( columns=[name, uri], - refcolumns=["dataset.name", "dataset.uri"], + refcolumns=["asset.name", "asset.uri"], name="asset_active_asset_name_uri_fkey", ondelete="CASCADE", ), @@ -314,25 +290,25 @@ class DagScheduleAssetAliasReference(Base): created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) updated_at = Column(UtcDateTime, default=timezone.utcnow, onupdate=timezone.utcnow, nullable=False) - dataset_alias = relationship("AssetAliasModel", back_populates="consuming_dags") - dag = relationship("DagModel", back_populates="schedule_dataset_alias_references") + asset_alias = relationship("AssetAliasModel", back_populates="consuming_dags") + dag = relationship("DagModel", back_populates="schedule_asset_alias_references") - __tablename__ = "dag_schedule_dataset_alias_reference" + __tablename__ = "dag_schedule_asset_alias_reference" __table_args__ = ( - PrimaryKeyConstraint(alias_id, dag_id, name="dsdar_pkey"), + PrimaryKeyConstraint(alias_id, dag_id, name="asaar_pkey"), ForeignKeyConstraint( (alias_id,), - ["dataset_alias.id"], - name="dsdar_dataset_alias_fkey", + ["asset_alias.id"], + name="dsaar_asset_alias_fkey", ondelete="CASCADE", ), ForeignKeyConstraint( columns=(dag_id,), refcolumns=["dag.dag_id"], - name="dsdar_dag_fkey", + name="dsaar_dag_fkey", ondelete="CASCADE", ), - Index("idx_dag_schedule_dataset_alias_reference_dag_id", dag_id), + Index("idx_dag_schedule_asset_alias_reference_dag_id", dag_id), ) def __eq__(self, other): @@ -344,104 +320,99 @@ def __hash__(self): return hash(self.__mapper__.primary_key) def __repr__(self): - args = [] - for attr in [x.name for x in self.__mapper__.primary_key]: - args.append(f"{attr}={getattr(self, attr)!r}") + args = [f"{x.name}={getattr(self, x.name)!r}" for x in self.__mapper__.primary_key] return f"{self.__class__.__name__}({', '.join(args)})" class DagScheduleAssetReference(Base): """References from a DAG to an asset of which it is a consumer.""" - dataset_id = Column(Integer, primary_key=True, nullable=False) + asset_id = Column(Integer, primary_key=True, nullable=False) dag_id = Column(StringID(), primary_key=True, nullable=False) created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) updated_at = Column(UtcDateTime, default=timezone.utcnow, onupdate=timezone.utcnow, nullable=False) - dataset = relationship("AssetModel", back_populates="consuming_dags") - dag = relationship("DagModel", back_populates="schedule_dataset_references") + asset = relationship("AssetModel", back_populates="consuming_dags") + dag = relationship("DagModel", back_populates="schedule_asset_references") queue_records = relationship( "AssetDagRunQueue", primaryjoin="""and_( - DagScheduleAssetReference.dataset_id == foreign(AssetDagRunQueue.dataset_id), + DagScheduleAssetReference.asset_id == foreign(AssetDagRunQueue.asset_id), DagScheduleAssetReference.dag_id == foreign(AssetDagRunQueue.target_dag_id), )""", cascade="all, delete, delete-orphan", ) - __tablename__ = "dag_schedule_dataset_reference" + __tablename__ = "dag_schedule_asset_reference" __table_args__ = ( - PrimaryKeyConstraint(dataset_id, dag_id, name="dsdr_pkey"), + PrimaryKeyConstraint(asset_id, dag_id, name="dsar_pkey"), ForeignKeyConstraint( - (dataset_id,), - ["dataset.id"], - name="dsdr_dataset_fkey", + (asset_id,), + ["asset.id"], + name="dsar_asset_fkey", ondelete="CASCADE", ), ForeignKeyConstraint( columns=(dag_id,), refcolumns=["dag.dag_id"], - name="dsdr_dag_id_fkey", + name="dsar_dag_id_fkey", ondelete="CASCADE", ), - Index("idx_dag_schedule_dataset_reference_dag_id", dag_id), + Index("idx_dag_schedule_asset_reference_dag_id", dag_id), ) def __eq__(self, other): if isinstance(other, self.__class__): - return self.dataset_id == other.dataset_id and self.dag_id == other.dag_id - else: - return NotImplemented + return self.asset_id == other.asset_id and self.dag_id == other.dag_id + return NotImplemented def __hash__(self): return hash(self.__mapper__.primary_key) def __repr__(self): - args = [] - for attr in [x.name for x in self.__mapper__.primary_key]: - args.append(f"{attr}={getattr(self, attr)!r}") + args = [f"{attr}={getattr(self, attr)!r}" for attr in [x.name for x in self.__mapper__.primary_key]] return f"{self.__class__.__name__}({', '.join(args)})" class TaskOutletAssetReference(Base): """References from a task to an asset that it updates / produces.""" - dataset_id = Column(Integer, primary_key=True, nullable=False) + asset_id = Column(Integer, primary_key=True, nullable=False) dag_id = Column(StringID(), primary_key=True, nullable=False) task_id = Column(StringID(), primary_key=True, nullable=False) created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) updated_at = Column(UtcDateTime, default=timezone.utcnow, onupdate=timezone.utcnow, nullable=False) - dataset = relationship("AssetModel", back_populates="producing_tasks") + asset = relationship("AssetModel", back_populates="producing_tasks") - __tablename__ = "task_outlet_dataset_reference" + __tablename__ = "task_outlet_asset_reference" __table_args__ = ( ForeignKeyConstraint( - (dataset_id,), - ["dataset.id"], - name="todr_dataset_fkey", + (asset_id,), + ["asset.id"], + name="toar_asset_fkey", ondelete="CASCADE", ), - PrimaryKeyConstraint(dataset_id, dag_id, task_id, name="todr_pkey"), + PrimaryKeyConstraint(asset_id, dag_id, task_id, name="toar_pkey"), ForeignKeyConstraint( columns=(dag_id,), refcolumns=["dag.dag_id"], - name="todr_dag_id_fkey", + name="toar_dag_id_fkey", ondelete="CASCADE", ), - Index("idx_task_outlet_dataset_reference_dag_id", dag_id), + Index("idx_task_outlet_asset_reference_dag_id", dag_id), ) def __eq__(self, other): if isinstance(other, self.__class__): return ( - self.dataset_id == other.dataset_id + self.asset_id == other.asset_id and self.dag_id == other.dag_id and self.task_id == other.task_id ) - else: - return NotImplemented + + return NotImplemented def __hash__(self): return hash(self.__mapper__.primary_key) @@ -456,31 +427,32 @@ def __repr__(self): class AssetDagRunQueue(Base): """Model for storing asset events that need processing.""" - dataset_id = Column(Integer, primary_key=True, nullable=False) + asset_id = Column(Integer, primary_key=True, nullable=False) target_dag_id = Column(StringID(), primary_key=True, nullable=False) created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) - dataset = relationship("AssetModel", viewonly=True) - __tablename__ = "dataset_dag_run_queue" + asset = relationship("AssetModel", viewonly=True) + + __tablename__ = "asset_dag_run_queue" __table_args__ = ( - PrimaryKeyConstraint(dataset_id, target_dag_id, name="datasetdagrunqueue_pkey"), + PrimaryKeyConstraint(asset_id, target_dag_id, name="assetdagrunqueue_pkey"), ForeignKeyConstraint( - (dataset_id,), - ["dataset.id"], - name="ddrq_dataset_fkey", + (asset_id,), + ["asset.id"], + name="adrq_asset_fkey", ondelete="CASCADE", ), ForeignKeyConstraint( (target_dag_id,), ["dag.dag_id"], - name="ddrq_dag_fkey", + name="adrq_dag_fkey", ondelete="CASCADE", ), - Index("idx_dataset_dag_run_queue_target_dag_id", target_dag_id), + Index("idx_asset_dag_run_queue_target_dag_id", target_dag_id), ) def __eq__(self, other): if isinstance(other, self.__class__): - return self.dataset_id == other.dataset_id and self.target_dag_id == other.target_dag_id + return self.asset_id == other.asset_id and self.target_dag_id == other.target_dag_id else: return NotImplemented @@ -495,12 +467,12 @@ def __repr__(self): association_table = Table( - "dagrun_dataset_event", + "dagrun_asset_event", Base.metadata, Column("dag_run_id", ForeignKey("dag_run.id", ondelete="CASCADE"), primary_key=True), - Column("event_id", ForeignKey("dataset_event.id", ondelete="CASCADE"), primary_key=True), - Index("idx_dagrun_dataset_events_dag_run_id", "dag_run_id"), - Index("idx_dagrun_dataset_events_event_id", "event_id"), + Column("event_id", ForeignKey("asset_event.id", ondelete="CASCADE"), primary_key=True), + Index("idx_dagrun_asset_events_dag_run_id", "dag_run_id"), + Index("idx_dagrun_asset_events_event_id", "event_id"), ) @@ -508,7 +480,7 @@ class AssetEvent(Base): """ A table to store assets events. - :param dataset_id: reference to AssetModel record + :param asset_id: reference to AssetModel record :param extra: JSON field for arbitrary extra info :param source_task_id: the task_id of the TI which updated the asset :param source_dag_id: the dag_id of the TI which updated the asset @@ -521,7 +493,7 @@ class AssetEvent(Base): """ id = Column(Integer, primary_key=True, autoincrement=True) - dataset_id = Column(Integer, nullable=False) + asset_id = Column(Integer, nullable=False) extra = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}) source_task_id = Column(StringID(), nullable=True) source_dag_id = Column(StringID(), nullable=True) @@ -529,22 +501,22 @@ class AssetEvent(Base): source_map_index = Column(Integer, nullable=True, server_default=text("-1")) timestamp = Column(UtcDateTime, default=timezone.utcnow, nullable=False) - __tablename__ = "dataset_event" + __tablename__ = "asset_event" __table_args__ = ( - Index("idx_dataset_id_timestamp", dataset_id, timestamp), + Index("idx_asset_id_timestamp", asset_id, timestamp), {"sqlite_autoincrement": True}, # ensures PK values not reused ) created_dagruns = relationship( "DagRun", secondary=association_table, - backref="consumed_dataset_events", + backref="consumed_asset_events", ) source_aliases = relationship( "AssetAliasModel", - secondary=dataset_alias_dataset_event_assocation_table, - back_populates="dataset_events", + secondary=asset_alias_asset_event_assocation_table, + back_populates="asset_events", ) source_task_instance = relationship( @@ -569,9 +541,9 @@ class AssetEvent(Base): lazy="select", uselist=False, ) - dataset = relationship( + asset = relationship( AssetModel, - primaryjoin="AssetEvent.dataset_id == foreign(AssetModel.id)", + primaryjoin="AssetEvent.asset_id == foreign(AssetModel.id)", viewonly=True, lazy="select", uselist=False, @@ -579,13 +551,13 @@ class AssetEvent(Base): @property def uri(self): - return self.dataset.uri + return self.asset.uri def __repr__(self) -> str: args = [] for attr in [ "id", - "dataset_id", + "asset_id", "extra", "source_task_id", "source_dag_id", diff --git a/airflow/models/dag.py b/airflow/models/dag.py index f0a7d7f56be2c..00943ec2ee262 100644 --- a/airflow/models/dag.py +++ b/airflow/models/dag.py @@ -268,12 +268,12 @@ def get_asset_triggered_next_run_info( .join( ADRQ, and_( - ADRQ.dataset_id == DagScheduleAssetReference.dataset_id, + ADRQ.asset_id == DagScheduleAssetReference.asset_id, ADRQ.target_dag_id == DagScheduleAssetReference.dag_id, ), isouter=True, ) - .join(AssetModel, AssetModel.id == DagScheduleAssetReference.dataset_id) + .join(AssetModel, AssetModel.id == DagScheduleAssetReference.asset_id) .group_by(DagScheduleAssetReference.dag_id) .where(DagScheduleAssetReference.dag_id.in_(dag_ids)) ).all() @@ -322,7 +322,7 @@ def _create_orm_dagrun( ) # Load defaults into the following two fields to ensure result can be serialized detached run.log_template_id = int(session.scalar(select(func.max(LogTemplate.__table__.c.id)))) - run.consumed_dataset_events = [] + run.consumed_asset_events = [] session.add(run) session.flush() run.dag = dag @@ -376,7 +376,7 @@ class DAG(LoggingMixin): .. versionadded:: 2.4 The *schedule* argument to specify either time-based scheduling logic - (timetable), or dataset-driven triggers. + (timetable), or asset-driven triggers. .. versionchanged:: 3.0 The default value of *schedule* has been changed to *None* (no schedule). @@ -2675,9 +2675,9 @@ def get_serialized_fields(cls): """Stringified DAGs and operators contain exactly these fields.""" if not cls.__serialized_fields: exclusion_list = { - "schedule_dataset_references", - "schedule_dataset_alias_references", - "task_outlet_dataset_references", + "schedule_asset_references", + "schedule_asset_alias_references", + "task_outlet_asset_references", "_old_context_manager_dags", "safe_dag_id", "last_loaded", @@ -2842,8 +2842,8 @@ class DagModel(Base): timetable_summary = Column(Text, nullable=True) # Timetable description timetable_description = Column(String(1000), nullable=True) - # Dataset expression based on dataset triggers - dataset_expression = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) + # Asset expression based on asset triggers + asset_expression = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) # Tags for view filter tags = relationship("DagTag", cascade="all, delete, delete-orphan", backref=backref("dag")) # Dag owner links for DAGs view @@ -2870,18 +2870,18 @@ class DagModel(Base): __table_args__ = (Index("idx_next_dagrun_create_after", next_dagrun_create_after, unique=False),) - schedule_dataset_references = relationship( + schedule_asset_references = relationship( "DagScheduleAssetReference", back_populates="dag", cascade="all, delete, delete-orphan", ) - schedule_dataset_alias_references = relationship( + schedule_asset_alias_references = relationship( "DagScheduleAssetAliasReference", back_populates="dag", cascade="all, delete, delete-orphan", ) - schedule_datasets = association_proxy("schedule_dataset_references", "dataset") - task_outlet_dataset_references = relationship( + schedule_assets = association_proxy("schedule_asset_references", "asset") + task_outlet_asset_references = relationship( "TaskOutletAssetReference", cascade="all, delete, delete-orphan", ) @@ -3093,7 +3093,7 @@ def dag_ready(dag_id: str, cond: BaseAsset, statuses: dict) -> bool | None: del all_records dag_statuses = {} for dag_id, records in by_dag.items(): - dag_statuses[dag_id] = {x.dataset.uri: True for x in records} + dag_statuses[dag_id] = {x.asset.uri: True for x in records} ser_dags = session.scalars( select(SerializedDagModel).where(SerializedDagModel.dag_id.in_(dag_statuses.keys())) ).all() @@ -3105,27 +3105,27 @@ def dag_ready(dag_id: str, cond: BaseAsset, statuses: dict) -> bool | None: del by_dag[dag_id] del dag_statuses[dag_id] del dag_statuses - dataset_triggered_dag_info = {} + asset_triggered_dag_info = {} for dag_id, records in by_dag.items(): times = sorted(x.created_at for x in records) - dataset_triggered_dag_info[dag_id] = (times[0], times[-1]) + asset_triggered_dag_info[dag_id] = (times[0], times[-1]) del by_dag - dataset_triggered_dag_ids = set(dataset_triggered_dag_info.keys()) - if dataset_triggered_dag_ids: + asset_triggered_dag_ids = set(asset_triggered_dag_info.keys()) + if asset_triggered_dag_ids: exclusion_list = set( session.scalars( select(DagModel.dag_id) .join(DagRun.dag_model) .where(DagRun.state.in_((DagRunState.QUEUED, DagRunState.RUNNING))) - .where(DagModel.dag_id.in_(dataset_triggered_dag_ids)) + .where(DagModel.dag_id.in_(asset_triggered_dag_ids)) .group_by(DagModel.dag_id) .having(func.count() >= func.max(DagModel.max_active_runs)) ) ) if exclusion_list: - dataset_triggered_dag_ids -= exclusion_list - dataset_triggered_dag_info = { - k: v for k, v in dataset_triggered_dag_info.items() if k not in exclusion_list + asset_triggered_dag_ids -= exclusion_list + asset_triggered_dag_info = { + k: v for k, v in asset_triggered_dag_info.items() if k not in exclusion_list } # We limit so that _one_ scheduler doesn't try to do all the creation of dag runs @@ -3137,7 +3137,7 @@ def dag_ready(dag_id: str, cond: BaseAsset, statuses: dict) -> bool | None: cls.has_import_errors == expression.false(), or_( cls.next_dagrun_create_after <= func.now(), - cls.dag_id.in_(dataset_triggered_dag_ids), + cls.dag_id.in_(asset_triggered_dag_ids), ), ) .order_by(cls.next_dagrun_create_after) @@ -3146,7 +3146,7 @@ def dag_ready(dag_id: str, cond: BaseAsset, statuses: dict) -> bool | None: return ( session.scalars(with_row_locks(query, of=cls, session=session, skip_locked=True)), - dataset_triggered_dag_info, + asset_triggered_dag_info, ) def calculate_dagrun_date_fields( @@ -3186,7 +3186,7 @@ def calculate_dagrun_date_fields( @provide_session def get_asset_triggered_next_run_info(self, *, session=NEW_SESSION) -> dict[str, int | str] | None: - if self.dataset_expression is None: + if self.asset_expression is None: return None # When an asset alias does not resolve into assets, get_asset_triggered_next_run_info returns diff --git a/airflow/models/dagrun.py b/airflow/models/dagrun.py index 20ec12b9915e7..5de0466a6be0e 100644 --- a/airflow/models/dagrun.py +++ b/airflow/models/dagrun.py @@ -91,7 +91,7 @@ CreatedTasks = TypeVar("CreatedTasks", Iterator["dict[str, Any]"], Iterator[TI]) -RUN_ID_REGEX = r"^(?:manual|scheduled|dataset_triggered)__(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00)$" +RUN_ID_REGEX = r"^(?:manual|scheduled|asset_triggered)__(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00)$" class TISchedulingDecision(NamedTuple): diff --git a/airflow/models/taskinstance.py b/airflow/models/taskinstance.py index 1dbe299f25b39..b5fcac30a9ad5 100644 --- a/airflow/models/taskinstance.py +++ b/airflow/models/taskinstance.py @@ -1086,11 +1086,11 @@ def get_triggering_events() -> dict[str, list[AssetEvent | AssetEventPydantic]]: nonlocal dag_run if dag_run not in session: dag_run = session.merge(dag_run, load=False) - asset_events = dag_run.consumed_dataset_events + asset_events = dag_run.consumed_asset_events triggering_events: dict[str, list[AssetEvent | AssetEventPydantic]] = defaultdict(list) for event in asset_events: - if event.dataset: - triggering_events[event.dataset.uri].append(event) + if event.asset: + triggering_events[event.asset.uri].append(event) return triggering_events @@ -2911,22 +2911,22 @@ def _register_asset_changes(self, *, events: OutletEventAccessors, session: Sess frozen_extra = frozenset(asset_alias_event["extra"].items()) asset_alias_names[(asset_uri, frozen_extra)].add(asset_alias_name) - dataset_models: dict[str, AssetModel] = { - dataset_obj.uri: dataset_obj - for dataset_obj in session.scalars( + asset_models: dict[str, AssetModel] = { + asset_obj.uri: asset_obj + for asset_obj in session.scalars( select(AssetModel).where(AssetModel.uri.in_(uri for uri, _ in asset_alias_names)) ) } - if missing_datasets := [Asset(uri=u) for u, _ in asset_alias_names if u not in dataset_models]: - dataset_models.update( - (dataset_obj.uri, dataset_obj) - for dataset_obj in asset_manager.create_assets(missing_datasets, session=session) + if missing_assets := [Asset(uri=u) for u, _ in asset_alias_names if u not in asset_models]: + asset_models.update( + (asset_obj.uri, asset_obj) + for asset_obj in asset_manager.create_assets(missing_assets, session=session) ) - self.log.warning("Created new datasets for alias reference: %s", missing_datasets) + self.log.warning("Created new assets for alias reference: %s", missing_assets) session.flush() # Needed because we need the id for fk. for (uri, extra_items), alias_names in asset_alias_names.items(): - asset_obj = dataset_models[uri] + asset_obj = asset_models[uri] self.log.info( 'Creating event for %r through aliases "%s"', asset_obj, @@ -2935,7 +2935,7 @@ def _register_asset_changes(self, *, events: OutletEventAccessors, session: Sess asset_manager.register_asset_change( task_instance=self, asset=asset_obj, - aliases=[AssetAlias(name) for name in alias_names], + aliases=[AssetAlias(name=name) for name in alias_names], extra=dict(extra_items), session=session, source_alias_names=alias_names, diff --git a/airflow/serialization/pydantic/asset.py b/airflow/serialization/pydantic/asset.py index 4cd264902091a..611730dd92e47 100644 --- a/airflow/serialization/pydantic/asset.py +++ b/airflow/serialization/pydantic/asset.py @@ -23,7 +23,7 @@ class DagScheduleAssetReferencePydantic(BaseModelPydantic): """Serializable version of the DagScheduleAssetReference ORM SqlAlchemyModel used by internal API.""" - dataset_id: int + asset_id: int dag_id: str created_at: datetime updated_at: datetime @@ -34,7 +34,7 @@ class DagScheduleAssetReferencePydantic(BaseModelPydantic): class TaskOutletAssetReferencePydantic(BaseModelPydantic): """Serializable version of the TaskOutletAssetReference ORM SqlAlchemyModel used by internal API.""" - dataset_id: int + asset_id: int dag_id: str task_id: str created_at: datetime @@ -62,13 +62,13 @@ class AssetEventPydantic(BaseModelPydantic): """Serializable representation of the AssetEvent ORM SqlAlchemyModel used by internal API.""" id: int - dataset_id: Optional[int] + asset_id: Optional[int] extra: dict source_task_id: Optional[str] source_dag_id: Optional[str] source_run_id: Optional[str] source_map_index: Optional[int] timestamp: datetime - dataset: Optional[AssetPydantic] + asset: Optional[AssetPydantic] model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True) diff --git a/airflow/serialization/pydantic/dag_run.py b/airflow/serialization/pydantic/dag_run.py index 86857452e8310..fd12ca12c0184 100644 --- a/airflow/serialization/pydantic/dag_run.py +++ b/airflow/serialization/pydantic/dag_run.py @@ -55,7 +55,7 @@ class DagRunPydantic(BaseModelPydantic): dag_hash: Optional[str] updated_at: Optional[datetime] dag: Optional[PydanticDag] - consumed_dataset_events: List[AssetEventPydantic] # noqa: UP006 + consumed_asset_events: List[AssetEventPydantic] # noqa: UP006 log_template_id: Optional[int] triggered_by: Optional[DagRunTriggeredByType] diff --git a/airflow/serialization/serialized_objects.py b/airflow/serialization/serialized_objects.py index 9f180c2a5deac..14528f62bddf1 100644 --- a/airflow/serialization/serialized_objects.py +++ b/airflow/serialization/serialized_objects.py @@ -265,7 +265,7 @@ def encode_asset_condition(var: BaseAsset) -> dict[str, Any]: def decode_asset_condition(var: dict[str, Any]) -> BaseAsset: """ - Decode a previously serialized dataset condition. + Decode a previously serialized asset condition. :meta private: """ @@ -740,8 +740,8 @@ def serialize( elif isinstance(var, LazySelectSequence): return cls.serialize(list(var)) elif isinstance(var, BaseAsset): - serialized_dataset = encode_asset_condition(var) - return cls._encode(serialized_dataset, type_=serialized_dataset.pop("__type")) + serialized_asset = encode_asset_condition(var) + return cls._encode(serialized_asset, type_=serialized_asset.pop("__type")) elif isinstance(var, SimpleTaskInstance): return cls._encode( cls.serialize(var.__dict__, strict=strict, use_pydantic_models=use_pydantic_models), diff --git a/airflow/timetables/assets.py b/airflow/timetables/assets.py index b158555590ad5..d69a8e4d80cc0 100644 --- a/airflow/timetables/assets.py +++ b/airflow/timetables/assets.py @@ -92,6 +92,6 @@ def next_dagrun_info( ) def generate_run_id(self, *, run_type: DagRunType, **kwargs: typing.Any) -> str: - if run_type != DagRunType.DATASET_TRIGGERED: + if run_type != DagRunType.ASSET_TRIGGERED: return self.timetable.generate_run_id(run_type=run_type, **kwargs) return super().generate_run_id(run_type=run_type, **kwargs) diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index aa6437118eeea..79a55e7b08dcc 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -383,7 +383,7 @@ export const $DAGDetailsResponse = { ], title: "Dag Run Timeout", }, - dataset_expression: { + asset_expression: { anyOf: [ { type: "object", @@ -392,7 +392,7 @@ export const $DAGDetailsResponse = { type: "null", }, ], - title: "Dataset Expression", + title: "Asset Expression", }, doc_md: { anyOf: [ @@ -538,7 +538,7 @@ export const $DAGDetailsResponse = { "owners", "catchup", "dag_run_timeout", - "dataset_expression", + "asset_expression", "doc_md", "start_date", "end_date", @@ -1002,13 +1002,13 @@ export const $DAGRunTypes = { type: "integer", title: "Manual", }, - dataset_triggered: { + asset_triggered: { type: "integer", - title: "Dataset Triggered", + title: "Asset Triggered", }, }, type: "object", - required: ["backfill", "scheduled", "manual", "dataset_triggered"], + required: ["backfill", "scheduled", "manual", "asset_triggered"], title: "DAGRunTypes", description: "DAG Run Types for responses.", } as const; @@ -1093,7 +1093,7 @@ export const $DagRunTriggeredByType = { export const $DagRunType = { type: "string", - enum: ["backfill", "scheduled", "manual", "dataset_triggered"], + enum: ["backfill", "scheduled", "manual", "asset_triggered"], title: "DagRunType", description: "Class with DagRun types.", } as const; diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index c795fce3e540d..83030dfb0d168 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -68,7 +68,7 @@ export type DAGDetailsResponse = { owners: Array; catchup: boolean; dag_run_timeout: string | null; - dataset_expression: { + asset_expression: { [key: string]: unknown; } | null; doc_md: string | null; @@ -174,7 +174,7 @@ export type DAGRunTypes = { backfill: number; scheduled: number; manual: number; - dataset_triggered: number; + asset_triggered: number; }; /** @@ -222,7 +222,7 @@ export type DagRunType = | "backfill" | "scheduled" | "manual" - | "dataset_triggered"; + | "asset_triggered"; /** * Serializable representation of the DagTag ORM SqlAlchemyModel used by internal API. diff --git a/airflow/utils/context.py b/airflow/utils/context.py index 4f308a6f9e060..9af1486f914e0 100644 --- a/airflow/utils/context.py +++ b/airflow/utils/context.py @@ -283,7 +283,7 @@ def __getitem__(self, key: int | str | Asset | AssetAlias) -> LazyAssetEventSele where_clause = AssetAliasModel.name == asset_alias.name elif isinstance(obj, (Asset, str)): asset = self._assets[extract_event_key(obj)] - join_clause = AssetEvent.dataset + join_clause = AssetEvent.asset where_clause = AssetModel.uri == asset.uri else: raise ValueError(key) diff --git a/airflow/utils/db.py b/airflow/utils/db.py index d6a367c4d91fe..8579be74ff4bd 100644 --- a/airflow/utils/db.py +++ b/airflow/utils/db.py @@ -96,7 +96,7 @@ class MappedClassProtocol(Protocol): "2.9.0": "1949afb29106", "2.9.2": "686269002441", "2.10.0": "22ed7efa9da2", - "3.0.0": "3a8972ecb8f9", + "3.0.0": "05234396c6fc", } diff --git a/airflow/utils/db_cleanup.py b/airflow/utils/db_cleanup.py index f98ef40c0af63..4fc387617be87 100644 --- a/airflow/utils/db_cleanup.py +++ b/airflow/utils/db_cleanup.py @@ -108,7 +108,7 @@ def readable_config(self): keep_last_filters=[column("external_trigger") == false()], keep_last_group_by=["dag_id"], ), - _TableConfig(table_name="dataset_event", recency_column_name="timestamp"), + _TableConfig(table_name="asset_event", recency_column_name="timestamp"), _TableConfig(table_name="import_error", recency_column_name="timestamp"), _TableConfig(table_name="log", recency_column_name="dttm"), _TableConfig(table_name="sla_miss", recency_column_name="timestamp"), diff --git a/airflow/utils/types.py b/airflow/utils/types.py index 80ee1d644d4d2..e012d9f1963cb 100644 --- a/airflow/utils/types.py +++ b/airflow/utils/types.py @@ -86,7 +86,7 @@ class DagRunType(str, enum.Enum): BACKFILL_JOB = "backfill" SCHEDULED = "scheduled" MANUAL = "manual" - DATASET_TRIGGERED = "dataset_triggered" + ASSET_TRIGGERED = "asset_triggered" def __str__(self) -> str: return self.value @@ -118,5 +118,5 @@ class DagRunTriggeredByType(enum.Enum): UI = "ui" # for clicking the `Trigger DAG` button TEST = "test" # for dag.test() TIMETABLE = "timetable" # for timetable based triggering - DATASET = "dataset" # for dataset_triggered run type + DATASET = "dataset" # for asset_triggered run type BACKFILL = "backfill" diff --git a/airflow/www/jest-setup.js b/airflow/www/jest-setup.js index ecf79db5cb19f..0c5ebf5c67586 100644 --- a/airflow/www/jest-setup.js +++ b/airflow/www/jest-setup.js @@ -61,7 +61,7 @@ global.defaultDagRunDisplayNumber = 245; global.filtersOptions = { // Must stay in sync with airflow/www/static/js/types/index.ts dagStates: ["success", "running", "queued", "failed"], - runTypes: ["manual", "backfill", "scheduled", "dataset_triggered"], + runTypes: ["manual", "backfill", "scheduled", "asset_triggered"], }; global.moment = moment; diff --git a/airflow/www/static/js/cluster-activity/index.test.tsx b/airflow/www/static/js/cluster-activity/index.test.tsx index 3381a5be87325..4a162e12bdf7e 100644 --- a/airflow/www/static/js/cluster-activity/index.test.tsx +++ b/airflow/www/static/js/cluster-activity/index.test.tsx @@ -37,7 +37,7 @@ const mockHistoricalMetricsData = { dag_run_states: { failed: 0, queued: 0, running: 0, success: 306 }, dag_run_types: { backfill: 0, - dataset_triggered: 0, + asset_triggered: 0, manual: 14, scheduled: 292, }, diff --git a/airflow/www/static/js/components/DatasetEventCard.tsx b/airflow/www/static/js/components/DatasetEventCard.tsx index 9dd1ee91e3731..d3931bf3c1de9 100644 --- a/airflow/www/static/js/components/DatasetEventCard.tsx +++ b/airflow/www/static/js/components/DatasetEventCard.tsx @@ -69,7 +69,7 @@ const DatasetEventCard = ({ - {assetEvent.datasetUri && assetEvent.datasetUri !== selectedUri ? ( + {assetEvent.assetUri && assetEvent.assetUri !== selectedUri ? ( - {assetEvent.datasetUri} + {assetEvent.assetUri} ) : ( - {assetEvent.datasetUri} + {assetEvent.assetUri} )} diff --git a/airflow/www/static/js/components/RunTypeIcon.tsx b/airflow/www/static/js/components/RunTypeIcon.tsx index 19e59babd3157..5d5be01663385 100644 --- a/airflow/www/static/js/components/RunTypeIcon.tsx +++ b/airflow/www/static/js/components/RunTypeIcon.tsx @@ -42,7 +42,7 @@ const DagRunTypeIcon = ({ runType, ...rest }: Props) => { return ; case "scheduled": return ; - case "dataset_triggered": + case "asset_triggered": return ; default: return null; diff --git a/airflow/www/static/js/dag/details/Header.tsx b/airflow/www/static/js/dag/details/Header.tsx index 172443ca47b36..f881a48213b9d 100644 --- a/airflow/www/static/js/dag/details/Header.tsx +++ b/airflow/www/static/js/dag/details/Header.tsx @@ -66,7 +66,7 @@ const Header = ({ mapIndex }: Props) => { runId.includes("manual__") || runId.includes("scheduled__") || runId.includes("backfill__") || - runId.includes("dataset_triggered__") ? ( + runId.includes("asset_triggered__") ? ( ) : ( runId diff --git a/airflow/www/static/js/dag/details/dag/Dag.tsx b/airflow/www/static/js/dag/details/dag/Dag.tsx index 9643064d2a3b1..cd14ed2cd42fe 100644 --- a/airflow/www/static/js/dag/details/dag/Dag.tsx +++ b/airflow/www/static/js/dag/details/dag/Dag.tsx @@ -242,14 +242,14 @@ const Dag = () => { - {!!dagDetailsData.datasetExpression && ( + {!!dagDetailsData.assetExpression && (
Dataset Conditions
                         {JSON.stringify(
-                          dagDetailsData.datasetExpression,
+                          dagDetailsData.assetExpression,
                           null,
                           2
                         )}
diff --git a/airflow/www/static/js/dag/details/dagRun/DatasetTriggerEvents.tsx b/airflow/www/static/js/dag/details/dagRun/DatasetTriggerEvents.tsx
index 6deedb073e8d9..0b9d34af1f1e8 100644
--- a/airflow/www/static/js/dag/details/dagRun/DatasetTriggerEvents.tsx
+++ b/airflow/www/static/js/dag/details/dagRun/DatasetTriggerEvents.tsx
@@ -52,7 +52,7 @@ const DatasetTriggerEvents = ({ runId }: Props) => {
       },
       {
         Header: "Dataset",
-        accessor: "datasetUri",
+        accessor: "assetUri",
       },
       {
         Header: "Source Task Instance",
diff --git a/airflow/www/static/js/dag/details/dagRun/index.tsx b/airflow/www/static/js/dag/details/dagRun/index.tsx
index 05679a598faa9..97ad33dcb86ce 100644
--- a/airflow/www/static/js/dag/details/dagRun/index.tsx
+++ b/airflow/www/static/js/dag/details/dagRun/index.tsx
@@ -58,9 +58,7 @@ const DagRun = ({ runId }: Props) => {
         initialValue={note}
         key={dagId + runId}
       />
-      {runType === "dataset_triggered" && (
-        
-      )}
+      {runType === "asset_triggered" && }
       
     
   );
diff --git a/airflow/www/static/js/dag/details/graph/index.tsx b/airflow/www/static/js/dag/details/graph/index.tsx
index b9511b6664181..2236c3e37b074 100644
--- a/airflow/www/static/js/dag/details/graph/index.tsx
+++ b/airflow/www/static/js/dag/details/graph/index.tsx
@@ -58,13 +58,13 @@ interface Props {
 
 const dagId = getMetaValue("dag_id");
 
-type DatasetExpression = {
-  all?: (string | DatasetExpression)[];
-  any?: (string | DatasetExpression)[];
+type AssetExpression = {
+  all?: (string | AssetExpression)[];
+  any?: (string | AssetExpression)[];
 };
 
 const getUpstreamDatasets = (
-  datasetExpression: DatasetExpression,
+  assetExpression: AssetExpression,
   firstChildId: string,
   level = 0
 ) => {
@@ -72,16 +72,16 @@ const getUpstreamDatasets = (
   let nodes: DepNode[] = [];
   let type: DepNode["value"]["class"] | undefined;
   const datasetIds: string[] = [];
-  let nestedExpression: DatasetExpression | undefined;
-  if (datasetExpression?.any) {
+  let nestedExpression: AssetExpression | undefined;
+  if (assetExpression?.any) {
     type = "or-gate";
-    datasetExpression.any.forEach((de) => {
+    assetExpression.any.forEach((de) => {
       if (typeof de === "string") datasetIds.push(de);
       else nestedExpression = de;
     });
-  } else if (datasetExpression?.all) {
+  } else if (assetExpression?.all) {
     type = "and-gate";
-    datasetExpression.all.forEach((de) => {
+    assetExpression.all.forEach((de) => {
       if (typeof de === "string") datasetIds.push(de);
       else nestedExpression = de;
     });
@@ -159,7 +159,7 @@ const Graph = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
 
   const { nodes: upstreamDatasetNodes, edges: upstreamDatasetEdges } =
     getUpstreamDatasets(
-      dagDetails.datasetExpression as DatasetExpression,
+      dagDetails.assetExpression as AssetExpression,
       data?.nodes?.children?.[0]?.id ?? ""
     );
 
@@ -212,17 +212,17 @@ const Graph = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
 
     // Check if there is a dataset event even though we did not find a dataset
     downstreamAssetEvents.forEach((de) => {
-      const hasNode = datasetNodes.find((node) => node.id === de.datasetUri);
-      if (!hasNode && de.sourceTaskId && de.datasetUri) {
+      const hasNode = datasetNodes.find((node) => node.id === de.assetUri);
+      if (!hasNode && de.sourceTaskId && de.assetUri) {
         datasetEdges.push({
           sourceId: de.sourceTaskId,
-          targetId: de.datasetUri,
+          targetId: de.assetUri,
         });
         datasetNodes.push({
-          id: de.datasetUri,
+          id: de.assetUri,
           value: {
             class: "asset",
-            label: de.datasetUri,
+            label: de.assetUri,
           },
         });
       }
diff --git a/airflow/www/static/js/dag/details/graph/utils.ts b/airflow/www/static/js/dag/details/graph/utils.ts
index d548b57d85466..c15dc6dcf0ce6 100644
--- a/airflow/www/static/js/dag/details/graph/utils.ts
+++ b/airflow/www/static/js/dag/details/graph/utils.ts
@@ -93,7 +93,7 @@ export const flattenNodes = ({
         },
         assetEvent:
           node.value.class === "asset"
-            ? assetEvents?.find((de) => de.datasetUri === node.value.label)
+            ? assetEvents?.find((de) => de.assetUri === node.value.label)
             : undefined,
         ...node.value,
       },
diff --git a/airflow/www/static/js/dag/details/taskInstance/DatasetUpdateEvents.tsx b/airflow/www/static/js/dag/details/taskInstance/DatasetUpdateEvents.tsx
index 110f462b69386..7d01794bfc0ac 100644
--- a/airflow/www/static/js/dag/details/taskInstance/DatasetUpdateEvents.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/DatasetUpdateEvents.tsx
@@ -55,7 +55,7 @@ const DatasetUpdateEvents = ({ runId, taskId }: Props) => {
       },
       {
         Header: "Dataset",
-        accessor: "datasetUri",
+        accessor: "assetUri",
       },
       {
         Header: "Triggered Runs",
diff --git a/airflow/www/static/js/datasetUtils.js b/airflow/www/static/js/datasetUtils.js
index 5ba5e3fe584fa..92d1a5c705f88 100644
--- a/airflow/www/static/js/datasetUtils.js
+++ b/airflow/www/static/js/datasetUtils.js
@@ -23,13 +23,13 @@ import { getMetaValue } from "./utils";
 
 export function openDatasetModal(dagId, summary, nextDatasets, error) {
   const assetEvents = nextDatasets.events || [];
-  const expression = nextDatasets.dataset_expression;
+  const expression = nextDatasets.asset_expression;
   const datasetsUrl = getMetaValue("datasets_url");
-  $("#dataset_expression").empty();
+  $("#asset_expression").empty();
   $("#datasets_tbody").empty();
   $("#datasets_error").hide();
   $("#dag_id").text(dagId);
-  $("#dataset_expression").text(JSON.stringify(expression, null, 2));
+  $("#asset_expression").text(JSON.stringify(expression, null, 2));
   $("#datasetNextRunModal").modal({});
   if (summary) $("#next_run_summary").text(summary);
   assetEvents.forEach((d) => {
diff --git a/airflow/www/static/js/datasets/AssetEvents.tsx b/airflow/www/static/js/datasets/AssetEvents.tsx
index ccddf4ace885d..60aed55196f53 100644
--- a/airflow/www/static/js/datasets/AssetEvents.tsx
+++ b/airflow/www/static/js/datasets/AssetEvents.tsx
@@ -65,7 +65,7 @@ const Events = ({ assetId, showLabel }: Props) => {
       },
       {
         Header: "Dataset",
-        accessor: "datasetUri",
+        accessor: "assetUri",
       },
       {
         Header: "Source Task Instance",
diff --git a/airflow/www/static/js/types/api-generated.ts b/airflow/www/static/js/types/api-generated.ts
index ef45dbd3b57b6..31b91600573f8 100644
--- a/airflow/www/static/js/types/api-generated.ts
+++ b/airflow/www/static/js/types/api-generated.ts
@@ -267,6 +267,7 @@ export interface paths {
      * Get asset for a dag run.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamDatasetEvents"
      */
     get: operations["get_upstream_asset_events"];
     parameters: {
@@ -299,19 +300,25 @@ export interface paths {
      * Get a queued asset event for a DAG.
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}"
      */
     get: operations["get_dag_asset_queued_event"];
     /**
      * Delete a queued Asset event for a DAG.
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}"
      */
     delete: operations["delete_dag_asset_queued_event"];
     parameters: {
       path: {
         /** The DAG ID. */
         dag_id: components["parameters"]["DAGID"];
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
     };
@@ -321,12 +328,14 @@ export interface paths {
      * Get queued Asset events for a DAG.
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent"
      */
     get: operations["get_dag_asset_queued_events"];
     /**
      * Delete queued Asset events for a DAG.
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent"
      */
     delete: operations["delete_dag_asset_queued_events"];
     parameters: {
@@ -355,17 +364,23 @@ export interface paths {
      * Get queued Asset events for an Asset
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}"
      */
     get: operations["get_asset_queued_events"];
     /**
      * Delete queued Asset events for a Asset.
      *
      * *New in version 2.9.0*
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}"
      */
     delete: operations["delete_asset_queued_events"];
     parameters: {
       path: {
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
     };
@@ -783,22 +798,39 @@ export interface paths {
     get: operations["get_dag_warnings"];
   };
   "/assets": {
+    /** *Changed in 3.0.0*: The endpoint value was renamed from "/datasets" */
     get: operations["get_assets"];
   };
   "/assets/{uri}": {
-    /** Get an asset by uri. */
+    /**
+     * Get an asset by uri.
+     *
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/{uri}"
+     */
     get: operations["get_asset"];
     parameters: {
       path: {
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
     };
   };
   "/assets/events": {
-    /** Get asset events */
+    /**
+     * Get asset events
+     *
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events"
+     */
     get: operations["get_asset_events"];
-    /** Create asset event */
+    /**
+     * Create asset event
+     *
+     * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events"
+     */
     post: operations["create_asset_event"];
   };
   "/config": {
@@ -1193,8 +1225,12 @@ export interface components {
       data_interval_end?: string | null;
       /** Format: date-time */
       last_scheduling_decision?: string | null;
-      /** @enum {string} */
-      run_type?: "backfill" | "manual" | "scheduled" | "dataset_triggered";
+      /**
+       * @description *Changed in 3.0.0*: The asset_triggered value was renamed from dataset_triggered.
+       *
+       * @enum {string}
+       */
+      run_type?: "backfill" | "manual" | "scheduled" | "asset_triggered";
       state?: components["schemas"]["DagState"];
       external_trigger?: boolean;
       /**
@@ -1647,8 +1683,12 @@ export interface components {
        */
       start_date?: string | null;
       dag_run_timeout?: components["schemas"]["TimeDelta"] | null;
-      /** @description Nested asset any/all conditions */
-      dataset_expression?: { [key: string]: unknown } | null;
+      /**
+       * @description Nested asset any/all conditions
+       *
+       * *Changed in 3.0.0*: The asset_expression value was renamed from dataset_expression.
+       */
+      asset_expression?: { [key: string]: unknown } | null;
       doc_md?: string | null;
       default_view?: string | null;
       /**
@@ -1852,6 +1892,7 @@ export interface components {
      * @description An asset item.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from Dataset.
      */
     Asset: {
       /** @description The asset id */
@@ -1871,6 +1912,7 @@ export interface components {
      * @description An asset reference to an upstream task.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from TaskOutletDatasetReference.
      */
     TaskOutletAssetReference: {
       /** @description The DAG ID that updates the asset. */
@@ -1886,6 +1928,7 @@ export interface components {
      * @description An asset reference to a downstream DAG.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from DagScheduleDatasetReference.
      */
     DagScheduleAssetReference: {
       /** @description The DAG ID that depends on the asset. */
@@ -1899,20 +1942,31 @@ export interface components {
      * @description A collection of assets.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from DatasetCollection.
      */
     AssetCollection: {
+      /** @description *Changed in 3.0.0*: This was renamed from datasets. */
       assets?: components["schemas"]["Asset"][];
     } & components["schemas"]["CollectionInfo"];
     /**
      * @description An asset event.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from DatasetEvent.
      */
     AssetEvent: {
-      /** @description The asset id */
-      dataset_id?: number;
-      /** @description The URI of the asset */
-      dataset_uri?: string;
+      /**
+       * @description The asset id
+       *
+       * *Changed in 3.0.0*: This was renamed from dataset_id.
+       */
+      asset_id?: number;
+      /**
+       * @description The URI of the asset
+       *
+       * *Changed in 3.0.0*: This was renamed from dataset_uri.
+       */
+      asset_uri?: string;
       /** @description The asset event extra */
       extra?: { [key: string]: unknown } | null;
       /** @description The DAG ID that updated the asset. */
@@ -1927,8 +1981,13 @@ export interface components {
       /** @description The asset event creation time */
       timestamp?: string;
     };
+    /** @description *Changed in 3.0.0*: This was renamed from CreateDatasetEvent. */
     CreateAssetEvent: {
-      /** @description The URI of the asset */
+      /**
+       * @description The URI of the asset
+       *
+       * *Changed in 3.0.0*: This was renamed from dataset_uri.
+       */
       asset_uri: string;
       /** @description The asset event extra */
       extra?: { [key: string]: unknown } | null;
@@ -1988,8 +2047,10 @@ export interface components {
      * @description A collection of asset events.
      *
      * *New in version 2.4.0*
+     * *Changed in 3.0.0*: This was renamed from DatasetEventCollection.
      */
     AssetEventCollection: {
+      /** @description *Changed in 3.0.0*: This was renamed from dataset_events. */
       asset_events?: components["schemas"]["AssetEvent"][];
     } & components["schemas"]["CollectionInfo"];
     /** @description The option of configuration. */
@@ -2543,7 +2604,11 @@ export interface components {
     EventLogID: number;
     /** @description The import error ID. */
     ImportErrorID: number;
-    /** @description The encoded Asset URI */
+    /**
+     * @description The encoded Asset URI
+     *
+     * *Changed in 3.0.0*: This was renamed from DatasetURI.
+     */
     AssetURI: string;
     /** @description The pool name. */
     PoolName: string;
@@ -2623,7 +2688,11 @@ export interface components {
      * *New in version 2.2.0*
      */
     FilterTags: string[];
-    /** @description The Asset ID that updated the asset. */
+    /**
+     * @description The Asset ID that updated the asset.
+     *
+     * *Changed in 3.0.0*: This was renamed from FilterDatasetID.
+     */
     FilterAssetID: number;
     /** @description The DAG ID that updated the asset. */
     FilterSourceDAGID: string;
@@ -3585,6 +3654,7 @@ export interface operations {
    * Get asset for a dag run.
    *
    * *New in version 2.4.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamDatasetEvents"
    */
   get_upstream_asset_events: {
     parameters: {
@@ -3644,13 +3714,18 @@ export interface operations {
    * Get a queued asset event for a DAG.
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}"
    */
   get_dag_asset_queued_event: {
     parameters: {
       path: {
         /** The DAG ID. */
         dag_id: components["parameters"]["DAGID"];
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
       query: {
@@ -3674,13 +3749,18 @@ export interface operations {
    * Delete a queued Asset event for a DAG.
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent/{uri}"
    */
   delete_dag_asset_queued_event: {
     parameters: {
       path: {
         /** The DAG ID. */
         dag_id: components["parameters"]["DAGID"];
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
       query: {
@@ -3701,6 +3781,7 @@ export interface operations {
    * Get queued Asset events for a DAG.
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent"
    */
   get_dag_asset_queued_events: {
     parameters: {
@@ -3729,6 +3810,7 @@ export interface operations {
    * Delete queued Asset events for a DAG.
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/dags/{dag_id}/datasets/queuedEvent"
    */
   delete_dag_asset_queued_events: {
     parameters: {
@@ -3774,11 +3856,16 @@ export interface operations {
    * Get queued Asset events for an Asset
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}"
    */
   get_asset_queued_events: {
     parameters: {
       path: {
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
       query: {
@@ -3802,11 +3889,16 @@ export interface operations {
    * Delete queued Asset events for a Asset.
    *
    * *New in version 2.9.0*
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/assets/queuedEvent/{uri}"
    */
   delete_asset_queued_events: {
     parameters: {
       path: {
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
       query: {
@@ -5040,6 +5132,7 @@ export interface operations {
       403: components["responses"]["PermissionDenied"];
     };
   };
+  /** *Changed in 3.0.0*: The endpoint value was renamed from "/datasets" */
   get_assets: {
     parameters: {
       query: {
@@ -5075,11 +5168,19 @@ export interface operations {
       403: components["responses"]["PermissionDenied"];
     };
   };
-  /** Get an asset by uri. */
+  /**
+   * Get an asset by uri.
+   *
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/{uri}"
+   */
   get_asset: {
     parameters: {
       path: {
-        /** The encoded Asset URI */
+        /**
+         * The encoded Asset URI
+         *
+         * *Changed in 3.0.0*: This was renamed from DatasetURI.
+         */
         uri: components["parameters"]["AssetURI"];
       };
     };
@@ -5095,7 +5196,11 @@ export interface operations {
       404: components["responses"]["NotFound"];
     };
   };
-  /** Get asset events */
+  /**
+   * Get asset events
+   *
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events"
+   */
   get_asset_events: {
     parameters: {
       query: {
@@ -5110,7 +5215,11 @@ export interface operations {
          * *New in version 2.1.0*
          */
         order_by?: components["parameters"]["OrderBy"];
-        /** The Asset ID that updated the asset. */
+        /**
+         * The Asset ID that updated the asset.
+         *
+         * *Changed in 3.0.0*: This was renamed from FilterDatasetID.
+         */
         asset_id?: components["parameters"]["FilterAssetID"];
         /** The DAG ID that updated the asset. */
         source_dag_id?: components["parameters"]["FilterSourceDAGID"];
@@ -5134,7 +5243,11 @@ export interface operations {
       404: components["responses"]["NotFound"];
     };
   };
-  /** Create asset event */
+  /**
+   * Create asset event
+   *
+   * *Changed in 3.0.0*: The endpoint value was renamed from "/datasets/events"
+   */
   create_asset_event: {
     responses: {
       /** Success. */
diff --git a/airflow/www/static/js/types/index.ts b/airflow/www/static/js/types/index.ts
index 5ae6868647994..4c916269840e5 100644
--- a/airflow/www/static/js/types/index.ts
+++ b/airflow/www/static/js/types/index.ts
@@ -54,7 +54,7 @@ interface Dag {
 
 interface DagRun {
   runId: string;
-  runType: "manual" | "backfill" | "scheduled" | "dataset_triggered";
+  runType: "manual" | "backfill" | "scheduled" | "asset_triggered";
   state: RunState;
   executionDate: string;
   dataIntervalStart: string;
diff --git a/airflow/www/templates/airflow/dataset_next_run_modal.html b/airflow/www/templates/airflow/asset_next_run_modal.html
similarity index 97%
rename from airflow/www/templates/airflow/dataset_next_run_modal.html
rename to airflow/www/templates/airflow/asset_next_run_modal.html
index 6371cd17b2030..676d3dd81b92b 100644
--- a/airflow/www/templates/airflow/dataset_next_run_modal.html
+++ b/airflow/www/templates/airflow/asset_next_run_modal.html
@@ -31,7 +31,7 @@ 
                     info
                   
             
-            
+            
               {% if dag.dag_id in asset_triggered_next_run_info %}
                 {%- with asset_info = asset_triggered_next_run_info[dag.dag_id] -%}
                   
                     
{% if asset_info.total == 1 -%} On {{ asset_info.uri[0:40] + '…' if asset_info.uri and asset_info.uri|length > 40 else asset_info.uri|default('', true) }} {%- else -%} - {{ asset_info.ready }} of {{ asset_info.total }} datasets updated + {{ asset_info.ready }} of {{ asset_info.total }} assets updated {%- endif %}
@@ -460,8 +460,8 @@

{{ page_title }}

- - {{ dataset_next_run_modal(id='dataset-next-run-modal') }} + + {{ asset_next_run_modal(id='asset-next-run-modal') }} {% endblock %} {% block tail %} diff --git a/airflow/www/webpack.config.js b/airflow/www/webpack.config.js index 45d3f90a4d4e3..633d9a68109c4 100644 --- a/airflow/www/webpack.config.js +++ b/airflow/www/webpack.config.js @@ -77,7 +77,7 @@ const config = { toggleTheme: `${JS_DIR}/toggle_theme.js`, grid: `${JS_DIR}/dag/index.tsx`, clusterActivity: `${JS_DIR}/cluster-activity/index.tsx`, - datasets: `${JS_DIR}/datasets/index.tsx`, + assets: `${JS_DIR}/assets/index.tsx`, trigger: `${JS_DIR}/trigger.js`, variableEdit: `${JS_DIR}/variable_edit.js`, }, diff --git a/docs/apache-airflow-providers-fab/auth-manager/access-control.rst b/docs/apache-airflow-providers-fab/auth-manager/access-control.rst index 914635942b7c1..f4b5fa5861d8c 100644 --- a/docs/apache-airflow-providers-fab/auth-manager/access-control.rst +++ b/docs/apache-airflow-providers-fab/auth-manager/access-control.rst @@ -264,8 +264,8 @@ Set Task Instance as failed DAGs.can_edit Set Task Instance as success DAGs.can_edit User Set Task Instance as up_for_retry DAGs.can_edit User Autocomplete DAGs.can_read Viewer -Show Dataset menu Assets.menu_access Viewer -Show Datasets Assets.can_read Viewer +Show Asset menu Assets.menu_access Viewer +Show Assets Assets.can_read Viewer Show Docs menu Docs.menu_access Viewer Show Documentation menu Documentation.menu_access Viewer Show Jobs menu Jobs.menu_access Viewer diff --git a/docs/apache-airflow-providers-google/operators/cloud/dataplex.rst b/docs/apache-airflow-providers-google/operators/cloud/dataplex.rst index 788024ce0bbea..cbeb5eafcd037 100644 --- a/docs/apache-airflow-providers-google/operators/cloud/dataplex.rst +++ b/docs/apache-airflow-providers-google/operators/cloud/dataplex.rst @@ -284,12 +284,12 @@ To delete a zone you can use: :start-after: [START howto_dataplex_delete_zone_operator] :end-before: [END howto_dataplex_delete_zone_operator] -Create a asset --------------- +Create an asset +--------------- Before you create a Dataplex asset you need to define its body. -For more information about the available fields to pass when creating a asset, visit `Dataplex create asset API. `__ +For more information about the available fields to pass when creating an asset, visit `Dataplex create asset API. `__ A simple asset configuration can look as followed: @@ -309,10 +309,10 @@ With this configuration we can create the asset: :start-after: [START howto_dataplex_create_asset_operator] :end-before: [END howto_dataplex_create_asset_operator] -Delete a asset --------------- +Delete an asset +--------------- -To delete a asset you can use: +To delete an asset you can use: :class:`~airflow.providers.google.cloud.operators.dataplex.DataplexDeleteAssetOperator` diff --git a/docs/apache-airflow/administration-and-deployment/logging-monitoring/metrics.rst b/docs/apache-airflow/administration-and-deployment/logging-monitoring/metrics.rst index 7ce9b9b765a9c..670d57d563c94 100644 --- a/docs/apache-airflow/administration-and-deployment/logging-monitoring/metrics.rst +++ b/docs/apache-airflow/administration-and-deployment/logging-monitoring/metrics.rst @@ -205,7 +205,7 @@ Name Descripti ``asset.updates`` Number of updated assets ``asset.orphaned`` Number of assets marked as orphans because they are no longer referenced in DAG schedule parameters or task outlets -``asset.triggered_dagruns`` Number of DAG runs triggered by a asset update +``asset.triggered_dagruns`` Number of DAG runs triggered by an asset update ====================================================================== ================================================================ Gauges diff --git a/docs/apache-airflow/img/asset-scheduled-dags.png b/docs/apache-airflow/img/asset-scheduled-dags.png index 35593fdcb5a5ac9b64a3de256993045b653f6a82..919ef21c4b9a400026e48122e80b6327372eabe8 100644 GIT binary patch literal 96199 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 z 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 literal 77225 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%0AR12IT=f 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 Ofnoa z2ecd>POU8}LQbc$wylkiBLXZ9^MqJ5LGh*X)E*Uw=Ff*@n2w7pB@X1jM|yqo1?M#L@7 zO25C?x}Qae3x)&;8Sr(*` z3Opt?WQ3rLGm)zdFL+%yp`j2Dyv+@ydX@o~NKmcJpbL>?+w~~l@Z<7yvXrol2_vcu zp}j~D2POI}fk!lUc*;OKS8G0M>xP16gtpMTDYLk5CX?cx&60;sPT2GI*_nJh-WZRB zsR>Lp+duEKgFKWs;>I}bgAuFN4bZ@LD2@|l%T7NcmbxWjaLB`g9Gv|G~TmXAQvxY z>x53nFK@TilD_bpQQ{8UrYr9RZe}x=pXei(bGCUZK6pn55bJU6kl(0T*5kCfM*MLa>7VY_*#*9X(0yWa~p;zzUf^9 zsfwo?tWFay#;qO$m?5`*z`md3n=e*ct?8Cs)6Qs&=BhmiF=276&vI0^kt(>3in`K% zI}ZhAP0;SyEtlA`^f+~zWPvY!zV<-rWU*p63vgt%vx(P+1Mi+%I05j0h}5KhIi24! z1f4J_<`S;-Fe%;$Y(5r2E}5wQPba>97sEjb$~H$EBGlr5BdYpR=>#pLr64S_1lQR3 z>6&<+(>2Zt5Hd;=x4XI9-#H1f(Iz1WOV*`cRArK^rm zD#Y|T^z(ZIYn8Ou;vq1x>$h&)H4z)M&G=S2GV*$Lp1j(#+L-(h(8y8i9=wOMFZBdM zCy3xuruz$8H3}2Hz<@j|Y4$s?9Fx=m^I;XO#h?ve6P#jhV?NM;94o1+PIkoYamYP| zh0eszy?hG<7@x>1EtV!#&;q7a$GS9I2o6@j zNLR=;doPdfmfG3%zNVj#Mut_Vqnpj9cm2&5iMlXlxqyhyG*I!Ppo=3`KvsL8DqEe8 zG=Xf=lof8lU5#0@t}`elUj3V-lQGQeSi?6qa_DSfx;)hBSAy95bA$E3B_pVx$YSt(wJ~fFHhf)haS}b1e*>_i4Id;ee&x+UQG^W`(W5ri; znh|sFUP__-uSfSkeB!U(63r#v;IhDgSN_j6{?CW>Q^Q;0(2`tV%j@HjM@_yuD=2%J zlTF!J@v(@7lO`?;TvLsh9rF58;#3RMZpcYLOK^GG!?XYM8@MbyY2ds#i!>bFM}NL3 z|2&re?FDahrb{*xVRPoI)m=5_Wf#%{Ze<8L{QD=oZxKuYSBcxF3)8oppZ@##{^=7l z{|HM4fQ8ejr;$j<|=5&VUr@0No@Qb&eH(CAt_40 z8%1(8&9M0k^HYQ6{~}(o!|?UcW((zK_2^719+41wrqGTty>!_dkM&WH?fr^ zFk_YmbLuE2!+>`}TMQr6AkT%8!|@Hnl{|B!IP-cS9Job9cHTF2O7wmSwLuz;s{2c0@jhrD1ShV{`1MT(6gvzS5UFB`pLg{>vGot%FFmLlW5?t zuw*mbxj-&<5KPLCX#eq%I?|7R-Zo-O}=*i7Z1+#Rf<*{oJBCEIaKel z--!5|v!Yy$ihPlyk=ZMBbQZn^3~sxZt;z`{ohpwxDX!8nTb*uj2zzwLjc(77a5+mX z=NA*kLi#W=*sgSJ*v*YZ&u#x&G_t0@)b!=3WwAY%Mmf7K++k(#i>0D{1YH=KW9yPK+o+QOnSPns=SD03Rqeqx?@j(Xti}st;D?d$d7{6W#BO> zyY@+BynFXU2Xn^SdIexDU)u++c#Hym8QkXoP{RIm1-8oIR@wGvVPSR*prUTY z))QX#T`{80z2bejKWWqg=^#aQpy3YQ1AMv1F*%@(;K)M-+`a+V_`-SiL|)SrEXL%h z4%nS@#T}d#$JeNxUN;ANFgGr+I@JTeDHFJ+(dIXnfZC&1qIuaCcAzQQzm-g6QCLN* z$OAfkA6+YStbK5F;8sp}3mIN{Wns__c1x%T=Y8)v@ zH-}SjP?ObSmL*|eIS*DMLR{**N_OboPHas3Q?uxX8S+ufJm~@SiX4%ewpq&A$}-zA zWVo#N!1G`D>Oyok0l;{LMvQQ0$#0A>nDGz%Ct?3j4|D5f+=nbphrl6CipL3Bz6}T< zE{zMWz`Y@#9hi>ih)#pYXI_TfeaP%pnqh|uRvhHoxKJGo$vJF%Or$ub;HunHoRM{7uF-%GSNqi z!;!76Z~gYbB*w_LP*y;<&{#{y5^3rZ?G)m@rrj#yWBA->zLQS^>RgLw!GqB`Eo>J+ zUr%4w-Ku?qC&bKSaVsSvt?HfG4&KL2?`#WxT z&<+0**~RvF{Ud{0j|1-hDG~GZ795Z{oK6;eXn*kG#)GDoE6amxI`8$cwXt_83D_g0 zQx9vj>T16zF~aC~zBj#vk;@;en*g70?nmM;zugdYeogkHx$S4=SKHl@qED|mR{vN$ z_i>Lr?ph^Ggg|uYcj8y(6obXW`*zXCKyE3Ui@}^;tDj12CyMp>yLFeh97S*a^1Q#u zmH-@hHK3gOtKgTH?lbxXm`=fm|od z8;|e<$;aVCQ0+&6w5t`UXO!sd6NI@dw1$!&7d7TwycQ(v&U8&kccz|M>H;+}pW2%X zl*I!exirIDaatcSZP9aBi#h2{7{o*Bp#k=C0H*ljyCr=aRdd&E7~ zP`Sgvp4*R2x0g3h#4tGXsHom+1P>)o=>f^6w+dZ<@ zYW@@N)HcG)Q5#u?PFfRad~x-UEB%SO_qdRdU>Q2){o0>fLg}4;ig~YcvlS^+@|^eJ zg{8e3rK#Q0lB&A>^`NqNdXkp8$cZZH^Rac?rGerHU}ZN{k=&W86BU~t?OEc6 zt@5fOAXAWu0v#PSz%c zf3n~QiEzU82i(q-NaqBREm)@s@b+Jl<>`U;9)isjOGym@wnMEz8V~{I?Oo!|sX@H{ z;H{MQY&0uJCI8udRmjA|19r7-NjQFZ)Sv3FU}gf|N}J<+%zonb!z*6#Prz1MoYWu@ zN~2ctg6f`TgV9o_Pi!m8O)v4_hHg=!rCMr(Z+Ea4HyZb8;EhmJ4O+JI%ZZYp&=#w= z@dE#Qx5*-#rv@$Scr4L36cOhuIQ~U!Z@X-*7}Zz5yc~Lw4Sc~Kfvp@m_*wY9@vlX| z>T$HhSkZ@>2eX)n;_Zrw=hCUe-}_6rirH{S-dyN?l*cn1jDb z`&fPi43_6NDDOSutOgppL%TjE;Q~+<5U-p7De(0uIuU)~r|1|$QtC)gLd{v~J>Iwn zi`jqKkZO%`0)}4ZH8G7RQ7*hjTQfiKv|*UTj88YV;@r`wiei@?qh)>Ec95JJM)rvx z0yTH5C>nY1u^(y0z@YnH_A5uh+YqziT*I6*0FL8p!$2%XD2LyDRjc>s)Wx>k%LvfP zLv?rJJg5rM;N)Ks1e#tU9=Iuu*Mq$lW#BFZvHmbX>3<&vm%5vB1j-Fuf}NJahzjG| zx;D`Gv5DJ$F?5oD%XGZiIjsl3ldnwe@~%Dv+MD7!@xW!;{z@DDzT7d|B@y$H=PoN* z^!x)A^eIv-UdO)VEF)hXfNa$5DF>74TRJ4D#RTN>;xP(9 zumO9jDEStOiLK9g^>E$2`^

    xFSJPadpZ6x_oNN9|A+dEhW|fN`)`?F>0H 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
    2LWy;1QTaprAu<$@Y!6LQH-%x1rn399q_S zOxeyhxxy6hjTVLcrcSFZ#_*+KA8Z&@a-JMaS(;z>YgKNU&~l6BF^qkyt2RgLV7UTt zLGvO|PSFkF{`eC-|12e0!mYBoe{~tCTKnCMDRa0E7}t<&-~eiy!ULc|e%HFbjUv3Y!K=9nu@`x-HQ{tgIRe zfv&3OvtK_D%O&f^t#msS_pSn1dAYEheN;)&-Ln62@FA^(ca{XYv<#TYMGo?yhle-> zrt8 z;bti%0HUbkR+RTqcJBTB?De{NeAv1C(9a8I8XoNdst+SF-EaB)bF?x848Vk$8RUiZ>8Cks#UYE$qSZ!pAjf5YII=PNcSBaX}Z z`Pd|mS)kg1#r2svNHVZJ281kf$%%cV5aO!fe{n=7Zf*Mlxm5xikGQJ|#%rEY#cEJ1 z9}Wl2WE#t)XKK+F`%P0iqaHkBkFaYXZ}Ro+rZYBFAMDoXEgCmK1xgsTxoO?GpmuKP-8rl2TFw?`M zO~7M6_H$((xOo$$WP{w37qu(;dx==`rTXIC8<;v`nyG zBwqboML&r%(o+KHUdIc@-X8}GA=2&grY5R1?9!w4znOIgoi#3kG$IIh#q&7Fln+YF zTt}W-q6@_?Qy8J$#yam^X8kM)fI7+VjI<;Z&t_D`Ho|BrDR1Q~1g4!mE3&)k1!n2! zgK!f}zVd*dW<680rG1`q)x%0iUu)ujSS}zPG~$LyAKn%0JNGTKlTMK01xvL@Q0i;x zmt+E#)zS1|^RR&;wK7Fq;Zx4`CQjt3awe55K?S2$!))pC(uI?FUW!?CATe&7Qfh>& zRLnm}JSFv6xa`R>znV!9_-?Bc#wx{6DRDRtQ4aBq75f%=BCZ8MSFw-B&rZi!&U>WQ zw;==C!e_B2^bV!hC#beP*8RV#1iCi#XNNkh8n|`eFGnuy+54MeqE9cM5xq{IXv}|= zk_TmiT=t>^kNp08ppmy>W{}1*0pCg9G5T8+}Hsy2&eJIMdt&)!IwWH2t1gsz;aa81i zRhu)sd5uB-5HFd&RjnxR?{F!46A*?xvg+VjUI>&V-Xj&q`!Y@u&ZY4?=)UTVB`{`;)y@8#LaCJ@2g+{xlEOOZv@ z!xXeko@R5v zOeYpL5DhUo{+x=v>We|Zpw+b@+vV<#fu~zzIpGBK3hLP$Rp0xAa>e#?_a-z9PJsUf z_Kj5zE#%|Xno*-DmW17N%$0`4{TjljA%Haaon3Vyj>s)YSPltNDi%^=eF$19nRvbU zTj0bfPZ7C21gIG#F330y`e|y9;A&|oyqqjae+_m2Xqr~U%+8*-6~Qg%`B^>43ox=M zOt32bGnbYk_w&MZrkz*vk?YohVYfGgiPA{TiBkoY3kR&oM4J*8oR4xL{`Pxl7+)R$ zbB|J4=)bIXnHxd_n-1dl>>qfrDVKt9$V;wK?*($2Gm$>DTp(WzFf`7SCa z``8d{qwPdK*ta&WE5m5$q|e!Mmfcx6)1VTkSL*2&{s)^%Ma|PknB){qnpUO4&-j#m+mff(j*XltXdRz6yq6&^d4V^ zPcFjGz>743AuTpA>Cl2# zI#*YPj;?jpopD^ktXNZU7&`?e9$#&9kg^KgZ%&D$6!wf9i*cu%t73{&D3@P~lL`I6C5LouLIR-G7k66uoBld8KoL!DXv|_9 z<=f#*V*RTYXaWFecbDu_3;~zUD#I_TL4pU!U$%dXW&@Bn#*-#^)u%#)6q#@F#mxcV zjy0(I`o#Tq+8Bnx(c@TCFTp`d^b76fIs!edYGJSA3e)!86ZlfIgG5;okf#Zu9#s>& zOf-D~Nn!5${wn-=FYY|BPvkHJe1)^)_=hwO`bVc#JBGt-due9X{ES09sR1`z++Uu+ zEzedEBS4w-m`s-G$eis=UG$Ko;|)RPCY_y0UpGsI$G@>s&*A(8kXc{y)nbww?BOJW zco5Yes7mUGzjCm$SZ-wGdgW!U2ls<yC$PoO4& znl{NR!b@rJ@GBrSXV-}4y3=9nGibJr4*59D1aMS1^I|EKU{RBX6Gxukmaa~mzM}p0 zadEW*z^dD2K4ImB<4SR+3>{wEhS7~5+u^u}4@$X>voQyuMsalVMp2vHaR%einFa@4 z7GjnKV{rO*N&PCS1;T_S);v2E4k9#X#*}2jBM>ZbVd9Q`G9E}RLyudCU7<`(_V*9m zTqzz7a8FXHXKrx|*L;nU*{e^JvTh&QtKx~sL4|XZp;VE)?EAN~D!E_L$#NBwy7AfqxT?8`Pp0s3sdh_m&EVnQ{jXa8Ju@8>wY)D*<{wR;_r~!#@+hvK7=_(nroE5bqHt!_DvTZCfds5>{c>Epc9x45 zO4^&p@4p^7+6+7&QUQ?Pd|euCe|3& z@OU=eaK9={=wEd@ZysYKJC5WzBV1#`*9>rsbI*vPR*HC?Z4b>KCFYuX;(*N>1 z$*EIv3;!3Y>3=TWe@G}0zAxYd35Pa&-^tv?qj50f3g2=jNw6eiesZhhK6LmC5USN~ zCqt!u$tK-|LG^zWJ_Q)c<5CK}48GwN=9}qj)(8n8VNEQ{HuK_+A@;h9BdpXY)s=ED z>_V#oVrzQR2T6(e%OAc*`E4qW88<3&J|znnCiQb975cfP*e=kuVo9o=9Txw|KM1=< z_5}tYex69{8;Ah0&TlRcoQC|d10q6sInBH}Kv=&#e!lp4aTwhRhHW=&Ma%3odV|z3 zGaMy#pAR0jD`NHx$0E+cwszoDnIAKtHEjN9u0=_0xJ8W){ZMx_j^;E%hvtt)HXQU`Nep&#=S(<&kl<*#*_LcC}W&N3_bl z=&JtKaQjjzCA)OG;%<1`Y9!d9Yur`yBn7|7S_11KPHsxYfFDqe+2rl>KB|z}L^FzWMjOnud8~Gou$9op!Ls}g- z(UZD?OemrnH&u_u$J?jqyG7u0;xH~;{pyF~K8}xvH&an<0Sq1V+5v=X9b%uStITlk zu6H>;=10JAe8bA*FzoCO3*X$F44rAOFyZ;Rzbb^wg!ZZxqGu}WF*7z>{xEG_4wgv) z`dfeWcz2o&rW_?xO@)FwyFY{G)`Drj8eWoqP)EN*phnln@c$uH6|*+EaI)k;JK=8y zcD_{9*ucQRA{wG{1X}7RWX^dT-5(28dcIF5jp>J-sktHNrJAiNhnVAosgb8|=77M2 z*(S3x{>|DgsTD1%wwWq=m<`qkKv1bx$RdQeIpHqDMCU4ZLA>~JyXbM9{U)sQ93{iBFm>NQ=Yz0^~*LqzZH4pM;;M01gmHV*WtQa(jD(iW>Wv_jh z$bCkel#WECx#7kUb426q>DcQAAcoHcFCeTiCZ^5#laBk@zCyNi)Hp&I{g~Hwp2kO$ zSRZp_1Xp5qPG&_pOX_duwb58wSSWRGgcWAafMG3dY8QJxJvhx|C~cvl5jcp(jsl-A z6m;79HeVfUeW%IwT^1^hQt9h85-^6q2(c=K*QJt$#@G1clLux9n4Uo2O{{*wt!ppB z6ITBD+Ng_Nh7cG?GLiLh#=!JKU}8WFnSU=fJZ^))9=;AS!_JPSM)iF-(baLk|n$#Mzeeu!x#z00AMo zBzqWT)Kn^t21^5xAQ4QzG?wu!$ATb$QJB|de_l(=Hh(j;VXz*YFyP#E-7G}Bz%;Q4 z;ptN|tg{I@IR9c@r!pZ@J9fP4WyDELMJ3Qn9Y2uFU>70v7td{+%i0v;a9f`i$b+5d znhf?wuP^B>9G7u=+R7afDb_D)4TAK$DNS!DJ#Dc_~v_E!2c3M{BuKH;)15x zkvA0=$C~Gzn#`GssLS*%Te#M>>TD?$($Av}M(VavRN~0{ot73`PWPK)a>^%d+&m-H z{Vei?*tDy2}t4*gtdr$Mv(_JpyOKmGdbfI~#n=X@qF%BTyVR|&iwH~Mn{ zRmF$hNu^{dH{#ziYg`A-5`Y-7E>=qgYK$1)e${c*_4^Tl3$c6ZTb-v}`@BTm6#h80 zpsA7~|F-qBs3urUeG^@49l9_7nPZ&dtRsy$`3m3<5`eyLvG)nA7_y)CRb2p4?3O>S zNKQePa3FY%%IpRBnYd;kb(e#eBrtEAT>LFC|B%xlpJ7;iGQ8NNM=YDn+(No8O?EDj zIETVUy4~ILZOR&Lr9QOhSEsdIuvCp!3q;YlP)2^6U$NiCofE-S+*!oy#t3(ipX=Rz z7yK&$vn{K^aRW*xBqPJ%1U()-v_9YWh}o`E68Q@H4H-oA7kWD)oVZ_(r#s#DR>ZT zbULN)CB;TOPAK7aftb_qagwGzOw-z4^e8w?4eY*tKAdW#H~rfz%@%QzxuDAw1ft?G zWi{ltgZkf`B^U&LI#;4zVMkW=U!;)jl!=I?%hVNc99hXX)IEAqng!B9vDB*kjPb@Q z<*&+Og)y@b9i6zD!}YLlSBk)w9U)Vs8;Kd%E$mnW!Wt&@2I8UB%~a<*rLZb4zIc!u5^18%44> zyJzAxz=Uk~l$+%5$o0*JE@xJI)bX*dzDps|(?4JinrAoZ8>o5|$oS?+(E#OgMlZk7 zf5|S@Z+S8NJ9A;>Gw9hlfenQk)$uX}da*#1f2rsC;okGA;2UYd%XHEt)A2ue-cs9& z>vLMw9r_hPci>5sq9t!p;l- zA`pP~g+W5UiV~2doH!V>0}?3b_v8XPwX#Fo`c!!v*H1B)8ch%ZUrmOIxdjzb=v>chA$o4&+8FjkeW!!`!B>0xE%fR=<_MSy}0 z%H_m$7y-+(qQ%y)ZCgCUZhW}EnP6reOXjoF+E_4Mw#ip7nFWAxvjB!HK=`KcK0(^! zwoB$n0u6UGbyWfk$$dC}pR+mAAr+Uzh+7xZ}PRe={vK_m!7Fl=_-MQnKDtDGD-oFt@$rCj7wy&y+-(j`hpj{x~2FSq~ zmRf$s)9gjUs440(>9c%~mal(z4OS}mO%X4jtUoe74(2|8xZ@j0e?YmrT!S_KK{+A$ zmf+_Yfjo?eERBz%1O#XqdD?a7<2MN@92sI7q!90b$PR8jGJ3Z*1RZoDoZTmpH?hsrROkwjZ}LJ*QCB; zy(Z~&L85L(sp;B!^BDL!Kkx3J6#%`@{>Btt-yfG}G{#sZH3n=X*E!`4ERKsxNg~r(t*alGN7t4^lSXo2PTG56^JBEO}b~>!Vph z#Io!C0Onpmfgk)yq zk%p{UY&K)$q2FP`o+Vue<9E zpw&w0jXMOwxP_9tPZy8v))oumJKwMmqBZZj@Ng`?a(B~nC}MyEv-7u041S-pceiuh zI{(jt|1W>=eJO=x^o10n4X;`0h6v>{emDwS@4Iv7>O`{~^H0)hB=p0-d|

    %cm8p9_CxbFfd(9Hf?43}pXO;gOFu2jdgh&VI1>$u3#Mp!Op{iNuf>Q7KW|C9y< z4TpU-@LD%Xt@gTcVdCc#xx9U~N|SSRA%?Wa=_yd`f|wP<)+=SfGeUa$4C~7BqQjW@ z1~3C8RmJkzLHa!9c~H87-;Pa|87Q4~9d6nN(KbDVT!VszE-y2&s8xWCgt{#qShw25 zH-IauK*}q3dMS5h4G3G_YjTx&PTqfuzShho{M=U!(B~j#PK9c%e#5Ez+$iV)EHmB^ zx)w}BaXxp@*3HN-LuR-Vot89P?WR6X`^xKjo3%lxxwOvi!4F1LAv=;ao^8hQw65ns zDoom5@hd~XOZ{me_wVcqh9Kv5%0INExUvqpsmcYRJ@p)MX%(#4>D*{Dw|UwuY%lLbjMWYN4i;L?1`khSUq_yNJCVr}s@EX~ z@c<2;KZQLm{%i$Px&O&y3q5|pYxz7tKP7AJ}JUK^wp5vU0!tKDPr_p4!AME%b`m<=du%$&Uayl}%tn8=}Rakdkk# zHrE%<>y{6%C?!Pu{^kjK&t}-5oSbzjFEQO02HFNW*W}eC>I;@K>$%C2YV{PEav)$y z*&h{~O{!DHa-wkJuU)^O9f35Xf5FXQ>gtRn8F)O)F;;|Y`Tp)k%|(EX4s3{K$)C=n@f>jK1RQGe&K!8lRG7eW zn($3FTW~bZd>Y;`*Gb(H-4fYs9o*^9H`2Mja|DJn6}%WfrA)siY!-5nGStLb6P3%o$W>jnLenuoOqOkv=+?|ntt_2EywfB};lHE%8!X}Fja|K%0(%5(WoX=1 zi@2u}|NRyA>FI;;)5|RiY9mLfTGIh-W=3I}IbpgTNj%M_*R$CfWUxcpEFA>$SZkwoDvtre>^40A`hpgE5SKO8xWt2W6U|PZ;kgGpPqKPa( z3`D}BvikS912g5C=IeE_ZiTqXQBu|*wNkpG$o--Jt&}8x`0~2nq%4W~(lbj@aiNg#cVLFDoiB_*mARL+^l z^H(R@D?Wb)g|Yc&S}`Qtqv+%~c0D%Wxf7~_`E0JE&!sW2qkkJY3eu(l z6ZWzTNEuOZX{n-CqTOAYRAuUAl*DI7Gketmdjafr{@^AZkLP5|Y#%V#{yr6}rpBQP z(I=74b0A2TKsrI}Y0JwKwGVow7j>>-@HDci9!o`V+?XVT!Ky} z0tn%~ID}kI6Rj3?YR|K+euD^TrU5%hjun$gh*Q;qWv!hqN#(1=?8}VevbOu)czSnU zAtBwU;cO!SBg+t+e7xH{od#7+-QTM_Sc1J~orq48vb=lkg(PyeO_z#w3u1x%M(ccH2(`BRSSp$;yW#NVteA<98ouy{`MSfGp4L{cZXo~ z7HYt}%}#py$tq*QA61JIU;LMn>9*W;Ux#k#{OkAbRzu5g`z(VpY{+sq5WFI;2 z)}N-s{w8z>qniMYQFYHpfvmza&IK?xGm=Bq_DF&F_!nSx%7jyepO>FUgJjvXD1=$- znKuDA_H=MAyN1DSyseS;jdUwv&=imtg;ON8<`f2dHJ#@|D=sOCe0DH3MVIEspaAU( zkEDGlDr+tzX%9}@Us(J`mFkp({>i{Mbz7L)s5nb1TaXR-H6!I=8JAlPmJ@uYT-;b? zLUvAG3k-`9NIoi9zPQDQi)Jr^Y^Z6Z$(8BQc@zTENfx`}Xbq;Onm|H#$w6!QS%lD= zkLrC}f1lv`1C0vm@5Y-nl;AiFplfXR6NOaV)gx9dIw{IrncNTAGI+?jI+DO)P}^_3fs^g!&~F$(=BQWilSuz3zhgPF!e9v!UkeJL z?A>p@p(4t8bUV~kO^P|dcxAX7Hq*A&^*t+M*q>#$M^N)c4O3Oj0R-SSAcx-CYCPN7 z;e5)}C9JQNNZQlA+pWTvq4FT%h76Trj!bjSO%<&bs=XWsc*xH zfoRsDkNFa?{RP$|N^oobW(#lynwQQuIM1Z}+zX@%I7$!EfAxASRuz_ue8pvnx*0iB zN)4!vefm}-Gx$$t`1`>G1*tvu5=&Ou?O*zjf-in>Q&$cyTorF&v&pMCw zb+$)MoTBURTiDXX5JT>NzIsICI~eP`nFVKz>}{QPH@ihqdR5#4?GUU(XSu^TCPsH9 zT_GrJKlozWa5$=Es%w9~kKeLiXq>caPz3o7B>PORiu~=Y(v^2<+D9>M`A2Q*8wAXv z$%tpR;hGn}5ay4!VO40YED#AZOdg1LYhO|>h$h-pZkN7D?8~-@nfWY}`^1ZGHfTYL zyzAY!@bh(e#}nVmU}|qkR6Dp^*3R;q$Z#vPS?@=kl}2z>7^w~30Y==4*P6#c>wD226zx=k;R-);*wEm-{$_Bb$O zxkT)P8Pey(t9w=&Wf(&PXck3>np-b#+hr~Q+s=o`8@iUbb}Jsp%c5pL<*(*GrT7YB z-Et(&$5hYcJ*7AMj84cCkkQK&>J`=M?gNz5=x*M&!p8}>(JqYf$5rgvoIfrkA7xdO z1j0xN+7u*h zI+CN)7YJ;wI&B9T+Qb)c7`2^*0kH~GmNa10&3Oea=DUc~9<G`+&3JDJ^xqeBCm6`rxU!QIIyupQ8_Kdnj>F^VJC%nRCsOBf2dVAmQ zZAr^2L(%-LD6=%WOm3nuF~AyBP9X$UPN>?3E1b@@ZYiRI+i8(94L#O6(f0WS(&^Jcfk>9#2jAoMf%hH(q|ji{XSkCOlU`vp zRl&2>RK?ZBk9mU$KW&fwdrH3g8rtUR?pG#W5fwLA5e1%YTY=B(z-Xgt!vhmp9zfwcrex zaZ-V18(O!$`3ip;QeHr6=W)h5ip-46L7_ypjUU+Qp2S!p}!CnMlQMvSL4( z{Fq7W5I)ojfu9!GCROCiF)AD6kN z8;nCaQJfMq`&veae|&BFBW>Ed@5uHFe)jXW*9~5!!mrBRVs`^q=D%JT2`P1O`hB!= zFeuh4%JtNTpP|t+XAVR;swcVQz&$Uhgtvbxj!>!%i@9;7*>#`ss%*#y48Bj6Y}jbQ z%4*x3l@dwMpb%n6+~pLMT5)@?H*}=?omK*sb=CYmmSN*eMIV==>R&`7Nuu+ykwM}* z%8T+AZJ zlu1^P0HC$SE}1r;C?ed0cX}?0Fj`tD#;StHTu{gX_r_Heh*BNs{T60cE=SS>89Dk; zdJZ^?H%DH?SXNUtuXPA&QJeq-!_Q75Nu`rCGj^*W31RB~%BL8?z@fN%c-)9Dn<{_{ zkcXi#fIK8vf~>vxhZ6fY%)KanB13cf01rf8(CaS_idr)7kQ|59#<%H9^u3phZw)%; zrml`1_AfU6Ckue3Y&hU(&FPIKhsz2kzD~6{6LK|q4LTZi`Y1+p!&N3Z9o-nqDPXuY zX@JA1V~o;pD8a+KdW~BN_aGHEO-_R{9TXbNKy!e|A%*aZf{SoJd%CCt95}7Xqs&hH3$=+;iQF%?cRTT)o_XKhjF~%F7vO%I=DQS_ z*##`3gEY+8l#Do3{N_iXgIbXatn=-nverUkTno#vfe)Y^>Mi)_H}!tZe;$$_O}Upx zj;mHp{uF{KM2!J`Wbf@_*P1fCF2-t7QDpfEX&(?Cf zh~n^8PrUx^=Pk|`fYO=RA$!0g3S>+?2}F}TYW4uP!1)fpl;N~XHH%C58b*-IFyJ>e znZ6Bn5f-8otPq1#!$$T~j~xD1MDW+uf;3kz#qzYq%9kW8VvOB5qKvB)DHPq2hYQ|? z_X*Dni30el*mR z@B25vV2RM7kf`5ONI7N4rsh>`bNy8-4&DC@B^hh+z)g20`!m-ph1n2-pSsq#a?TFA zuy0o3IasM%6LOf9?&_YXc{V4g)lATt@4Rt?&l7RxuQZlBuocd+VHp2C+^y-=>G>D= zAu0e!Frk94E06Gc@0Qe^@}Td#ox9G|D=o03aOZo_f_Y|bE<>V^1t7@y(U zW&e(J7J7P4a{oxnplluf*#CTp{Kopyemi=eiIQU=-EK)gBo*yLwshorFnQdN?$k5f zal-Y844^N(;Bz|D4iapd;I-m0o(wGy#8U}h-)Z?L#;!BBwZ{R(D`)x-Q;YeC5jS?q zTIX^PrE`$-pPq7D+FmNIFR{D40&JUvXx{wn?&mw}%=k!kKzKqN^~$oq`~dTUM_DnS zkUp4_oDq$Zv3yB^=R#n0#SKt=wtg&k2I*GaLiBp_`TDka2OOC>zH)*Gu#1 z#xB?6kvLLHyxgJH8Mkhrx_RTU0~39bd;` z8%o>0#_moDI&$DBmZzxzan5~2hX)#Vk<1&=W;k0CEMJF@Z~;0^Mq#@maE@Y}Hi`*@ zXtlz!#e9!4YS${LG(*_L0LJ7A-8+JtFbF#RaIDfW@QO^co*h40{OUm}B%+jb-8%mo z%EqU#U@*d-%x{6;p3NKYz!UMyw-zPW#UD)!JrpRAG@0^X-fue0!t_R;V{_gae#1QkEX-ec#6`j2s^MY@rf>!Mp8 z;gZlv+k?RK&T=Y_C>1pWiXu|Bo}S@zQJKCvRNQEIO>YcyoZZbmAQ1m^7p53(3kS_2 z%$4E@vgi1<@@>8VA8DSu8)X6=%1zSA4-4zwn=dOGqx9z1@eY0A#xoYxJaxCeU2C(K z=Yq!QS^PJ5?YdN(FVc)>Lq2fr1>KCxjO8{h27jYAX`3z}P)NS7FsWwu(6ZDQelH~{ zxi#1bda~LuII#RgGfBj-u{S%{cOLU&UTEt}k(mSb4-q#qZ@xfJK_+3E}2pm|~k zZ$3xCIXeA49cOu&BXu+#*F&Hz)s*GCY!JyWoCIMyIqW)Tp{g|Ia5( z1AM|Z&rMn*tjXIru9#p)g+flM;(~-jTvOO)R058dX>;{5E0i?gkX7M=nZe(8I5-~{ zUkFWe8_`DTH3hmua-7WR=cWt?A8&XQVC~`B{cQ-a#@|tWKR-Oj{Hzj{BE(R&-oJ6{ z!1|4w|MBhz=G*_h1`+y}^N^@1D`Nf~G)hup+zkCKLx1MW|IK;jEum0hD$s`qOpI0EPs9ZI8yj7O^Ef2?XA~Hxx0OQ#}DQk zs!lIj+qSj4-u5OlaI6!hzIeL8ph>LT(b>7Zr1Q}0^+Uyz@izyP=Ko>utHZL|wyyZrqYRx zLjR(*5%Qv(e8`k{i)|7|T*Vy2p8i#pr6O0nL~q|bzsFv)xDRzr_(}uL#5rR}<0p^# zW<+k%4hb?5kf^nUJ(|o5RjXH-0zq6Op7CF2DQGb~zekSCaCT;@iv7ClTiK?qM{dls ziB1nXqJW@Tr~;ryQ%qQ|slA;cHZBB@#Az4UzC-A1bial`$^ZEqxO%pyJsDAuid{^F z$OYEu{l-rX_wGM-j(Z<`v{#kU!J1<|#m_R@qC**ug+NVoym^-00g`U5uy)!1x|D%4 z|MV4%id8CtBfc`1_+2Z^WUBTzOK&8KZ+@xo$Y7~*muyEH{Sjq^W=j$V1(c`HK558e zOT1tW<_VKV?t*;ZtLH3b1ms3$GZsWoGR?-G7G4J9M4m}Ixnfo!yHLD?-L@w$3(=56oPXn)YQnXxDLy58ag0J)vW*7B$GoIMw&aGsE zT(kmEYF<1+plQ#|o0^nj_#Cyvm(BRwGj5Uo1aog8OVlhNYTK^z`eGTfO1l@t=U zrcLISXs~=asGRI|^?tR5;eCis_A@N7w21CWl^(l~y(ZRno4CjdVei}EL{csCHQRr` za*`N&=We}Civ+`cCVElyo3bE11O7|9*q(qp*mO#EU9Ax!7`n}G0nwx}7Te27Dlwb^ zjORDG&xy>=#i2dG5jyZFAQySDAKPtnAl;n8>FDS%Zvg_F&z9R- zK)s9NW=5xM_rsx3gUn8s)Z0qXPl9pYfpoFew$W<4w>Jk0pxM>Q`TFSrsBB(2WK1#H zp62782}$98Uaa4KzzfZ0Ix@6>Lu~Lk;7&H5>;1UK{o6HZfKN;8Ywl}cz~8BrzWkt? z%6!wNNP4KFU#952|M=>Ha+D8W%r<1zIM<;|+$cHSvZ@a$H43nwpmr=cDfCd6x)AkB9OzWY1 zsRNp&%jL!g+C#D}lkN?6tSPtb#MCTk4}r@<$gfOm?gD zPc)AK8`Quh+E5lwzM4FUe%QHDdpnATWob8~Fg*VH`e5!eUg(p2wMw6C+4E=F(uo&! zSoJ)Hc7bl2OStb|5bK_X37mG3iG2x5+}&kWt8~NDS&1`YF>Lv$>uvQ~(VNV$&f~Ib zmY7nqJ)SIcvYrfQ^>lt$vsK%3OxQ9knDC?FMbi+6O9!mV?LlXYteR@yGdKC_rdy-h8#Cr)q zms?y)wtneZrw8CrrW+T_y7Mo2Mez-rR6E)gO4=1}Zx}VIvUQBUI|olQAzZ)OwqHgb zy&k5*9jCs=**qxCYruM@!1Cj@Ga3XQjX1s05;YyH^Akd8^eT^Xhc_1}1dkGfap;z# zsMs+;4qBwHE&U*Xw@gZur*iF#wUzfY4aK~iVAZ-Fn$p)$T9>a`kw;;ISdFrD_{uK5;3t5g;Dkk`Lh z=P?`x@7n-f(|XN(T+fq;DsD44RN`;4R5Hoo&rt~2*rOt)PC&`XP^PQ0PrAT87iIdet|$N$ zf0Qd#dzdR8-r1GJlhl*S^|;z$YpBaN-p$)oKf3d`rvCeP6%@W7PQjPlWb2^vjRsJGMQV?zdtOWDIIbK-;w0s^0g??l0Kp8? zZ%$Q<1?UlcD>bD`;c{1F97%s$0@(xG{CJT^O4t6=+A|!q^`pZ^p7Yhgxknb6?p(H4 zpae5g{$c9G^U-}sZ$ir{3X{gev^w=_O8rmHq=br38mM`BlP#ueK4jco#5yO7cMlI| zUrxgAG&%ejL(eYzl*e!Ve{(WHp&?MvXxau&wkw@Z5q>Tv#vc7HaBXBJ3mT&bbPBpypDHU3b{Ekjo0 zpH5^z$m22;2~Rvsr~E-eR$$QIX0gjeT%H9;S-mxo#KGXLP2;o{7By;%j{wqnlD+kP z-XjI4u_GSUiE=sOl9@Ajxj0dSkq;#MTO@HAtdF==?rIw9&O0sb86>hmY{#!4B(dgQ zQB-I#ch65g zq-FtI5pq?D6^3GR>Q{TB!(bnTXj*yy0~ZhG{TUu7YyiSH~D^+s4F)lpOLxF z+0yDC)$*%)X&)sv@>**&3>dXNopL?OCi8`BTapp8hY=cIFRXoyXwVxG1&4}H-*f>I ze*T9!{NJbgwE?uauoWtqQ<~OPDZKSLs^5FZfmhaci)}hD$3B+I6sYF1`=uad=Bbvf zaJtTxYkT%TSIkxO%LB}GS@OaPxf{zUMzZ5OTD7aKB>H7%DEM-~&<)*>UEQO8?(#{u zg$C>uv7doM*w2$G#_bYbXS0OFpnAb}xzi+CMLDvhkDePw$p7l}y@G$a#tJiE6&f4U zVzcZiyz(Z^WLwr*)}@qVK^GJCl{v6o@?(40D-(({Z5cCw(sMfHf?uen@J=R?l(S_d zze9DP4E9QVI=A$DxA>2jhT9st%X1I^H|^7vBuwP1S+|j{uu?0$>AE?h8o@?kJ8`EW z|3LidcRI+pT~vWzk#U0#h%cL?x;2j3YP^}w@TZ%}{pNRWX@R2k(Tl6QBcA0-RHOOI z+5z3SY69pmYv4*JN3dyHe@4O`hYeGUkhv<=fO>)>Jbly zG#~fj2`bLFT&V#b`32;PxF^ruqo)$-E4qM1I>X98vQ1<)$>P}3Z3!9N)oWdaq>*#! zLLuY~=E%w(E95PfxS5gre9$u+#lStio$pu;(N5p=G>d|^*_a7hXqCI>n2A*Mf$W!y zy&2NohhOIv88)311CyjbwXfX#Qqwh|P_8s_13mJ&)0WYX1L7atjeqhVQi;1h`!jyZ z76y7>rL*W&OFpW8RIZ2WK5@d&*Qk^FdU-TasAg5b@&p!eUmEXKWBT1Yq$m1zU4C%( zhQdjbcxU;AL@IBkK?Eo}!G!tPF@W^;Qxt=Gm5OK_HuNr7r(6d8hrZA7eN+Z#H8p~| z$#Ax(9KD`gOaptQR=>rw+A^lmbI0;**OiaQZE-?jrHAM6;x?OU`{up95kb6Tz_czW znIgdo*_S;eT!Y)5=m_EBE!)PN?w-NYwG2F17`U)bzUqf}8FrjjjXyggG2XSAs&STd zaXmm_K)T+!{J`qAXk05*1Z9WXu)Xbm{$4vRwK$$zc*WxQ$ef8p_mZqh?0GDG|MX$n zl{NuB$FjMc)}c}G!U%GsbCLb$QLZ}3?R<{V7OnZ@mFdzFUO4Nzyh{f*9RasTj-gv^ z-l2Cld1|ORZbNiNE%@|L@;T1e7oe>z$kCgRkM__0z1#G+ZDcq)f(rH!;#*OnA+q^# zw%Q>OeYaDAP2}CB0Oxs)E4dt*cNy;4JN|bpr(7Mj2Xt-qY1Ka8yL9bTmU34PiaZcK zPCbK{9d+sM}Juv1gO+uTH6PQ$M39E@EblIn7X z$3{RU?@2iK$PFaGy3~Rs-qOJCh;SJvMHDZTA7X8ZGM7+dB<_Md_1^9s9~i zCT`h}{|2QG;$eqh1-jV3Xu=A%r|R_h`}rU4hOOu=#=< zNB-$W@ib167gK3&>|$5`cL?U9Ab^Nt0J%oiC-B=mNJhkV1 zp+YwBEQU`7Le>$>SDjcY#iHLPh(aK?eGTaTRR$JJv%wKHn#Ym2b=Z4b4+Ka}svS26 zbWl&Iyku}++TC)RNMqGW)|y>X=BZ{LP2l|J-SVe7Is2GYyyD#a+xthCpI60v+OX;i zaXn}}ckXElJCPNttZIE!s)bu>OBjUr!h`x`3Ff|?f|JUu!5Lp-x9_G=laHLdoY#k-G@JK z6Eh=q8~Ga$0yaKR^BmaH8=5lRBJWI`UEacK6CD1p>nrzeY6~3+lUjlzFZ8Tpg=Znin+O( zcyv5jH%FuxChbP(SwLV63J1~-R`U`L#_3YBoCxQa1W}+5`7hz0VsONI$Gbp6>!CH1 z*)3F<{MJ;`wJza8J5zW8TjVv->f4Efx_ss|wpS%PeMC_PpqAqiGE9ZuH1U%Z?$2?l zY03pEjAaG^I7xIuRqk^;I}eFsXygrv9FVdwGxjTr;yF`M(MEH-6B&{KVX0K_JV=9Z z?6?cD-l^E`1vv;?3Rd&Yn;$ak&mJs(2IfS5tayoDQ7VqV)*#jBua@r91FKe|kx?oR zi|Jrb zY+&*RTDZMc;-C0pCPzeTbS4bzy*ML&?`1aFLXY~{>I_Ee5)4i9qXPU@hE&{%vsK2+ zal7vJGzCIO`PCHxfNNvQu0OqzLYvz0@tFGpi_dBNFonl46nN}vTlL_C@bpOtw(Vrx z>Y!0NtzvG!|KYm+*{4jg4|WN~q0XBTpe`ZyIS`))-1j!$qLz4H%{+8cF0`JxWH!5E zfnAgVbPY8K+Es32#1HsV(N;#2zGS`{Q&B!eatzs|Cg=SK$pKkw&kHmvKWtvBVTzn( zgLutwjAq5`j4N0CSHG6#N10@Ob>K`lLz~QlyLrl8&1tBs=fM6+b)+jquP;rr(&h=3 zQP0Mtx1doGY|Y2a{BI&G)#mF;XC4c1lJ3fk+Molc_&c&f#!e zAo`IzK3nGvi8~nOif$MyO?C#FvYd^HSL}=3>g9oc(#xrk-@A|V0S)=;*8{_*BaSXK zr`3`d4Up1@-lT{Wc#W0g9!{JE;8<}%hDzKF=}n@meWT~msy(p^&>Oq8Me#%qNA`@B zt^}3>AKR=X;vCB$_#VNHowvR)*4%kQnO{RgCb9VJlsrr1vWGo($ZYnQQz@nKS9OSV z_d)57q)>%CKb8%M!EJt;)R^lPFdxv}2gR-Z>10i9?VV+VWlO!)78eQtb@9V8OL#GQ-TupA-kTbF z$10W90O<_k@>y{0V#zd`k0tDVGd$vV8$yO&VRcG7cRQwdu|4Yfnu7S&aQQfIDzmqU zB&qpYG*08GUnfR1yELtY4uLdfzXhps^38aM2O_3F?pw-LYIMf8o|1V92ZiY|`ljC+ z08^r>Ebk;=CUs2wip}1%$Qwp>w#GOh!TMPn6jn*jc+e5=5&K8x5Vu2-k%4HNl& zDUNH@POlsnKAW}QP3i3vBqc<~rp;5XbQzXVp5rOAT#bBMxUX_6bt>kac#;xaZweos z-#P7c^3}yYr7io)!=rP*{jKSFAXk#z$!Hunzfcld1sz2F5CC4a+a!7W=!aW8Ucp#z z2rF+9u~SOlOA0LW&js8tf3Ws~!sOv(=CqPAv-S& zZSXW{JbCG6sw2J<Nt}59aUcwV#a5Z)RO)}n<~{H4~?Fb#&1dVgdoGr z4of09cKXN-?QN~*Ml@D=QhCtt)+l1oE$&Orx|i-bar{K}gOX3r0|A(lj6mVic$(rD z@-%`5=3QqTgFP1V0<7WL0p7t81ef^JqclcMeOUu1RPEvw(-aH1I@QW#)Qy?KOeE}J zoEJBG8S{np1NDJ{Z6UKDOW9<~(R7g+jZY&klFIRGjhji)r*3~LN2-v(z%7);#TLTDl!uIJuU!{JKev{F6BtHKMia1Q!tzx0 z>m^7C@0IDb*Dy(5ilh@tf~;5&Gl|M=vVOSPbnGWazK;B|rXuyQ{SL!HPb3@N6mXjg zbdNT;?3@5U5qBOmsKVjfoGKCPK1-QK=hZ6~$825gfiu)1v=l=niG9AK?>Tdu&ECds z^m!p?=xqx~#Nf-RZv`5kz~C>SRX+89jkj6}(GDkJta4bFah$~9a$~MZDO6jXpdkkw zv-|sLo_VPR$!iVPe_n?d9EgIXF<+M%jt628xb2rS4xj_bDt?9JNj(tW&p9;pYVBdb zH)6_V-!YlbxnF|=OrY))(#jL1s4)kRFHIZmgSZGJiPDxOpJ?M*e#ul6%tLIzB@o>e z7F&DRpUo^5BBUc;$rmGpkg$=;(jT-tlptxhG%jE_c#n%}HSb3!qC36Xe5k%|aJshoiZvYm8D(mr5x`BPCCjU2z7@>m_$4Vm;2pA4GesB;~ zlAH+{T!8uYKAPIWuyC!6E1BbzqEFpSg^;Kx?qZ)IiOzr+1+=z7w|ZIR#jqQgb@?0xIbhbQSxT9O5%USNSZRoDWPv2g?LhI^ywfx+}xXjZ`X_eRD7wrAy zLHHy0M2ht(jR=t>jNaq&*U$UN8U$?l#~&qi{MfC3oOvGHGWw#*yvI{p@viF~3zX%J zZQ*)ow>_5RQnOoia1X_DiNyWkaEuJ_3c&?s3IIltI&r(MeGmkip2gm+ zBIsARtUEXE+33xpa?0Y_^=`Ec%nBpoqwT!MYfO5$F%@?cDcJg;5;X3(Ld_fCNgCqU z>NvR0bAOxmxm=)|yb06b@|iCR{Fmo3n#BqxFGsIn&z#h)9UUf-@u{jI7lyEF4aDs$qn6O0^;(9-A#&B_<*|Ud{ z?8{QqqNklGY@&#hjfU{aHS=p&(sQgd`_FkN?7b&?Nle-v_(a2a(8F#ZaG{WGrYiYn zU0~b`xZgQ^Lt*%jqQA;GDLZmfGs0Zo90eKo&n@fkdaT5M@v%h~{bd8WNUJ^f3d?XJ z;Jl(h7rrYjy^_)=gszbILMWK^i975k?_la+Y-Cy$bPV)oMamZ;nj$Tt&#_=yd=Lg) znGngsVea0YoiduY*kX*c`k)axbHZie;9%%@RE@iB<9ww0LlLy$ZeLy{2KJZ57aAE^ zsic`)Oi19PhBO#fTbmY}w|6(JIgmUP$!&pF7c11IYN>~Qx2EpH@0!v(Oi(E7$2A0g zp4|^E4KWppUnvxO)Q9o84V_Iqte=6rp;iA?dm0L+nCaR5hkb525NG(r4a*=51kz{d z(B810qAsSzK)7*fOk6(8Js>n;>tkgM%0>I9-ZoVb|2xFmu@i}gBeBHuIJzp7Z zY$?)$Z*kQqm8}tC@TFUd*Zxg;{J zr@yp*r24o*fl^h}CD}zI^_|it_wv0nZR>XsTEtJqrMYR`E;uM(Nw_!ISDj5PjTC9< zDke)|)GR&F_%GhCtS{Lu1?LvvbQf%|8He?|PjPQJRO&TP;f1~>g&6lz6{h<7NSxNt zTyohksE+ld9)7Ur@VT8`Z74iywmjXzcl^eVxAYpqh--TAc}uu&$p^U`_V)DG0slWp zELI?`ki5%PK#D6upYiOqCXyur`fh@x+pxkuzFEbG*UJMVEI>OCDZtAxmVt!$zPI

    WwTlEx+;_2tq|-$kTvWL1&m1O);V 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

    bqrI%ONv2*jkIX<`0VB?+JVgUy% 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 diff --git a/docs/apache-airflow/templates-ref.rst b/docs/apache-airflow/templates-ref.rst index e3de362191340..17780bc323c77 100644 --- a/docs/apache-airflow/templates-ref.rst +++ b/docs/apache-airflow/templates-ref.rst @@ -88,7 +88,7 @@ Variable Type Description ``{{ expanded_ti_count }}`` int | ``None`` | Number of task instances that a mapped task was expanded into. If | the current task is not mapped, this should be ``None``. | Added in version 2.5. -``{{ triggering_asset_events }}`` dict[str, | If in a Asset Scheduled DAG, a map of Asset URI to a list of triggering :class:`~airflow.models.asset.AssetEvent` +``{{ triggering_asset_events }}`` dict[str, | If in an Asset Scheduled DAG, a map of Asset URI to a list of triggering :class:`~airflow.models.asset.AssetEvent` list[AssetEvent]] | (there may be more than one, if there are multiple Assets with different frequencies). | Read more here :doc:`Assets `. | Added in version 2.4. diff --git a/docs/apache-airflow/ui.rst b/docs/apache-airflow/ui.rst index 05d71c9a96176..c07cf9503b180 100644 --- a/docs/apache-airflow/ui.rst +++ b/docs/apache-airflow/ui.rst @@ -63,10 +63,10 @@ Native Airflow dashboard page into the UI to collect several useful metrics for .. _ui:assets-view: -Datasets View +Asset View ............. -A combined listing of the current datasets and a graph illustrating how they are produced and consumed by DAGs. +A combined listing of the current assets and a graph illustrating how they are produced and consumed by DAGs. Clicking on any dataset in either the list or the graph will highlight it and its relationships, and filter the list to show the recent history of task instances that have updated that dataset and whether it has triggered further DAG runs. @@ -105,7 +105,7 @@ Or selecting a Task across all runs by click on the task_id: .. image:: img/grid_task_details.png Manual runs are indicated by a play icon (just like the Trigger DAG button). -Dataset triggered runs are indicated by a database icon: +Asset triggered runs are indicated by a database icon: .. image:: img/run_types.png diff --git a/docs/exts/operators_and_hooks_ref.py b/docs/exts/operators_and_hooks_ref.py index 25b9d1d779426..82dac517bd139 100644 --- a/docs/exts/operators_and_hooks_ref.py +++ b/docs/exts/operators_and_hooks_ref.py @@ -513,8 +513,8 @@ def render_content(self, *, tags: set[str] | None, header_separator: str = DEFAU ) -class DatasetSchemeDirective(BaseJinjaReferenceDirective): - """Generate list of Dataset URI schemes""" +class AssetSchemeDirective(BaseJinjaReferenceDirective): + """Generate list of Asset URI schemes""" def render_content(self, *, tags: set[str] | None, header_separator: str = DEFAULT_HEADER_SEPARATOR): return _common_render_list_content( @@ -538,7 +538,7 @@ def setup(app): app.add_directive("airflow-executors", ExecutorsDirective) app.add_directive("airflow-deferrable-operators", DeferrableOperatorDirective) app.add_directive("airflow-deprecations", DeprecationsDirective) - app.add_directive("airflow-dataset-schemes", DatasetSchemeDirective) + app.add_directive("airflow-dataset-schemes", AssetSchemeDirective) return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/newsfragments/43073.significant.rst b/newsfragments/43073.significant.rst new file mode 100644 index 0000000000000..46bd71a6f0d80 --- /dev/null +++ b/newsfragments/43073.significant.rst @@ -0,0 +1 @@ +Rename ``DagRunTriggeredByType.DATASET`` as ``DagRunTriggeredByType.ASSET`` and all the name ``dataset`` in all the UI component to ``asset``. diff --git a/tests/api_connexion/endpoints/test_dag_run_endpoint.py b/tests/api_connexion/endpoints/test_dag_run_endpoint.py index c42d9b4c7f7f7..576b28b153531 100644 --- a/tests/api_connexion/endpoints/test_dag_run_endpoint.py +++ b/tests/api_connexion/endpoints/test_dag_run_endpoint.py @@ -1772,7 +1772,7 @@ def test_should_respond_404(self): @pytest.mark.need_serialized_dag -class TestGetDagRunDatasetTriggerEvents(TestDagRunEndpoint): +class TestGetDagRunAssetTriggerEvents(TestDagRunEndpoint): def test_should_respond_200(self, dag_maker, session): asset1 = Asset(uri="ds1") diff --git a/tests/api_connexion/schemas/test_dataset_schema.py b/tests/api_connexion/schemas/test_asset_schema.py similarity index 99% rename from tests/api_connexion/schemas/test_dataset_schema.py rename to tests/api_connexion/schemas/test_asset_schema.py index cbbc427a89709..e403e1c6a2863 100644 --- a/tests/api_connexion/schemas/test_dataset_schema.py +++ b/tests/api_connexion/schemas/test_asset_schema.py @@ -173,7 +173,7 @@ def test_serialize(self, session): } -class TestDatasetEventCreateSchema(TestAssetSchemaBase): +class TestAssetEventCreateSchema(TestAssetSchemaBase): def test_serialize(self, session): asset = AssetModel("s3://abc") session.add(asset) diff --git a/tests/api_fastapi/core_api/routes/public/test_dag_run.py b/tests/api_fastapi/core_api/routes/public/test_dag_run.py index badb32404b37a..554bc73ebab4f 100644 --- a/tests/api_fastapi/core_api/routes/public/test_dag_run.py +++ b/tests/api_fastapi/core_api/routes/public/test_dag_run.py @@ -45,7 +45,7 @@ DAG2_RUN1_RUN_TYPE = DagRunType.BACKFILL_JOB DAG2_RUN2_RUN_TYPE = DagRunType.ASSET_TRIGGERED DAG1_RUN1_TRIGGERED_BY = DagRunTriggeredByType.UI -DAG1_RUN2_TRIGGERED_BY = DagRunTriggeredByType.DATASET +DAG1_RUN2_TRIGGERED_BY = DagRunTriggeredByType.ASSET DAG2_RUN1_TRIGGERED_BY = DagRunTriggeredByType.CLI DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc) diff --git a/tests/assets/test_asset.py b/tests/assets/test_asset.py index f1a0ed13bfa95..a454fd2826bd8 100644 --- a/tests/assets/test_asset.py +++ b/tests/assets/test_asset.py @@ -422,7 +422,7 @@ def assets_equal(a1: BaseAsset, a2: BaseAsset) -> bool: # Compare each pair of objects for obj1, obj2 in zip(a1.objects, a2.objects): - # If obj1 or obj2 is a Asset, AssetAny, or AssetAll instance, + # If obj1 or obj2 is an Asset, AssetAny, or AssetAll instance, # recursively call assets_equal if not assets_equal(obj1, obj2): return False diff --git a/tests/assets/test_manager.py b/tests/assets/test_manager.py index 6b9e5623608da..eb12f281606e2 100644 --- a/tests/assets/test_manager.py +++ b/tests/assets/test_manager.py @@ -121,7 +121,7 @@ def test_register_asset_change_asset_doesnt_exist(self, mock_task_instance): task_instance=mock_task_instance, asset=asset, session=mock_session ) - # Ensure that we have ignored the asset and _not_ created a AssetEvent or + # Ensure that we have ignored the asset and _not_ created an AssetEvent or # AssetDagRunQueue rows mock_session.add.assert_not_called() mock_session.merge.assert_not_called() diff --git a/tests/models/test_taskinstance.py b/tests/models/test_taskinstance.py index d864d7f475e90..def462c2e1270 100644 --- a/tests/models/test_taskinstance.py +++ b/tests/models/test_taskinstance.py @@ -2273,7 +2273,7 @@ def test_success_callback_no_race_condition(self, create_task_instance): def test_outlet_assets(self, create_task_instance): """ Verify that when we have an outlet asset on a task, and the task - completes successfully, a AssetDagRunQueue is logged. + completes successfully, an AssetDagRunQueue is logged. """ from airflow.example_dags import example_assets from airflow.example_dags.example_assets import dag1 @@ -2332,7 +2332,7 @@ def test_outlet_assets(self, create_task_instance): def test_outlet_assets_failed(self, create_task_instance): """ Verify that when we have an outlet asset on a task, and the task - failed, a AssetDagRunQueue is not logged, and an AssetEvent is + failed, an AssetDagRunQueue is not logged, and an AssetEvent is not generated """ from tests.dags import test_assets @@ -2388,7 +2388,7 @@ def raise_an_exception(placeholder: int): def test_outlet_assets_skipped(self): """ Verify that when we have an outlet asset on a task, and the task - is skipped, a AssetDagRunQueue is not logged, and an AssetEvent is + is skipped, an AssetDagRunQueue is not logged, and an AssetEvent is not generated """ from tests.dags import test_assets diff --git a/tests/timetables/test_assets_timetable.py b/tests/timetables/test_assets_timetable.py index 0d931aa336fff..f461afa31c890 100644 --- a/tests/timetables/test_assets_timetable.py +++ b/tests/timetables/test_assets_timetable.py @@ -111,7 +111,7 @@ def test_assets() -> list[Asset]: @pytest.fixture def asset_timetable(test_timetable: MockTimetable, test_assets: list[Asset]) -> AssetOrTimeSchedule: """ - Pytest fixture for creating a AssetOrTimeSchedule object. + Pytest fixture for creating an AssetOrTimeSchedule object. :param test_timetable: The test timetable instance. :param test_assets: A list of Asset instances. diff --git a/tests/www/views/test_views_dataset.py b/tests/www/views/test_views_asset.py similarity index 98% rename from tests/www/views/test_views_dataset.py rename to tests/www/views/test_views_asset.py index 0a5cd9c7169c2..f2e860958ca41 100644 --- a/tests/www/views/test_views_dataset.py +++ b/tests/www/views/test_views_asset.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. + from __future__ import annotations import pendulum @@ -31,7 +32,7 @@ pytestmark = pytest.mark.db_test -class TestDatasetEndpoint: +class TestAssetEndpoint: @pytest.fixture(autouse=True) def _cleanup(self): clear_db_assets() @@ -51,7 +52,7 @@ def create(indexes): return create -class TestGetDatasets(TestDatasetEndpoint): +class TestGetAssets(TestAssetEndpoint): def test_should_respond_200(self, admin_client, create_assets, session): create_assets([1, 2]) session.commit() @@ -353,7 +354,7 @@ def test_correct_counts_update(self, admin_client, session, dag_maker, app, monk } -class TestGetDatasetsEndpointPagination(TestDatasetEndpoint): +class TestGetAssetsEndpointPagination(TestAssetEndpoint): @pytest.mark.parametrize( "url, expected_asset_uris", [ @@ -396,7 +397,7 @@ def test_should_return_max_if_req_above(self, admin_client, create_assets, sessi assert len(response.json["assets"]) == 50 -class TestGetDatasetNextRunSummary(TestDatasetEndpoint): +class TestGetAssetNextRunSummary(TestAssetEndpoint): def test_next_run_asset_summary(self, dag_maker, admin_client): with dag_maker(dag_id="upstream", schedule=[Asset(uri="s3://bucket/key/1")], serialized=True): EmptyOperator(task_id="task1") From 274b6e1168f84561e8f652a1a4a6fc82aeadc477 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 23 Oct 2024 09:54:26 +0200 Subject: [PATCH 066/258] Fix edge-case when conflicting constraints prevent k8s env creation (#43276) The #36930 added constraints to creation of k8s environment, but it had a side effect - the constraints could not be created if source of airflow had dependencies conflicting with constraints (which might happen for example when we upgrade FAB - because locally pinned FAB might be different than the one in constraints). Also the constraints were "hard-coded" - the python version, branch and github repository were hard-coded. This PR fixes both problems: * constraints URL is dynamically generated based on current branch, repo and python version * in case attempts to create the venv with constraints fails, we attempt to install it again without constraints --- .../airflow_breeze/utils/kubernetes_utils.py | 38 ++++++++++++++++--- scripts/ci/kubernetes/k8s_requirements.txt | 1 - 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py index 34c9db0766ea2..69703b4692b50 100644 --- a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py +++ b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py @@ -33,9 +33,11 @@ from typing import Any, NamedTuple from urllib import request +from airflow_breeze.branch_defaults import AIRFLOW_BRANCH from airflow_breeze.global_constants import ( ALLOWED_ARCHITECTURES, ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS, + APACHE_AIRFLOW_GITHUB_REPOSITORY, HELM_VERSION, KIND_VERSION, PIP_VERSION, @@ -299,7 +301,7 @@ def _requirements_changed() -> bool: def _install_packages_in_k8s_virtualenv(): - install_command = [ + install_command_no_constraints = [ str(PYTHON_BIN_PATH), "-m", "pip", @@ -311,16 +313,42 @@ def _install_packages_in_k8s_virtualenv(): capture_output = True if get_verbose(): capture_output = False + python_major_minor_version = run_command( + [ + str(PYTHON_BIN_PATH), + "-c", + "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')", + ], + capture_output=True, + check=True, + text=True, + ).stdout.strip() + install_command_with_constraints = install_command_no_constraints.copy() + install_command_with_constraints.extend( + [ + "--constraint", + "https://raw.githubusercontent.com/" + f"{APACHE_AIRFLOW_GITHUB_REPOSITORY}/" + f"constraints-{AIRFLOW_BRANCH}/constraints-{python_major_minor_version}.txt", + ], + ) install_packages_result = run_command( - install_command, check=False, capture_output=capture_output, text=True, env=env + install_command_with_constraints, check=False, capture_output=capture_output, text=True, env=env ) if install_packages_result.returncode != 0: - get_console().print( - f"[error]Error when installing packages from : {K8S_REQUIREMENTS_PATH.resolve()}[/]\n" - ) if not get_verbose(): get_console().print(install_packages_result.stdout) get_console().print(install_packages_result.stderr) + install_packages_result = run_command( + install_command_no_constraints, check=False, capture_output=capture_output, text=True, env=env + ) + if install_packages_result.returncode != 0: + get_console().print( + f"[error]Error when installing packages from : {K8S_REQUIREMENTS_PATH.resolve()}[/]\n" + ) + if not get_verbose(): + get_console().print(install_packages_result.stdout) + get_console().print(install_packages_result.stderr) return install_packages_result diff --git a/scripts/ci/kubernetes/k8s_requirements.txt b/scripts/ci/kubernetes/k8s_requirements.txt index e04ef56412794..2d00510e38261 100644 --- a/scripts/ci/kubernetes/k8s_requirements.txt +++ b/scripts/ci/kubernetes/k8s_requirements.txt @@ -1,4 +1,3 @@ ---constraint https://raw.githubusercontent.com/apache/airflow/constraints-main/constraints-3.9.txt -e .[devel-devscripts,devel-tests,cncf.kubernetes] -e ./providers -e ./task_sdk From 3fceaa69260be80bc2123cd4664db79d96142b9f Mon Sep 17 00:00:00 2001 From: luoyuliuyin Date: Wed, 23 Oct 2024 16:35:24 +0800 Subject: [PATCH 067/258] fix schedule_downstream_tasks bug (#42582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix schedule_downstream_tasks bug * remove partial_subset * Update comment --------- Co-authored-by: 维湘 --- airflow/models/taskinstance.py | 18 +++----- tests/models/test_taskinstance.py | 76 ++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/airflow/models/taskinstance.py b/airflow/models/taskinstance.py index b5fcac30a9ad5..7cb61bcb61de4 100644 --- a/airflow/models/taskinstance.py +++ b/airflow/models/taskinstance.py @@ -3673,21 +3673,15 @@ def _schedule_downstream_tasks( assert task assert task.dag - # Get a partial DAG with just the specific tasks we want to examine. - # In order for dep checks to work correctly, we include ourself (so - # TriggerRuleDep can check the state of the task we just executed). - partial_dag = task.dag.partial_subset( - task.downstream_task_ids, - include_downstream=True, - include_upstream=False, - include_direct_upstream=True, - ) - - dag_run.dag = partial_dag + # Previously, this section used task.dag.partial_subset to retrieve a partial DAG. + # However, this approach is unsafe as it can result in incomplete or incorrect task execution, + # leading to potential bad cases. As a result, the operation has been removed. + # For more details, refer to the discussion in PR #[https://github.com/apache/airflow/pull/42582]. + dag_run.dag = task.dag info = dag_run.task_instance_scheduling_decisions(session) skippable_task_ids = { - task_id for task_id in partial_dag.task_ids if task_id not in task.downstream_task_ids + task_id for task_id in task.dag.task_ids if task_id not in task.downstream_task_ids } schedulable_tis = [ diff --git a/tests/models/test_taskinstance.py b/tests/models/test_taskinstance.py index def462c2e1270..eee3c6c591e07 100644 --- a/tests/models/test_taskinstance.py +++ b/tests/models/test_taskinstance.py @@ -77,7 +77,7 @@ from airflow.models.xcom import LazyXComSelectSequence, XCom from airflow.notifications.basenotifier import BaseNotifier from airflow.operators.empty import EmptyOperator -from airflow.operators.python import PythonOperator +from airflow.operators.python import BranchPythonOperator, PythonOperator from airflow.providers.standard.operators.bash import BashOperator from airflow.sensors.base import BaseSensorOperator from airflow.sensors.python import PythonSensor @@ -5258,6 +5258,80 @@ def last_task(): assert "3 downstream tasks scheduled from follow-on schedule" in caplog.text +@pytest.mark.skip_if_database_isolation_mode +def test_one_success_task_in_mini_scheduler_if_upstreams_are_done(dag_maker, caplog, session): + """Test that mini scheduler with one_success task""" + with dag_maker() as dag: + branch = BranchPythonOperator(task_id="branch", python_callable=lambda: "task_run") + task_run = BashOperator(task_id="task_run", bash_command="echo 0") + task_skip = BashOperator(task_id="task_skip", bash_command="echo 0") + task_1 = BashOperator(task_id="task_1", bash_command="echo 0") + task_one_success = BashOperator( + task_id="task_one_success", bash_command="echo 0", trigger_rule="one_success" + ) + task_2 = BashOperator(task_id="task_2", bash_command="echo 0") + + task_1 >> task_2 + branch >> task_skip + branch >> task_run + task_run >> task_one_success + task_skip >> task_one_success + task_one_success >> task_2 + task_skip >> task_2 + + dr = dag_maker.create_dagrun() + + branch = dr.get_task_instance(task_id="branch") + task_1 = dr.get_task_instance(task_id="task_1") + task_skip = dr.get_task_instance(task_id="task_skip") + branch.state = State.SUCCESS + task_1.state = State.SUCCESS + task_skip.state = State.SKIPPED + session.merge(branch) + session.merge(task_1) + session.merge(task_skip) + session.commit() + task_1.refresh_from_task(dag.get_task("task_1")) + task_1.schedule_downstream_tasks(session=session) + + branch = dr.get_task_instance(task_id="branch") + task_run = dr.get_task_instance(task_id="task_run") + task_skip = dr.get_task_instance(task_id="task_skip") + task_1 = dr.get_task_instance(task_id="task_1") + task_one_success = dr.get_task_instance(task_id="task_one_success") + task_2 = dr.get_task_instance(task_id="task_2") + assert branch.state == State.SUCCESS + assert task_run.state == State.NONE + assert task_skip.state == State.SKIPPED + assert task_1.state == State.SUCCESS + # task_one_success should not be scheduled + assert task_one_success.state == State.NONE + assert task_2.state == State.SKIPPED + assert "0 downstream tasks scheduled from follow-on schedule" in caplog.text + + task_run = dr.get_task_instance(task_id="task_run") + task_run.state = State.SUCCESS + session.merge(task_run) + session.commit() + task_run.refresh_from_task(dag.get_task("task_run")) + task_run.schedule_downstream_tasks(session=session) + + branch = dr.get_task_instance(task_id="branch") + task_run = dr.get_task_instance(task_id="task_run") + task_skip = dr.get_task_instance(task_id="task_skip") + task_1 = dr.get_task_instance(task_id="task_1") + task_one_success = dr.get_task_instance(task_id="task_one_success") + task_2 = dr.get_task_instance(task_id="task_2") + assert branch.state == State.SUCCESS + assert task_run.state == State.SUCCESS + assert task_skip.state == State.SKIPPED + assert task_1.state == State.SUCCESS + # task_one_success should not be scheduled + assert task_one_success.state == State.SCHEDULED + assert task_2.state == State.SKIPPED + assert "1 downstream tasks scheduled from follow-on schedule" in caplog.text + + @pytest.mark.skip_if_database_isolation_mode # Does not work in db isolation mode def test_mini_scheduler_not_skip_mapped_downstream_until_all_upstreams_finish(dag_maker, session): with dag_maker(session=session): From 0c4ed7a58eeb5904b6fa06120532f9f0f344cd3f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 23 Oct 2024 17:27:52 +0800 Subject: [PATCH 068/258] Ignore attr-defined for compat import (#43301) --- providers/src/airflow/providers/openlineage/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/src/airflow/providers/openlineage/utils/utils.py b/providers/src/airflow/providers/openlineage/utils/utils.py index 424dc90c8f42b..a00552eed251f 100644 --- a/providers/src/airflow/providers/openlineage/utils/utils.py +++ b/providers/src/airflow/providers/openlineage/utils/utils.py @@ -678,7 +678,7 @@ def translate_airflow_asset(asset: Asset, lineage_context) -> OpenLineageDataset from airflow.assets import _get_normalized_scheme except ModuleNotFoundError: try: - from airflow.datasets import _get_normalized_scheme # type: ignore[no-redef] + from airflow.datasets import _get_normalized_scheme # type: ignore[no-redef, attr-defined] except ImportError: return None From 6786032d4b69e9b3a44165daf04d2270d40271ad Mon Sep 17 00:00:00 2001 From: Maciej Obuchowski Date: Wed, 23 Oct 2024 12:12:47 +0200 Subject: [PATCH 069/258] Remove sqlalchemy-redshift dependency (#43271) * Remove sqlalchemy-redshift dependency from Amazon provider `sqlalchemy-redshift` is unused. It is also not compatible with sqlalchemy>2, so good riddance! * move redshift hook to use postgres connector Signed-off-by: Maciej Obuchowski --------- Signed-off-by: Maciej Obuchowski Co-authored-by: Ash Berlin-Taylor --- Dockerfile.ci | 2 +- dev/breeze/src/airflow_breeze/global_constants.py | 2 +- generated/provider_dependencies.json | 1 - .../src/airflow/providers/amazon/aws/hooks/redshift_sql.py | 2 +- providers/src/airflow/providers/amazon/provider.yaml | 1 - providers/tests/amazon/aws/hooks/test_redshift_sql.py | 2 +- 6 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index 3ddba289a807f..cdf80c9b91593 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1195,7 +1195,7 @@ ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" # NOTE! When you want to make sure dependencies are installed from scratch in your PR after removing # some dependencies, you also need to set "disable image cache" in your PR to make sure the image is # not built using the "main" version of those dependencies. -ARG DEPENDENCIES_EPOCH_NUMBER="11" +ARG DEPENDENCIES_EPOCH_NUMBER="12" # Make sure noninteractive debian install is used and language variables set ENV PYTHON_BASE_IMAGE=${PYTHON_BASE_IMAGE} \ diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py index 3cc937f775775..e90632330e25e 100644 --- a/dev/breeze/src/airflow_breeze/global_constants.py +++ b/dev/breeze/src/airflow_breeze/global_constants.py @@ -548,7 +548,7 @@ def get_airflow_extras(): # END OF EXTRAS LIST UPDATED BY PRE COMMIT ] -CHICKEN_EGG_PROVIDERS = " ".join(["standard"]) +CHICKEN_EGG_PROVIDERS = " ".join(["standard amazon"]) BASE_PROVIDERS_COMPATIBILITY_CHECKS: list[dict[str, str | list[str]]] = [ diff --git a/generated/provider_dependencies.json b/generated/provider_dependencies.json index 7bfbd76acad7f..d2c69029cc08b 100644 --- a/generated/provider_dependencies.json +++ b/generated/provider_dependencies.json @@ -38,7 +38,6 @@ "jsonpath_ng>=1.5.3", "python3-saml>=1.16.0", "redshift_connector>=2.0.918", - "sqlalchemy_redshift>=0.8.6", "watchtower>=3.0.0,!=3.3.0,<4" ], "devel-deps": [ diff --git a/providers/src/airflow/providers/amazon/aws/hooks/redshift_sql.py b/providers/src/airflow/providers/amazon/aws/hooks/redshift_sql.py index 2b3b75dca6d45..bfdf807318a20 100644 --- a/providers/src/airflow/providers/amazon/aws/hooks/redshift_sql.py +++ b/providers/src/airflow/providers/amazon/aws/hooks/redshift_sql.py @@ -163,7 +163,7 @@ def get_uri(self) -> str: # Compatibility: The 'create' factory method was added in SQLAlchemy 1.4 # to replace calling the default URL constructor directly. create_url = getattr(URL, "create", URL) - return str(create_url(drivername="redshift+redshift_connector", **conn_params)) + return str(create_url(drivername="postgresql", **conn_params)) def get_sqlalchemy_engine(self, engine_kwargs=None): """Overridden to pass Redshift-specific arguments.""" diff --git a/providers/src/airflow/providers/amazon/provider.yaml b/providers/src/airflow/providers/amazon/provider.yaml index a8d182b904954..11e6dc8db753f 100644 --- a/providers/src/airflow/providers/amazon/provider.yaml +++ b/providers/src/airflow/providers/amazon/provider.yaml @@ -106,7 +106,6 @@ dependencies: - watchtower>=3.0.0,!=3.3.0,<4 - jsonpath_ng>=1.5.3 - redshift_connector>=2.0.918 - - sqlalchemy_redshift>=0.8.6 - asgiref>=2.3.0 - PyAthena>=3.0.10 - jmespath>=0.7.0 diff --git a/providers/tests/amazon/aws/hooks/test_redshift_sql.py b/providers/tests/amazon/aws/hooks/test_redshift_sql.py index aced8cae13674..70cd69c40ed80 100644 --- a/providers/tests/amazon/aws/hooks/test_redshift_sql.py +++ b/providers/tests/amazon/aws/hooks/test_redshift_sql.py @@ -50,7 +50,7 @@ def setup_method(self): self.db_hook.get_connection.return_value = self.connection def test_get_uri(self): - expected = "redshift+redshift_connector://login:password@host:5439/dev" + expected = "postgresql://login:password@host:5439/dev" x = self.db_hook.get_uri() assert x == expected From 8ab555c363dcac6dce65be8a0b2f71dde9e77f7e Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 23 Oct 2024 12:56:13 +0200 Subject: [PATCH 070/258] Fixes behaviour of example dag tests for main/other branches (#43273) While the #43260 attempted to address the problem where example dag importability tests should skip provider tests on non-main, it did not actually solve the problem. While debugging it, it turned out that since #42505, the provider tests were not executed in main "at all" - the "providers" directory was not included in the list of places to check for the example dags (they were in "airflow" in v2-10-test") this is why it "looked like" the solution worked in "main". This PR fixes both problems: * brings back importability of provider's example_dags in main branch * properly excludes the providers examples in non-main branch --- tests/always/test_example_dags.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/always/test_example_dags.py b/tests/always/test_example_dags.py index e45e1427a321b..7ea1ac9609bd2 100644 --- a/tests/always/test_example_dags.py +++ b/tests/always/test_example_dags.py @@ -114,7 +114,14 @@ def get_python_excluded_providers_folders() -> list[str]: def example_not_excluded_dags(xfail_db_exception: bool = False): - example_dirs = ["airflow/**/example_dags/example_*.py", "tests/system/**/example_*.py"] + example_dirs = [ + "airflow/**/example_dags/example_*.py", + "tests/system/**/example_*.py", + "providers/**/example_*.py", + ] + + default_branch = os.environ.get("DEFAULT_BRANCH", "main") + include_providers = default_branch == "main" suspended_providers_folders = get_suspended_providers_folders() current_python_excluded_providers_folders = get_python_excluded_providers_folders() @@ -129,14 +136,7 @@ def example_not_excluded_dags(xfail_db_exception: bool = False): for provider in current_python_excluded_providers_folders ] providers_folders = tuple([AIRFLOW_SOURCES_ROOT.joinpath(pp).as_posix() for pp in PROVIDERS_PREFIXES]) - - default_branch = os.environ.get("DEFAULT_BRANCH", "main") - include_providers = default_branch == "main" - for example_dir in example_dirs: - if not include_providers and "providers/" in example_dir: - print(f"Skipping {example_dir} because providers are not included for {default_branch} branch.") - continue candidates = glob(f"{AIRFLOW_SOURCES_ROOT.as_posix()}/{example_dir}", recursive=True) for candidate in sorted(candidates): param_marks = [] @@ -157,6 +157,11 @@ def example_not_excluded_dags(xfail_db_exception: bool = False): param_marks.append(pytest.mark.skip(reason=reason)) if candidate.startswith(providers_folders): + if not include_providers: + print( + f"Skipping {candidate} because providers are not included for {default_branch} branch." + ) + continue # Do not raise an error for airflow.exceptions.RemovedInAirflow3Warning. # We should not rush to enforce new syntax updates in providers # because a version of Airflow that deprecates certain features may not yet be released. From e49033d930da515d189dea7ee01bdc8575f371e1 Mon Sep 17 00:00:00 2001 From: Danny Liu Date: Wed, 23 Oct 2024 04:09:19 -0700 Subject: [PATCH 071/258] PyDocStyle Check - PT005: Fixture returns a value, remove leading underscore (#43292) * style: remove underscore from function name * style: remove PT005 from ignore list --- providers/tests/ssh/operators/test_ssh.py | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/tests/ssh/operators/test_ssh.py b/providers/tests/ssh/operators/test_ssh.py index 810f5363d1bf5..bedd6c5373b42 100644 --- a/providers/tests/ssh/operators/test_ssh.py +++ b/providers/tests/ssh/operators/test_ssh.py @@ -66,7 +66,7 @@ def setup_method(self): # Make sure nothing in this test actually connects to SSH -- that's for hook tests. @pytest.fixture(autouse=True) - def _patch_exec_ssh_client(self): + def patch_exec_ssh_client(self): with mock.patch.object(self.hook, "exec_ssh_client_command") as exec_ssh_client_command: self.exec_ssh_client_command = exec_ssh_client_command exec_ssh_client_command.return_value = (0, b"airflow", "") diff --git a/pyproject.toml b/pyproject.toml index 3eb574c92c216..b97d86540b8b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -295,7 +295,6 @@ ignore = [ "E731", # Do not assign a lambda expression, use a def "TCH003", # Do not move imports from stdlib to TYPE_CHECKING block "PT004", # Fixture does not return anything, add leading underscore - "PT005", # Fixture returns a value, remove leading underscore "PT006", # Wrong type of names in @pytest.mark.parametrize "PT007", # Wrong type of values in @pytest.mark.parametrize "PT013", # silly rule prohibiting e.g. `from pytest import param` From 076da924696dc7d8814a38f442e9ca3eb3e90634 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 23 Oct 2024 14:59:50 +0200 Subject: [PATCH 072/258] Check python version that was used to install pre-commit venvs (#43282) Since we moved to Python 3.9 and started usign 3.9+ only features line functools.cache, having Python 3.8 as default python version will result in virtualenvs created with outdated python. This pre-commit checks what is the global version of python3 and it will complain if it is lower than 3.9. This is done via explicit call of "python3", because this is what pre-commit does. Also global setting is needed because otherwise new virtualenvs created by pre-commit might be created using lower python version. --- .pre-commit-config.yaml | 6 ++ contributing-docs/08_static_code_checks.rst | 2 + .../doc/images/output_static-checks.svg | 2 +- .../doc/images/output_static-checks.txt | 2 +- .../src/airflow_breeze/pre_commit_ids.py | 1 + .../ci/pre_commit/check_min_python_version.py | 68 +++++++++++++++++++ 6 files changed, 79 insertions(+), 2 deletions(-) create mode 100755 scripts/ci/pre_commit/check_min_python_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index caad954c2e6fd..f6112cea4c20b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -168,6 +168,12 @@ repos: \.cfg$|\.conf$|\.ini$|\.ldif$|\.properties$|\.readthedocs$|\.service$|\.tf$|Dockerfile.*$ - repo: local hooks: + - id: check-min-python-version + name: Check minimum Python version + entry: ./scripts/ci/pre_commit/check_min_python_version.py + language: python + additional_dependencies: ['rich>=12.4.4'] + require_serial: true - id: update-common-sql-api-stubs name: Check and update common.sql API stubs entry: ./scripts/ci/pre_commit/update_common_sql_api_stubs.py diff --git a/contributing-docs/08_static_code_checks.rst b/contributing-docs/08_static_code_checks.rst index aa9955da1afdd..fc0d4280b9d0e 100644 --- a/contributing-docs/08_static_code_checks.rst +++ b/contributing-docs/08_static_code_checks.rst @@ -194,6 +194,8 @@ require Breeze Docker image to be built locally. +-----------------------------------------------------------+--------------------------------------------------------+---------+ | check-merge-conflict | Check that merge conflicts are not being committed | | +-----------------------------------------------------------+--------------------------------------------------------+---------+ +| check-min-python-version | Check minimum Python version | | ++-----------------------------------------------------------+--------------------------------------------------------+---------+ | check-newsfragments-are-valid | Check newsfragments are valid | | +-----------------------------------------------------------+--------------------------------------------------------+---------+ | check-no-airflow-deprecation-in-providers | Do not use DeprecationWarning in providers | | diff --git a/dev/breeze/doc/images/output_static-checks.svg b/dev/breeze/doc/images/output_static-checks.svg index 96a324e22c427..c18571e592e9a 100644 --- a/dev/breeze/doc/images/output_static-checks.svg +++ b/dev/breeze/doc/images/output_static-checks.svg @@ -345,7 +345,7 @@ check-hatch-build-order | check-hooks-apply | check-incorrect-use-of-LoggingMixin | check-init-decorator-arguments | check-integrations-list-consistent |           check-lazy-logging | check-links-to-example-dags-do-not-use-hardcoded-versions |  -check-merge-conflict | check-newsfragments-are-valid |                            +check-merge-conflict | check-min-python-version | check-newsfragments-are-valid | check-no-airflow-deprecation-in-providers | check-no-providers-in-core-examples | check-only-new-session-with-provide-session |                                     check-persist-credentials-disabled-in-github-workflows |                          diff --git a/dev/breeze/doc/images/output_static-checks.txt b/dev/breeze/doc/images/output_static-checks.txt index b0c56ad6640b1..53c5351a1c504 100644 --- a/dev/breeze/doc/images/output_static-checks.txt +++ b/dev/breeze/doc/images/output_static-checks.txt @@ -1 +1 @@ -b4becd0ef113ac04210350ea8f9f98b9 +53b7f32a93cb7dec849138d404c47f6c diff --git a/dev/breeze/src/airflow_breeze/pre_commit_ids.py b/dev/breeze/src/airflow_breeze/pre_commit_ids.py index 91b3ad06330ac..9e46069eab306 100644 --- a/dev/breeze/src/airflow_breeze/pre_commit_ids.py +++ b/dev/breeze/src/airflow_breeze/pre_commit_ids.py @@ -62,6 +62,7 @@ "check-lazy-logging", "check-links-to-example-dags-do-not-use-hardcoded-versions", "check-merge-conflict", + "check-min-python-version", "check-newsfragments-are-valid", "check-no-airflow-deprecation-in-providers", "check-no-providers-in-core-examples", diff --git a/scripts/ci/pre_commit/check_min_python_version.py b/scripts/ci/pre_commit/check_min_python_version.py new file mode 100755 index 0000000000000..825b899241816 --- /dev/null +++ b/scripts/ci/pre_commit/check_min_python_version.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# 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 + +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.resolve())) + +from common_precommit_utils import console + +# update this version when we switch to a newer version of Python +required_version = tuple(map(int, "3.9".split("."))) +required_version_str = f"{required_version[0]}.{required_version[1]}" +global_version = tuple( + map( + int, + subprocess.run( + [ + "python3", + "-c", + 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")', + ], + capture_output=True, + text=True, + check=True, + ) + .stdout.strip() + .split("."), + ) +) + + +if global_version < required_version: + console.print(f"[red]Python {required_version_str} or higher is required to install pre-commit.\n") + console.print(f"[green]Current version is {global_version}\n") + console.print( + "[bright_yellow]Please follow those steps:[/]\n\n" + f" * make sure that `python3 --version` is at least {required_version_str}\n" + f" * run `pre-commit clean`\n" + f" * run `pre-commit install --install-hooks`\n\n" + ) + console.print( + "There are various ways you can set `python3` to point to a newer version of Python.\n\n" + f"For example run `pyenv global {required_version_str}` if you use pyenv, or\n" + f"you can use venv with python {required_version_str} when you use pre-commit, or\n" + "you can use `update-alternatives` if you use Ubuntu, or\n" + "you can set `PATH` to point to the newer version of Python.\n\n" + ) + sys.exit(1) +else: + console.print(f"Python version is sufficient: {required_version_str}") From 84ff10bf06cf1a529169990d25c00a33d06e740e Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 23 Oct 2024 15:12:35 +0200 Subject: [PATCH 073/258] Upgrade FAB to 4.5.1 (#43251) FAB 4.5.1 has been released in September with a few small fixes. This change updates fab to 4.5.1 including changing the rate limiter creation that is vendored in. It has been changed in https://github.com/dpgaspar/Flask-AppBuilder/pull/2254 and relased in 4.5.1. That's the only dfference in security manager between 4.5.0 and 4.5.1. --- airflow/www/security_manager.py | 5 +++-- dev/breeze/tests/test_packages.py | 6 +++--- generated/provider_dependencies.json | 2 +- providers/src/airflow/providers/fab/provider.yaml | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/airflow/www/security_manager.py b/airflow/www/security_manager.py index 77fd653b5f416..cd2f9563fe219 100644 --- a/airflow/www/security_manager.py +++ b/airflow/www/security_manager.py @@ -108,8 +108,9 @@ def before_request(): g.user = get_auth_manager().get_user() def create_limiter(self) -> Limiter: - limiter = Limiter(key_func=get_remote_address) - limiter.init_app(self.appbuilder.get_app) + app = self.appbuilder.get_app + limiter = Limiter(key_func=app.config.get("RATELIMIT_KEY_FUNC", get_remote_address)) + limiter.init_app(app) return limiter def register_views(self): diff --git a/dev/breeze/tests/test_packages.py b/dev/breeze/tests/test_packages.py index e315a40ed1c22..6cac54a14c31a 100644 --- a/dev/breeze/tests/test_packages.py +++ b/dev/breeze/tests/test_packages.py @@ -169,7 +169,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1", "apache-airflow>=2.9.0", - "flask-appbuilder==4.5.0", + "flask-appbuilder==4.5.1", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", @@ -183,7 +183,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1.dev0", "apache-airflow>=2.9.0.dev0", - "flask-appbuilder==4.5.0", + "flask-appbuilder==4.5.1", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", @@ -197,7 +197,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1b0", "apache-airflow>=2.9.0b0", - "flask-appbuilder==4.5.0", + "flask-appbuilder==4.5.1", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", diff --git a/generated/provider_dependencies.json b/generated/provider_dependencies.json index d2c69029cc08b..a5c97c51237a0 100644 --- a/generated/provider_dependencies.json +++ b/generated/provider_dependencies.json @@ -571,7 +571,7 @@ "deps": [ "apache-airflow-providers-common-compat>=1.2.1", "apache-airflow>=2.9.0", - "flask-appbuilder==4.5.0", + "flask-appbuilder==4.5.1", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", diff --git a/providers/src/airflow/providers/fab/provider.yaml b/providers/src/airflow/providers/fab/provider.yaml index 4b72c0ab9a5ee..86b0598df4533 100644 --- a/providers/src/airflow/providers/fab/provider.yaml +++ b/providers/src/airflow/providers/fab/provider.yaml @@ -55,7 +55,7 @@ dependencies: # Every time we update FAB version here, please make sure that you review the classes and models in # `airflow/providers/fab/auth_manager/security_manager/override.py` with their upstream counterparts. # In particular, make sure any breaking changes, for example any new methods, are accounted for. - - flask-appbuilder==4.5.0 + - flask-appbuilder==4.5.1 - flask-login>=0.6.2 - google-re2>=1.0 - jmespath>=0.7.0 From b84249d9ffff49f91ccef2e42f5528fec90dc17a Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Wed, 23 Oct 2024 06:15:51 -0700 Subject: [PATCH 074/258] Implement basic backfill dry run (#43241) Add simple dry run functionality. Does not check whether these runs exist. Just doing the basic thing first. Sample console output. --- airflow/cli/cli_config.py | 6 ++++ airflow/cli/commands/backfill_command.py | 39 +++++++++++++++++++++++- airflow/models/backfill.py | 27 +++++++++------- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/airflow/cli/cli_config.py b/airflow/cli/cli_config.py index 5d1fe9ba8e51e..b818ea08d5714 100644 --- a/airflow/cli/cli_config.py +++ b/airflow/cli/cli_config.py @@ -325,6 +325,11 @@ def string_lower_type(val): type=positive_int(allow_zero=False), help="Max active runs for this backfill.", ) +ARG_BACKFILL_DRY_RUN = Arg( + ("--dry-run",), + help="Perform a dry run", + action="store_true", +) # misc @@ -1030,6 +1035,7 @@ class GroupCommand(NamedTuple): ARG_DAG_RUN_CONF, ARG_RUN_BACKWARDS, ARG_MAX_ACTIVE_RUNS, + ARG_BACKFILL_DRY_RUN, ), ), ) diff --git a/airflow/cli/commands/backfill_command.py b/airflow/cli/commands/backfill_command.py index 8714ed5585004..378c6ea5f9502 100644 --- a/airflow/cli/commands/backfill_command.py +++ b/airflow/cli/commands/backfill_command.py @@ -21,10 +21,31 @@ import signal from airflow import settings -from airflow.models.backfill import _create_backfill +from airflow.models.backfill import _create_backfill, _get_info_list +from airflow.models.serialized_dag import SerializedDagModel from airflow.utils import cli as cli_utils from airflow.utils.cli import sigint_handler from airflow.utils.providers_configuration_loader import providers_configuration_loaded +from airflow.utils.session import create_session + + +def _do_dry_run(*, params, dag_id, from_date, to_date, reverse): + print("Performing dry run of backfill.") + print("Printing params:") + for k, v in params.items(): + print(f" - {k} = {v}") + with create_session() as session: + serdag = session.get(SerializedDagModel, dag_id) + + info_list = _get_info_list( + dag=serdag.dag, + from_date=from_date, + to_date=to_date, + reverse=reverse, + ) + print("Logical dates to be attempted:") + for info in info_list: + print(f" - {info.logical_date}") @cli_utils.action_cli @@ -34,6 +55,22 @@ def create_backfill(args) -> None: logging.basicConfig(level=settings.LOGGING_LEVEL, format=settings.SIMPLE_LOG_FORMAT) signal.signal(signal.SIGTERM, sigint_handler) + if args.dry_run: + _do_dry_run( + params=dict( + dag_id=args.dag, + from_date=args.from_date, + to_date=args.to_date, + max_active_runs=args.max_active_runs, + reverse=args.run_backwards, + dag_run_conf=args.dag_run_conf, + ), + dag_id=args.dag, + from_date=args.from_date, + to_date=args.to_date, + reverse=args.run_backwards, + ) + return _create_backfill( dag_id=args.dag, from_date=args.from_date, diff --git a/airflow/models/backfill.py b/airflow/models/backfill.py index 37a9533113067..53a9fca1df11a 100644 --- a/airflow/models/backfill.py +++ b/airflow/models/backfill.py @@ -175,6 +175,13 @@ def _create_backfill_dag_run(dag, info, backfill_id, dag_run_conf, backfill_sort ) +def _get_info_list(*, dag, from_date, to_date, reverse): + dagrun_info_list = dag.iter_dagrun_infos_between(from_date, to_date) + if reverse: + dagrun_info_list = reversed([x for x in dag.iter_dagrun_infos_between(from_date, to_date)]) + return dagrun_info_list + + def _create_backfill( *, dag_id: str, @@ -200,6 +207,14 @@ def _create_backfill( f"There can be only one running backfill per dag." ) + dag = serdag.dag + depends_on_past = any(x.depends_on_past for x in dag.tasks) + if depends_on_past: + if reverse is True: + raise ValueError( + "Backfill cannot be run in reverse when the dag has tasks where depends_on_past=True" + ) + br = Backfill( dag_id=dag_id, from_date=from_date, @@ -210,18 +225,8 @@ def _create_backfill( session.add(br) session.commit() - dag = serdag.dag - depends_on_past = any(x.depends_on_past for x in dag.tasks) - if depends_on_past: - if reverse is True: - raise ValueError( - "Backfill cannot be run in reverse when the dag has tasks where depends_on_past=True" - ) - backfill_sort_ordinal = 0 - dagrun_info_list = dag.iter_dagrun_infos_between(from_date, to_date) - if reverse: - dagrun_info_list = reversed([x for x in dag.iter_dagrun_infos_between(from_date, to_date)]) + dagrun_info_list = _get_info_list(dag=dag, from_date=from_date, to_date=to_date, reverse=reverse) for info in dagrun_info_list: backfill_sort_ordinal += 1 session.commit() From f4d9a1b068ab3fed4e527bd8dbc67043caae620e Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Wed, 23 Oct 2024 15:21:10 +0200 Subject: [PATCH 075/258] Add isolation mode exclusion for mapped operator test (#43297) This test has been cherry-picked to v2-10-test and should be excluded from isolation mode. --- tests/models/test_mappedoperator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/models/test_mappedoperator.py b/tests/models/test_mappedoperator.py index c7bd11b966758..98fa304917b1b 100644 --- a/tests/models/test_mappedoperator.py +++ b/tests/models/test_mappedoperator.py @@ -731,6 +731,7 @@ def test_expand_mapped_task_instance_with_named_index( assert indices == expected_rendered_names +@pytest.mark.skip_if_database_isolation_mode # Does not work in db isolation mode @pytest.mark.parametrize( "create_mapped_task", [ From 3b0cb76b6d8c4dcbf0c4b1425a16d73660bb3f1f Mon Sep 17 00:00:00 2001 From: Oliver Wannenwetsch <97086319+OliverWannenwetsch@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:31:03 +0200 Subject: [PATCH 076/258] Made usage of Path explicit for Edge Worker pid files (#43308) * Made usage of Path explicit for Edge Worker pid files * Updated version number of Edge Worker provider package * Fixed pytests by findings of mypy --- providers/src/airflow/providers/edge/CHANGELOG.rst | 8 ++++++++ providers/src/airflow/providers/edge/cli/edge_command.py | 6 +++--- providers/src/airflow/providers/edge/provider.yaml | 4 ++-- providers/tests/edge/cli/test_edge_command.py | 2 +- providers/tests/edge/models/test_edge_worker.py | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/providers/src/airflow/providers/edge/CHANGELOG.rst b/providers/src/airflow/providers/edge/CHANGELOG.rst index bdb34450d1c3f..56f2dc1243c5c 100644 --- a/providers/src/airflow/providers/edge/CHANGELOG.rst +++ b/providers/src/airflow/providers/edge/CHANGELOG.rst @@ -27,6 +27,14 @@ Changelog --------- +0.2.2re0 +......... + +Misc +~~~~ + +* ``Fixed type confusion for PID file paths (#43308)`` + 0.2.1re0 ......... diff --git a/providers/src/airflow/providers/edge/cli/edge_command.py b/providers/src/airflow/providers/edge/cli/edge_command.py index 37ce931957bb0..1d5013703c3ba 100644 --- a/providers/src/airflow/providers/edge/cli/edge_command.py +++ b/providers/src/airflow/providers/edge/cli/edge_command.py @@ -95,9 +95,9 @@ def _pid_file_path(pid_file: str | None) -> str: return cli_utils.setup_locations(process=EDGE_WORKER_PROCESS_NAME, pid=pid_file)[0] -def _write_pid_to_pidfile(pid_file_path): +def _write_pid_to_pidfile(pid_file_path: str): """Write PIDs for Edge Workers to disk, handling existing PID files.""" - if pid_file_path.exists(): + if Path(pid_file_path).exists(): # Handle existing PID files on disk logger.info("An existing PID file has been found: %s.", pid_file_path) pid_stored_in_pid_file = read_pid_from_pidfile(pid_file_path) @@ -142,7 +142,7 @@ class _EdgeWorkerCli: def __init__( self, - pid_file_path: Path, + pid_file_path: str, hostname: str, queues: list[str] | None, concurrency: int, diff --git a/providers/src/airflow/providers/edge/provider.yaml b/providers/src/airflow/providers/edge/provider.yaml index f6e4cbde93d91..32952295a9769 100644 --- a/providers/src/airflow/providers/edge/provider.yaml +++ b/providers/src/airflow/providers/edge/provider.yaml @@ -23,10 +23,10 @@ description: | Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites state: not-ready -source-date-epoch: 1729588146 +source-date-epoch: 1729683247 # note that those versions are maintained by release manager - do not update them manually versions: - - 0.2.1pre0 + - 0.2.2pre0 dependencies: - apache-airflow>=2.10.0 diff --git a/providers/tests/edge/cli/test_edge_command.py b/providers/tests/edge/cli/test_edge_command.py index 5014edea11d4a..51aad5806802e 100644 --- a/providers/tests/edge/cli/test_edge_command.py +++ b/providers/tests/edge/cli/test_edge_command.py @@ -120,7 +120,7 @@ def returncode(self): @pytest.fixture def worker_with_job(self, tmp_path: Path, dummy_joblist: list[_Job]) -> _EdgeWorkerCli: - test_worker = _EdgeWorkerCli(tmp_path / "dummy.pid", "dummy", None, 8, 5, 5) + test_worker = _EdgeWorkerCli(str(tmp_path / "dummy.pid"), "dummy", None, 8, 5, 5) test_worker.jobs = dummy_joblist return test_worker diff --git a/providers/tests/edge/models/test_edge_worker.py b/providers/tests/edge/models/test_edge_worker.py index d67cfdbb2cd61..20e394ffd5767 100644 --- a/providers/tests/edge/models/test_edge_worker.py +++ b/providers/tests/edge/models/test_edge_worker.py @@ -39,7 +39,7 @@ class TestEdgeWorker: @pytest.fixture def cli_worker(self, tmp_path: Path) -> _EdgeWorkerCli: - test_worker = _EdgeWorkerCli(tmp_path / "dummy.pid", "dummy", None, 8, 5, 5) + test_worker = _EdgeWorkerCli(str(tmp_path / "dummy.pid"), "dummy", None, 8, 5, 5) return test_worker @pytest.fixture(autouse=True) From ca2c809b30607af7ee320510587fe93b1ab90218 Mon Sep 17 00:00:00 2001 From: Lennox Stevenson Date: Wed, 23 Oct 2024 09:50:50 -0400 Subject: [PATCH 077/258] feat: sensor to check status of Dataform action (#43055) Adds a new sensor to check the status of a WorkflowInvocationAction in Google Cloud Dataform. Heavily based on theDataformWorkflowInvocationStateSensor which already exists. Useful for checking the status of a specific target within a Dataform workflow invocation and taking action based on the status. --- .../operators/cloud/dataform.rst | 12 ++ .../google/cloud/sensors/dataform.py | 75 +++++++++ .../google/cloud/sensors/test_dataform.py | 150 ++++++++++++++++++ .../google/cloud/dataform/example_dataform.py | 40 ++++- 4 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 providers/tests/google/cloud/sensors/test_dataform.py diff --git a/docs/apache-airflow-providers-google/operators/cloud/dataform.rst b/docs/apache-airflow-providers-google/operators/cloud/dataform.rst index 09d8a6e6b8f93..208035af53c3c 100644 --- a/docs/apache-airflow-providers-google/operators/cloud/dataform.rst +++ b/docs/apache-airflow-providers-google/operators/cloud/dataform.rst @@ -95,6 +95,12 @@ We have possibility to run this operation in the sync mode and async, for async a sensor: :class:`~airflow.providers.google.cloud.operators.dataform.DataformWorkflowInvocationStateSensor` +We also have a sensor to check the status of a particular action for a workflow invocation triggered +asynchronously. + +:class:`~airflow.providers.google.cloud.operators.dataform.DataformWorkflowInvocationActionStateSensor` + + .. exampleinclude:: /../../providers/tests/system/google/cloud/dataform/example_dataform.py :language: python :dedent: 4 @@ -107,6 +113,12 @@ a sensor: :start-after: [START howto_operator_create_workflow_invocation_async] :end-before: [END howto_operator_create_workflow_invocation_async] +.. exampleinclude:: /../../providers/tests/system/google/cloud/dataform/example_dataform.py + :language: python + :dedent: 4 + :start-after: [START howto_operator_create_workflow_invocation_action_async] + :end-before: [END howto_operator_create_workflow_invocation_action_async] + Get Workflow Invocation ----------------------- diff --git a/providers/src/airflow/providers/google/cloud/sensors/dataform.py b/providers/src/airflow/providers/google/cloud/sensors/dataform.py index 0e4676749eb47..17d1351404fa4 100644 --- a/providers/src/airflow/providers/google/cloud/sensors/dataform.py +++ b/providers/src/airflow/providers/google/cloud/sensors/dataform.py @@ -103,3 +103,78 @@ def poke(self, context: Context) -> bool: raise AirflowException(message) return workflow_status in self.expected_statuses + + +class DataformWorkflowInvocationActionStateSensor(BaseSensorOperator): + """ + Checks for the status of a Workflow Invocation Action in Google Cloud Dataform. + + :param project_id: Required, the Google Cloud project ID in which to start a job. + If set to None or missing, the default project_id from the Google Cloud connection is used. + :param region: Required, The location of the Dataform workflow invocation (for example europe-west1). + :param repository_id: Required. The ID of the Dataform repository that the task belongs to. + :param workflow_invocation_id: Required, ID of the workflow invocation to be checked. + :param target_name: Required. The name of the target to be checked in the workflow. + :param expected_statuses: The expected state of the action. + See: + https://cloud.google.com/python/docs/reference/dataform/latest/google.cloud.dataform_v1beta1.types.WorkflowInvocationAction.State + :param failure_statuses: State that will terminate the sensor with an exception + :param gcp_conn_id: The connection ID to use connecting to Google Cloud. + :param impersonation_chain: Optional service account to impersonate using short-term + credentials, or chained list of accounts required to get the access_token + of the last account in the list, which will be impersonated in the request. + If set as a string, the account must grant the originating account + the Service Account Token Creator IAM role. + If set as a sequence, the identities from the list must grant + Service Account Token Creator IAM role to the directly preceding identity, with first + account from the list granting this role to the originating account (templated). + """ + + template_fields: Sequence[str] = ("workflow_invocation_id",) + + def __init__( + self, + *, + project_id: str, + region: str, + repository_id: str, + workflow_invocation_id: str, + target_name: str, + expected_statuses: Iterable[int], + failure_statuses: Iterable[int], + gcp_conn_id: str = "google_cloud_default", + impersonation_chain: str | Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.repository_id = repository_id + self.workflow_invocation_id = workflow_invocation_id + self.project_id = project_id + self.region = region + self.target_name = target_name + self.expected_statuses = expected_statuses + self.failure_statuses = failure_statuses + self.gcp_conn_id = gcp_conn_id + self.impersonation_chain = impersonation_chain + self.hook: DataformHook | None = None + + def poke(self, context: Context) -> bool: + self.hook = DataformHook(gcp_conn_id=self.gcp_conn_id, impersonation_chain=self.impersonation_chain) + + workflow_invocation_actions = self.hook.query_workflow_invocation_actions( + project_id=self.project_id, + region=self.region, + repository_id=self.repository_id, + workflow_invocation_id=self.workflow_invocation_id, + ) + + for workflow_invocation_action in workflow_invocation_actions: + if workflow_invocation_action.target.name == self.target_name: + state = workflow_invocation_action.state + if state in self.failure_statuses: + raise AirflowException( + f"Workflow Invocation Action target {self.target_name} state is: {state}." + ) + return state in self.expected_statuses + + raise AirflowException(f"Workflow Invocation Action target {self.target_name} not found.") diff --git a/providers/tests/google/cloud/sensors/test_dataform.py b/providers/tests/google/cloud/sensors/test_dataform.py new file mode 100644 index 0000000000000..d3bcd8b6c9aad --- /dev/null +++ b/providers/tests/google/cloud/sensors/test_dataform.py @@ -0,0 +1,150 @@ +# +# 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 unittest import mock + +import pytest +from google.cloud.dataform_v1beta1.types import Target, WorkflowInvocationAction + +from airflow.exceptions import AirflowException +from airflow.providers.google.cloud.sensors.dataform import DataformWorkflowInvocationActionStateSensor + +TEST_TASK_ID = "task_id" +TEST_PROJECT_ID = "test_project" +TEST_REGION = "us-central1" +TEST_REPOSITORY_ID = "test_repository_id" +TEST_WORKFLOW_INVOCATION_ID = "test_workflow_invocation_id" +TEST_TARGET_NAME = "test_target_name" +TEST_GCP_CONN_ID = "test_gcp_conn_id" +TEST_IMPERSONATION_CHAIN = ["ACCOUNT_1", "ACCOUNT_2", "ACCOUNT_3"] + + +class TestDataformWorkflowInvocationActionStateSensor: + @pytest.mark.parametrize( + "expected_status, current_status, sensor_return", + [ + (WorkflowInvocationAction.State.SUCCEEDED, WorkflowInvocationAction.State.SUCCEEDED, True), + (WorkflowInvocationAction.State.SUCCEEDED, WorkflowInvocationAction.State.RUNNING, False), + ], + ) + @mock.patch("airflow.providers.google.cloud.sensors.dataform.DataformHook") + def test_poke( + self, + mock_hook: mock.MagicMock, + expected_status: WorkflowInvocationAction.State, + current_status: WorkflowInvocationAction.State, + sensor_return: bool, + ): + target = Target(database="", schema="", name=TEST_TARGET_NAME) + workflow_invocation_action = WorkflowInvocationAction(target=target, state=current_status) + mock_query_workflow_invocation_actions = mock_hook.return_value.query_workflow_invocation_actions + mock_query_workflow_invocation_actions.return_value = [workflow_invocation_action] + + task = DataformWorkflowInvocationActionStateSensor( + task_id=TEST_TASK_ID, + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + target_name=TEST_TARGET_NAME, + expected_statuses=[expected_status], + failure_statuses=[], + gcp_conn_id=TEST_GCP_CONN_ID, + impersonation_chain=TEST_IMPERSONATION_CHAIN, + ) + results = task.poke(mock.MagicMock()) + + assert sensor_return == results + + mock_hook.assert_called_once_with( + gcp_conn_id=TEST_GCP_CONN_ID, impersonation_chain=TEST_IMPERSONATION_CHAIN + ) + mock_query_workflow_invocation_actions.assert_called_once_with( + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + ) + + @mock.patch("airflow.providers.google.cloud.sensors.dataform.DataformHook") + def test_target_state_failure_raises_exception(self, mock_hook: mock.MagicMock): + target = Target(database="", schema="", name=TEST_TARGET_NAME) + workflow_invocation_action = WorkflowInvocationAction( + target=target, state=WorkflowInvocationAction.State.FAILED + ) + mock_query_workflow_invocation_actions = mock_hook.return_value.query_workflow_invocation_actions + mock_query_workflow_invocation_actions.return_value = [workflow_invocation_action] + + task = DataformWorkflowInvocationActionStateSensor( + task_id=TEST_TASK_ID, + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + target_name=TEST_TARGET_NAME, + expected_statuses=[WorkflowInvocationAction.State.SUCCEEDED], + failure_statuses=[WorkflowInvocationAction.State.FAILED], + gcp_conn_id=TEST_GCP_CONN_ID, + impersonation_chain=TEST_IMPERSONATION_CHAIN, + ) + + with pytest.raises(AirflowException): + task.poke(mock.MagicMock()) + + mock_hook.assert_called_once_with( + gcp_conn_id=TEST_GCP_CONN_ID, impersonation_chain=TEST_IMPERSONATION_CHAIN + ) + mock_query_workflow_invocation_actions.assert_called_once_with( + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + ) + + @mock.patch("airflow.providers.google.cloud.sensors.dataform.DataformHook") + def test_target_not_found_raises_exception(self, mock_hook: mock.MagicMock): + mock_query_workflow_invocation_actions = mock_hook.return_value.query_workflow_invocation_actions + mock_query_workflow_invocation_actions.return_value = [] + + task = DataformWorkflowInvocationActionStateSensor( + task_id=TEST_TASK_ID, + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + target_name=TEST_TARGET_NAME, + expected_statuses=[WorkflowInvocationAction.State.SUCCEEDED], + failure_statuses=[WorkflowInvocationAction.State.FAILED], + gcp_conn_id=TEST_GCP_CONN_ID, + impersonation_chain=TEST_IMPERSONATION_CHAIN, + ) + + with pytest.raises(AirflowException): + task.poke(mock.MagicMock()) + + mock_hook.assert_called_once_with( + gcp_conn_id=TEST_GCP_CONN_ID, impersonation_chain=TEST_IMPERSONATION_CHAIN + ) + mock_query_workflow_invocation_actions.assert_called_once_with( + project_id=TEST_PROJECT_ID, + region=TEST_REGION, + repository_id=TEST_REPOSITORY_ID, + workflow_invocation_id=TEST_WORKFLOW_INVOCATION_ID, + ) diff --git a/providers/tests/system/google/cloud/dataform/example_dataform.py b/providers/tests/system/google/cloud/dataform/example_dataform.py index e88a37caef2e6..f15e629b0f4b6 100644 --- a/providers/tests/system/google/cloud/dataform/example_dataform.py +++ b/providers/tests/system/google/cloud/dataform/example_dataform.py @@ -24,7 +24,7 @@ import os from datetime import datetime -from google.cloud.dataform_v1beta1 import WorkflowInvocation +from google.cloud.dataform_v1beta1 import WorkflowInvocation, WorkflowInvocationAction from airflow.models.dag import DAG from airflow.providers.google.cloud.operators.bigquery import BigQueryDeleteDatasetOperator @@ -45,7 +45,10 @@ DataformRemoveFileOperator, DataformWriteFileOperator, ) -from airflow.providers.google.cloud.sensors.dataform import DataformWorkflowInvocationStateSensor +from airflow.providers.google.cloud.sensors.dataform import ( + DataformWorkflowInvocationActionStateSensor, + DataformWorkflowInvocationStateSensor, +) from airflow.providers.google.cloud.utils.dataform import make_initialization_workspace_flow from airflow.utils.trigger_rule import TriggerRule @@ -174,6 +177,37 @@ ) # [END howto_operator_create_workflow_invocation_async] + # [START howto_operator_create_workflow_invocation_action_async] + create_workflow_invocation_async_action = DataformCreateWorkflowInvocationOperator( + task_id="create-workflow-invocation-async", + project_id=PROJECT_ID, + region=REGION, + repository_id=REPOSITORY_ID, + asynchronous=True, + workflow_invocation={ + "compilation_result": "{{ task_instance.xcom_pull('create-compilation-result')['name'] }}" + }, + ) + + is_workflow_invocation_action_done = DataformWorkflowInvocationActionStateSensor( + task_id="is-workflow-invocation-done", + project_id=PROJECT_ID, + region=REGION, + repository_id=REPOSITORY_ID, + workflow_invocation_id=( + "{{ task_instance.xcom_pull('create-workflow-invocation')['name'].split('/')[-1] }}" + ), + target_name="YOUR_TARGET_HERE", + expected_statuses={WorkflowInvocationAction.State.SUCCEEDED}, + failure_statuses={ + WorkflowInvocationAction.State.SKIPPED, + WorkflowInvocationAction.State.DISABLED, + WorkflowInvocationAction.State.CANCELLED, + WorkflowInvocationAction.State.FAILED, + }, + ) + # [END howto_operator_create_workflow_invocation_action_async] + # [START howto_operator_get_workflow_invocation] get_workflow_invocation = DataformGetWorkflowInvocationOperator( task_id="get-workflow-invocation", @@ -314,6 +348,8 @@ >> query_workflow_invocation_actions >> create_workflow_invocation_async >> is_workflow_invocation_done + >> create_workflow_invocation_async_action + >> is_workflow_invocation_action_done >> create_workflow_invocation_for_cancel >> cancel_workflow_invocation >> make_test_directory From 6a17a62f1b3b48071027b89b9c56e09861b06838 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Wed, 23 Oct 2024 22:28:03 +0800 Subject: [PATCH 078/258] AIP-84 Patch Pool (#43266) * AIP-84 Patch Pool * Fix CI --- .../api_connexion/endpoints/pool_endpoint.py | 1 + .../core_api/openapi/v1-generated.yaml | 91 ++++++++++ .../core_api/routes/public/pools.py | 45 ++++- .../core_api/routes/public/variables.py | 1 - .../api_fastapi/core_api/serializers/pools.py | 23 ++- airflow/ui/openapi-gen/queries/common.ts | 3 + airflow/ui/openapi-gen/queries/queries.ts | 54 +++++- .../ui/openapi-gen/requests/schemas.gen.ts | 52 ++++++ .../ui/openapi-gen/requests/services.gen.ts | 36 ++++ airflow/ui/openapi-gen/requests/types.gen.ts | 47 ++++++ .../core_api/routes/public/test_pools.py | 155 ++++++++++++++++++ 11 files changed, 500 insertions(+), 8 deletions(-) diff --git a/airflow/api_connexion/endpoints/pool_endpoint.py b/airflow/api_connexion/endpoints/pool_endpoint.py index 2e62ce0f3d3d3..497f31c21c22f 100644 --- a/airflow/api_connexion/endpoints/pool_endpoint.py +++ b/airflow/api_connexion/endpoints/pool_endpoint.py @@ -87,6 +87,7 @@ def get_pools( return pool_collection_schema.dump(PoolCollection(pools=pools, total_entries=total_entries)) +@mark_fastapi_migration_done @security.requires_access_pool("PUT") @action_logging @provide_session diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 182bde36b71a5..9078c90a2c891 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1163,6 +1163,72 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + patch: + tags: + - Pool + summary: Patch Pool + description: Update a Pool. + operationId: patch_pool + parameters: + - name: pool_name + in: path + required: true + schema: + type: string + title: Pool Name + - name: update_mask + in: query + required: false + schema: + anyOf: + - type: array + items: + type: string + - type: 'null' + title: Update Mask + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PoolBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PoolResponse' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Bad Request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Unauthorized + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Not Found + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /public/pools/: get: tags: @@ -2222,6 +2288,31 @@ components: - timetables title: PluginResponse description: Plugin serializer. + PoolBody: + properties: + pool: + anyOf: + - type: string + - type: 'null' + title: Pool + slots: + anyOf: + - type: integer + - type: 'null' + title: Slots + description: + anyOf: + - type: string + - type: 'null' + title: Description + include_deferred: + anyOf: + - type: boolean + - type: 'null' + title: Include Deferred + type: object + title: PoolBody + description: Pool serializer for bodies. PoolCollectionResponse: properties: pools: diff --git a/airflow/api_fastapi/core_api/routes/public/pools.py b/airflow/api_fastapi/core_api/routes/public/pools.py index 0f5329a1ccb95..c9e30a7e2504b 100644 --- a/airflow/api_fastapi/core_api/routes/public/pools.py +++ b/airflow/api_fastapi/core_api/routes/public/pools.py @@ -16,7 +16,9 @@ # under the License. from __future__ import annotations -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, Query +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError from sqlalchemy import delete, select from sqlalchemy.orm import Session from typing_extensions import Annotated @@ -25,7 +27,12 @@ from airflow.api_fastapi.common.parameters import QueryLimit, QueryOffset, SortParam from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc -from airflow.api_fastapi.core_api.serializers.pools import PoolCollectionResponse, PoolResponse +from airflow.api_fastapi.core_api.serializers.pools import ( + BasePool, + PoolBody, + PoolCollectionResponse, + PoolResponse, +) from airflow.models.pool import Pool pools_router = AirflowRouter(tags=["Pool"], prefix="/pools") @@ -95,3 +102,37 @@ async def get_pools( pools=[PoolResponse.model_validate(pool, from_attributes=True) for pool in pools], total_entries=total_entries, ) + + +@pools_router.patch("/{pool_name}", responses=create_openapi_http_exception_doc([400, 401, 403, 404])) +async def patch_pool( + pool_name: str, + patch_body: PoolBody, + session: Annotated[Session, Depends(get_session)], + update_mask: list[str] | None = Query(None), +) -> PoolResponse: + """Update a Pool.""" + # Only slots and include_deferred can be modified in 'default_pool' + if pool_name == Pool.DEFAULT_POOL_NAME: + if update_mask and all(mask.strip() in {"slots", "include_deferred"} for mask in update_mask): + pass + else: + raise HTTPException(400, "Only slots and included_deferred can be modified on Default Pool") + + pool = session.scalar(select(Pool).where(Pool.pool == pool_name).limit(1)) + if not pool: + raise HTTPException(404, detail=f"The Pool with name: `{pool_name}` was not found") + + if update_mask: + data = patch_body.model_dump(include=set(update_mask), by_alias=True) + else: + data = patch_body.model_dump(by_alias=True) + try: + BasePool.model_validate(data) + except ValidationError as e: + raise RequestValidationError(errors=e.errors()) + + for key, value in data.items(): + setattr(pool, key, value) + + return PoolResponse.model_validate(pool, from_attributes=True) diff --git a/airflow/api_fastapi/core_api/routes/public/variables.py b/airflow/api_fastapi/core_api/routes/public/variables.py index 3110512c2cc65..6b834a6de7581 100644 --- a/airflow/api_fastapi/core_api/routes/public/variables.py +++ b/airflow/api_fastapi/core_api/routes/public/variables.py @@ -119,7 +119,6 @@ async def patch_variable( data = patch_body.model_dump(exclude=non_update_fields) for key, val in data.items(): setattr(variable, key, val) - session.add(variable) return variable diff --git a/airflow/api_fastapi/core_api/serializers/pools.py b/airflow/api_fastapi/core_api/serializers/pools.py index 4bfa7137f1231..dd1d6df884cc8 100644 --- a/airflow/api_fastapi/core_api/serializers/pools.py +++ b/airflow/api_fastapi/core_api/serializers/pools.py @@ -19,7 +19,7 @@ from typing import Annotated, Callable -from pydantic import BaseModel, BeforeValidator, Field +from pydantic import BaseModel, BeforeValidator, ConfigDict, Field def _call_function(function: Callable[[], int]) -> int: @@ -31,14 +31,18 @@ def _call_function(function: Callable[[], int]) -> int: return function() -class PoolResponse(BaseModel): - """Pool serializer for responses.""" +class BasePool(BaseModel): + """Base serializer for Pool.""" - pool: str = Field(serialization_alias="name", validation_alias="pool") + pool: str = Field(serialization_alias="name") slots: int description: str | None include_deferred: bool + +class PoolResponse(BasePool): + """Pool serializer for responses.""" + occupied_slots: Annotated[int, BeforeValidator(_call_function)] running_slots: Annotated[int, BeforeValidator(_call_function)] queued_slots: Annotated[int, BeforeValidator(_call_function)] @@ -52,3 +56,14 @@ class PoolCollectionResponse(BaseModel): pools: list[PoolResponse] total_entries: int + + +class PoolBody(BaseModel): + """Pool serializer for bodies.""" + + model_config = ConfigDict(populate_by_name=True) + + name: str | None = Field(default=None, alias="pool") + slots: int | None = None + description: str | None = None + include_deferred: bool | None = None diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index 45ffa188ac858..c621a3fdb54a7 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -357,6 +357,9 @@ export type DagServicePatchDagMutationResult = Awaited< export type VariableServicePatchVariableMutationResult = Awaited< ReturnType >; +export type PoolServicePatchPoolMutationResult = Awaited< + ReturnType +>; export type DagServiceDeleteDagMutationResult = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index 288bef37335a3..7141ac00011de 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -18,7 +18,12 @@ import { ProviderService, VariableService, } from "../requests/services.gen"; -import { DAGPatchBody, DagRunState, VariableBody } from "../requests/types.gen"; +import { + DAGPatchBody, + DagRunState, + PoolBody, + VariableBody, +} from "../requests/types.gen"; import * as Common from "./common"; /** @@ -776,6 +781,53 @@ export const useVariableServicePatchVariable = < }) as unknown as Promise, ...options, }); +/** + * Patch Pool + * Update a Pool. + * @param data The data for the request. + * @param data.poolName + * @param data.requestBody + * @param data.updateMask + * @returns PoolResponse Successful Response + * @throws ApiError + */ +export const usePoolServicePatchPool = < + TData = Common.PoolServicePatchPoolMutationResult, + TError = unknown, + TContext = unknown, +>( + options?: Omit< + UseMutationOptions< + TData, + TError, + { + poolName: string; + requestBody: PoolBody; + updateMask?: string[]; + }, + TContext + >, + "mutationFn" + >, +) => + useMutation< + TData, + TError, + { + poolName: string; + requestBody: PoolBody; + updateMask?: string[]; + }, + TContext + >({ + mutationFn: ({ poolName, requestBody, updateMask }) => + PoolService.patchPool({ + poolName, + requestBody, + updateMask, + }) as unknown as Promise, + ...options, + }); /** * Delete Dag * Delete the specific DAG. diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index f5ca444b535b2..530125a19dd28 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1436,6 +1436,58 @@ export const $PluginResponse = { description: "Plugin serializer.", } as const; +export const $PoolBody = { + properties: { + pool: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Pool", + }, + slots: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Slots", + }, + description: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Description", + }, + include_deferred: { + anyOf: [ + { + type: "boolean", + }, + { + type: "null", + }, + ], + title: "Include Deferred", + }, + }, + type: "object", + title: "PoolBody", + description: "Pool serializer for bodies.", +} as const; + export const $PoolCollectionResponse = { properties: { pools: { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index 8aa2949f29cde..c4b0c987c4324 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -46,6 +46,8 @@ import type { DeletePoolResponse, GetPoolData, GetPoolResponse, + PatchPoolData, + PatchPoolResponse, GetPoolsData, GetPoolsResponse, GetProvidersData, @@ -688,6 +690,40 @@ export class PoolService { }); } + /** + * Patch Pool + * Update a Pool. + * @param data The data for the request. + * @param data.poolName + * @param data.requestBody + * @param data.updateMask + * @returns PoolResponse Successful Response + * @throws ApiError + */ + public static patchPool( + data: PatchPoolData, + ): CancelablePromise { + return __request(OpenAPI, { + method: "PATCH", + url: "/public/pools/{pool_name}", + path: { + pool_name: data.poolName, + }, + query: { + update_mask: data.updateMask, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 422: "Validation Error", + }, + }); + } + /** * Get Pools * Get all pools entries. diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 1e22e3937f6b2..75f2296379aab 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -325,6 +325,16 @@ export type PluginResponse = { timetables: Array; }; +/** + * Pool serializer for bodies. + */ +export type PoolBody = { + pool?: string | null; + slots?: number | null; + description?: string | null; + include_deferred?: boolean | null; +}; + /** * Pool Collection serializer for responses. */ @@ -595,6 +605,14 @@ export type GetPoolData = { export type GetPoolResponse = PoolResponse; +export type PatchPoolData = { + poolName: string; + requestBody: PoolBody; + updateMask?: Array | null; +}; + +export type PatchPoolResponse = PoolResponse; + export type GetPoolsData = { limit?: number; offset?: number; @@ -1166,6 +1184,35 @@ export type $OpenApiTs = { 422: HTTPValidationError; }; }; + patch: { + req: PatchPoolData; + res: { + /** + * Successful Response + */ + 200: PoolResponse; + /** + * Bad Request + */ + 400: HTTPExceptionResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Not Found + */ + 404: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; }; "/public/pools/": { get: { diff --git a/tests/api_fastapi/core_api/routes/public/test_pools.py b/tests/api_fastapi/core_api/routes/public/test_pools.py index e97f85b95dcd4..e2a61fedd2026 100644 --- a/tests/api_fastapi/core_api/routes/public/test_pools.py +++ b/tests/api_fastapi/core_api/routes/public/test_pools.py @@ -126,3 +126,158 @@ def test_should_respond_200( body = response.json() assert body["total_entries"] == expected_total_entries assert [pool["name"] for pool in body["pools"]] == expected_ids + + +class TestPatchPool(TestPoolsEndpoint): + @pytest.mark.parametrize( + "pool_name, query_params, body, expected_status_code, expected_response", + [ + # Error + ( + Pool.DEFAULT_POOL_NAME, + {}, + {}, + 400, + {"detail": "Only slots and included_deferred can be modified on Default Pool"}, + ), + ( + Pool.DEFAULT_POOL_NAME, + {"update_mask": ["description"]}, + {}, + 400, + {"detail": "Only slots and included_deferred can be modified on Default Pool"}, + ), + ( + "unknown_pool", + {}, + {}, + 404, + {"detail": "The Pool with name: `unknown_pool` was not found"}, + ), + ( + POOL1_NAME, + {}, + {}, + 422, + { + "detail": [ + { + "input": None, + "loc": ["pool"], + "msg": "Input should be a valid string", + "type": "string_type", + }, + { + "input": None, + "loc": ["slots"], + "msg": "Input should be a valid integer", + "type": "int_type", + }, + { + "input": None, + "loc": ["include_deferred"], + "msg": "Input should be a valid boolean", + "type": "bool_type", + }, + ], + }, + ), + # Success + # Partial body + ( + POOL1_NAME, + {"update_mask": ["name"]}, + {"slots": 150, "name": "pool_1_updated"}, + 200, + { + "deferred_slots": 0, + "description": None, + "include_deferred": True, + "name": "pool_1_updated", + "occupied_slots": 0, + "open_slots": 3, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 3, + }, + ), + # Partial body on default_pool + ( + Pool.DEFAULT_POOL_NAME, + {"update_mask": ["slots"]}, + {"slots": 150}, + 200, + { + "deferred_slots": 0, + "description": "Default pool", + "include_deferred": False, + "name": "default_pool", + "occupied_slots": 0, + "open_slots": 150, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 150, + }, + ), + # Partial body on default_pool alternate + ( + Pool.DEFAULT_POOL_NAME, + {"update_mask": ["slots", "include_deferred"]}, + {"slots": 150, "include_deferred": True}, + 200, + { + "deferred_slots": 0, + "description": "Default pool", + "include_deferred": True, + "name": "default_pool", + "occupied_slots": 0, + "open_slots": 150, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 150, + }, + ), + # Full body + ( + POOL1_NAME, + {}, + { + "slots": 8, + "description": "Description Updated", + "name": "pool_1_updated", + "include_deferred": False, + }, + 200, + { + "deferred_slots": 0, + "description": "Description Updated", + "include_deferred": False, + "name": "pool_1_updated", + "occupied_slots": 0, + "open_slots": 8, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 8, + }, + ), + ], + ) + def test_should_respond_200( + self, test_client, session, pool_name, query_params, body, expected_status_code, expected_response + ): + self.create_pools() + response = test_client.patch(f"/public/pools/{pool_name}", params=query_params, json=body) + assert response.status_code == expected_status_code + + body = response.json() + + if response.status_code == 422: + for error in body["detail"]: + # pydantic version can vary in tests (lower constraints), we do not assert the url. + del error["url"] + + assert body == expected_response From d7f50baa6fa74eb6d7493e3abadb687b39ca0b5d Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Wed, 23 Oct 2024 15:36:30 +0100 Subject: [PATCH 079/258] Bump Flask-AppBuilder to ``4.5.2`` (#43309) https://pypi.org/project/Flask-AppBuilder/4.5.2/ # Conflicts: # dev/breeze/tests/test_packages.py # generated/provider_dependencies.json # providers/src/airflow/providers/fab/provider.yaml --- Dockerfile.ci | 2 +- dev/breeze/tests/test_packages.py | 6 +++--- generated/provider_dependencies.json | 2 +- hatch_build.py | 7 ------- providers/src/airflow/providers/fab/provider.yaml | 2 +- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index cdf80c9b91593..88eec69a24e35 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1195,7 +1195,7 @@ ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" # NOTE! When you want to make sure dependencies are installed from scratch in your PR after removing # some dependencies, you also need to set "disable image cache" in your PR to make sure the image is # not built using the "main" version of those dependencies. -ARG DEPENDENCIES_EPOCH_NUMBER="12" +ARG DEPENDENCIES_EPOCH_NUMBER="13" # Make sure noninteractive debian install is used and language variables set ENV PYTHON_BASE_IMAGE=${PYTHON_BASE_IMAGE} \ diff --git a/dev/breeze/tests/test_packages.py b/dev/breeze/tests/test_packages.py index 6cac54a14c31a..c8b4596b03f9b 100644 --- a/dev/breeze/tests/test_packages.py +++ b/dev/breeze/tests/test_packages.py @@ -169,7 +169,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1", "apache-airflow>=2.9.0", - "flask-appbuilder==4.5.1", + "flask-appbuilder==4.5.2", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", @@ -183,7 +183,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1.dev0", "apache-airflow>=2.9.0.dev0", - "flask-appbuilder==4.5.1", + "flask-appbuilder==4.5.2", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", @@ -197,7 +197,7 @@ def test_get_documentation_package_path(): """ "apache-airflow-providers-common-compat>=1.2.1b0", "apache-airflow>=2.9.0b0", - "flask-appbuilder==4.5.1", + "flask-appbuilder==4.5.2", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", diff --git a/generated/provider_dependencies.json b/generated/provider_dependencies.json index a5c97c51237a0..c483d38c55e3e 100644 --- a/generated/provider_dependencies.json +++ b/generated/provider_dependencies.json @@ -571,7 +571,7 @@ "deps": [ "apache-airflow-providers-common-compat>=1.2.1", "apache-airflow>=2.9.0", - "flask-appbuilder==4.5.1", + "flask-appbuilder==4.5.2", "flask-login>=0.6.2", "flask>=2.2,<2.3", "google-re2>=1.0", diff --git a/hatch_build.py b/hatch_build.py index 1b8d4fd6f6f44..d30ee3f3b8b5f 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -379,13 +379,6 @@ # all parameters now are mandatory which make AirflowDatabaseSessionInterface incompatible with this version. "flask-session>=0.4.0,<0.6", "flask-wtf>=1.1.0", - # WTForms are limited to 3.2.0 because of the error in tests. We technically do not need it directly - # as this is a dependency of Flask-WTF, but we need to specify it here to add the limitation - # The issue to track it is https://github.com/pallets-eco/wtforms/issues/863 - # Note. 3.2.0 has been broken because of imports https://github.com/pallets-eco/wtforms/issues/861 which - # was fixed in 3.2.1, but after import was fixed, the tests started to work with 3.2.1 - # when the issue 863 is fixed, we should likely leave the line below and specify !=3.2.0,!=3.2.1 - "wtforms>=3.1.0,<3.2.0", # Flask 2.3 is scheduled to introduce a number of deprecation removals - some of them might be breaking # for our dependencies - notably `_app_ctx_stack` and `_request_ctx_stack` removals. # We should remove the limitation after 2.3 is released and our dependencies are updated to handle it diff --git a/providers/src/airflow/providers/fab/provider.yaml b/providers/src/airflow/providers/fab/provider.yaml index 86b0598df4533..fe8e0b9753474 100644 --- a/providers/src/airflow/providers/fab/provider.yaml +++ b/providers/src/airflow/providers/fab/provider.yaml @@ -55,7 +55,7 @@ dependencies: # Every time we update FAB version here, please make sure that you review the classes and models in # `airflow/providers/fab/auth_manager/security_manager/override.py` with their upstream counterparts. # In particular, make sure any breaking changes, for example any new methods, are accounted for. - - flask-appbuilder==4.5.1 + - flask-appbuilder==4.5.2 - flask-login>=0.6.2 - google-re2>=1.0 - jmespath>=0.7.0 From 9da6fc3b8f8b4989d94de381c4bdfc993113f981 Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Wed, 23 Oct 2024 15:37:05 +0100 Subject: [PATCH 080/258] Remove the ability to import executors from plugins (#43289) Executors should no longer be registered or imported via Airflow's plugin mechanism -- these types of classes are just treated as plain python classes by Airflow, so there is no need to register them with Airflow. --- .../core_api/openapi/v1-generated.yaml | 6 --- .../core_api/serializers/plugins.py | 1 - airflow/executors/executor_loader.py | 12 ----- airflow/plugins_manager.py | 33 +----------- .../ui/openapi-gen/requests/schemas.gen.ts | 8 --- airflow/ui/openapi-gen/requests/types.gen.ts | 1 - airflow/www/views.py | 1 - newsfragments/43289.significant.rst | 4 ++ .../endpoints/test_plugin_endpoint.py | 1 - .../schemas/test_plugin_schema.py | 3 -- tests/cli/commands/test_plugins_command.py | 1 - tests/executors/test_executor_loader.py | 50 +------------------ tests/plugins/test_plugin.py | 8 --- tests_common/test_utils/mock_plugins.py | 2 - 14 files changed, 7 insertions(+), 124 deletions(-) create mode 100644 newsfragments/43289.significant.rst diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 9078c90a2c891..3d890d4da310d 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -2212,11 +2212,6 @@ components: type: string type: array title: Hooks - executors: - items: - type: string - type: array - title: Executors macros: items: type: string @@ -2274,7 +2269,6 @@ components: required: - name - hooks - - executors - macros - flask_blueprints - fastapi_apps diff --git a/airflow/api_fastapi/core_api/serializers/plugins.py b/airflow/api_fastapi/core_api/serializers/plugins.py index ee6812bb954c9..68bc8ea443c25 100644 --- a/airflow/api_fastapi/core_api/serializers/plugins.py +++ b/airflow/api_fastapi/core_api/serializers/plugins.py @@ -65,7 +65,6 @@ class PluginResponse(BaseModel): name: str hooks: list[str] - executors: list[str] macros: list[str] flask_blueprints: list[str] fastapi_apps: list[FastAPIAppResponse] diff --git a/airflow/executors/executor_loader.py b/airflow/executors/executor_loader.py index 4a940793df27f..f74153f95fc94 100644 --- a/airflow/executors/executor_loader.py +++ b/airflow/executors/executor_loader.py @@ -21,7 +21,6 @@ import functools import logging import os -from contextlib import suppress from typing import TYPE_CHECKING from airflow.api_internal.internal_api_call import InternalApiConfig @@ -284,17 +283,6 @@ def _import_and_validate(path: str) -> type[BaseExecutor]: cls.validate_database_executor_compatibility(executor) return executor - if executor_name.connector_source == ConnectorSource.PLUGIN: - with suppress(ImportError, AttributeError): - # Load plugins here for executors as at that time the plugins might not have been - # initialized yet - from airflow import plugins_manager - - plugins_manager.integrate_executor_plugins() - return ( - _import_and_validate(f"airflow.executors.{executor_name.module_path}"), - ConnectorSource.PLUGIN, - ) return _import_and_validate(executor_name.module_path), executor_name.connector_source @classmethod diff --git a/airflow/plugins_manager.py b/airflow/plugins_manager.py index 2ec1388d16361..fc7adc5993f64 100644 --- a/airflow/plugins_manager.py +++ b/airflow/plugins_manager.py @@ -64,7 +64,6 @@ # Plugin components to integrate as modules registered_hooks: list[BaseHook] | None = None macros_modules: list[Any] | None = None -executors_modules: list[Any] | None = None # Plugin components to integrate directly admin_views: list[Any] | None = None @@ -88,7 +87,6 @@ """ PLUGINS_ATTRIBUTES_TO_DUMP = { "hooks", - "executors", "macros", "admin_views", "flask_blueprints", @@ -154,7 +152,6 @@ class AirflowPlugin: name: str | None = None source: AirflowPluginSource | None = None hooks: list[Any] = [] - executors: list[Any] = [] macros: list[Any] = [] admin_views: list[Any] = [] flask_blueprints: list[Any] = [] @@ -533,33 +530,6 @@ def initialize_hook_lineage_readers_plugins(): hook_lineage_reader_classes.extend(plugin.hook_lineage_readers) -def integrate_executor_plugins() -> None: - """Integrate executor plugins to the context.""" - global plugins - global executors_modules - - if executors_modules is not None: - return - - ensure_plugins_loaded() - - if plugins is None: - raise AirflowPluginException("Can't load plugins.") - - log.debug("Integrate executor plugins") - - executors_modules = [] - for plugin in plugins: - if plugin.name is None: - raise AirflowPluginException("Invalid plugin name") - plugin_name: str = plugin.name - - executors_module = make_module("airflow.executors." + plugin_name, plugin.executors) - if executors_module: - executors_modules.append(executors_module) - sys.modules[executors_module.__name__] = executors_module - - def integrate_macros_plugins() -> None: """Integrates macro plugins.""" global plugins @@ -615,7 +585,6 @@ def get_plugin_info(attrs_to_dump: Iterable[str] | None = None) -> list[dict[str :param attrs_to_dump: A list of plugin attributes to dump """ ensure_plugins_loaded() - integrate_executor_plugins() integrate_macros_plugins() initialize_web_ui_plugins() initialize_fastapi_plugins() @@ -629,7 +598,7 @@ def get_plugin_info(attrs_to_dump: Iterable[str] | None = None) -> list[dict[str for attr in attrs_to_dump: if attr in ("global_operator_extra_links", "operator_extra_links"): info[attr] = [f"<{qualname(d.__class__)} object>" for d in getattr(plugin, attr)] - elif attr in ("macros", "timetables", "hooks", "executors", "priority_weight_strategies"): + elif attr in ("macros", "timetables", "hooks", "priority_weight_strategies"): info[attr] = [qualname(d) for d in getattr(plugin, attr)] elif attr == "listeners": # listeners may be modules or class instances diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index 530125a19dd28..356814d15d53a 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1333,13 +1333,6 @@ export const $PluginResponse = { type: "array", title: "Hooks", }, - executors: { - items: { - type: "string", - }, - type: "array", - title: "Executors", - }, macros: { items: { type: "string", @@ -1419,7 +1412,6 @@ export const $PluginResponse = { required: [ "name", "hooks", - "executors", "macros", "flask_blueprints", "fastapi_apps", diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 75f2296379aab..4005412147078 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -311,7 +311,6 @@ export type PluginCollectionResponse = { export type PluginResponse = { name: string; hooks: Array; - executors: Array; macros: Array; flask_blueprints: Array; fastapi_apps: Array; diff --git a/airflow/www/views.py b/airflow/www/views.py index c5bbdd389b093..c153cc80597f5 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -4286,7 +4286,6 @@ class PluginView(AirflowBaseView): def list(self): """List loaded plugins.""" plugins_manager.ensure_plugins_loaded() - plugins_manager.integrate_executor_plugins() plugins_manager.initialize_extra_operators_links_plugins() plugins_manager.initialize_web_ui_plugins() plugins_manager.initialize_fastapi_plugins() diff --git a/newsfragments/43289.significant.rst b/newsfragments/43289.significant.rst new file mode 100644 index 0000000000000..15063202640fb --- /dev/null +++ b/newsfragments/43289.significant.rst @@ -0,0 +1,4 @@ +Support for adding executors via Airflow Plugins is removed + +Executors should no longer be registered or imported via Airflow's plugin mechanism -- these types of classes +are just treated as plain Python classes by Airflow, so there is no need to register them with Airflow. diff --git a/tests/api_connexion/endpoints/test_plugin_endpoint.py b/tests/api_connexion/endpoints/test_plugin_endpoint.py index 924de84dc0d48..487ba53a30080 100644 --- a/tests/api_connexion/endpoints/test_plugin_endpoint.py +++ b/tests/api_connexion/endpoints/test_plugin_endpoint.py @@ -145,7 +145,6 @@ def test_get_plugins_return_200(self): { "appbuilder_menu_items": [appbuilder_menu_items], "appbuilder_views": [{"view": qualname(MockView)}], - "executors": [], "flask_blueprints": [ f"<{qualname(bp.__class__)}: name={bp.name!r} import_name={bp.import_name!r}>" ], diff --git a/tests/api_connexion/schemas/test_plugin_schema.py b/tests/api_connexion/schemas/test_plugin_schema.py index 0c7141e3493f8..951933e9ffc44 100644 --- a/tests/api_connexion/schemas/test_plugin_schema.py +++ b/tests/api_connexion/schemas/test_plugin_schema.py @@ -86,7 +86,6 @@ def test_serialize(self): assert deserialized_plugin == { "appbuilder_menu_items": [appbuilder_menu_items], "appbuilder_views": [{"view": self.mock_plugin.appbuilder_views[0]["view"]}], - "executors": [], "flask_blueprints": [str(bp)], "fastapi_apps": [ {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, @@ -113,7 +112,6 @@ def test_serialize(self): { "appbuilder_menu_items": [appbuilder_menu_items], "appbuilder_views": [{"view": self.mock_plugin.appbuilder_views[0]["view"]}], - "executors": [], "flask_blueprints": [str(bp)], "fastapi_apps": [ {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, @@ -131,7 +129,6 @@ def test_serialize(self): { "appbuilder_menu_items": [appbuilder_menu_items], "appbuilder_views": [{"view": self.mock_plugin.appbuilder_views[0]["view"]}], - "executors": [], "flask_blueprints": [str(bp)], "fastapi_apps": [ {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, diff --git a/tests/cli/commands/test_plugins_command.py b/tests/cli/commands/test_plugins_command.py index d07641ec841d1..c9807520e4ed3 100644 --- a/tests/cli/commands/test_plugins_command.py +++ b/tests/cli/commands/test_plugins_command.py @@ -69,7 +69,6 @@ def test_should_display_one_plugin(self): "admin_views": [], "macros": ["tests.plugins.test_plugin.plugin_macro"], "menu_links": [], - "executors": ["tests.plugins.test_plugin.PluginExecutor"], "flask_blueprints": [ "" ], diff --git a/tests/executors/test_executor_loader.py b/tests/executors/test_executor_loader.py index 40a336bc580c3..68bc02a6300e4 100644 --- a/tests/executors/test_executor_loader.py +++ b/tests/executors/test_executor_loader.py @@ -22,7 +22,6 @@ import pytest -from airflow import plugins_manager from airflow.exceptions import AirflowConfigException from airflow.executors import executor_loader from airflow.executors.executor_loader import ConnectorSource, ExecutorLoader, ExecutorName @@ -34,9 +33,6 @@ pytestmark = pytest.mark.skip_if_database_isolation_mode -# Plugin Manager creates new modules, which is difficult to mock, so we use test isolation by a unique name. -TEST_PLUGIN_NAME = "unique_plugin_name_to_avoid_collision_i_love_kitties" - class FakeExecutor: is_single_threaded = False @@ -46,11 +42,6 @@ class FakeSingleThreadedExecutor: is_single_threaded = True -class FakePlugin(plugins_manager.AirflowPlugin): - name = TEST_PLUGIN_NAME - executors = [FakeExecutor] - - class TestExecutorLoader: def setup_method(self) -> None: from airflow.executors import executor_loader @@ -89,17 +80,6 @@ def test_should_support_executor_from_core(self, executor_name): assert executor.name == ExecutorName(ExecutorLoader.executors[executor_name], alias=executor_name) assert executor.name.connector_source == ConnectorSource.CORE - @mock.patch("airflow.plugins_manager.plugins", [FakePlugin()]) - @mock.patch("airflow.plugins_manager.executors_modules", None) - def test_should_support_plugins(self): - with conf_vars({("core", "executor"): f"{TEST_PLUGIN_NAME}.FakeExecutor"}): - executor = ExecutorLoader.get_default_executor() - assert executor is not None - assert "FakeExecutor" == executor.__class__.__name__ - assert executor.name is not None - assert executor.name == ExecutorName(f"{TEST_PLUGIN_NAME}.FakeExecutor") - assert executor.name.connector_source == ConnectorSource.PLUGIN - def test_should_support_custom_path(self): with conf_vars({("core", "executor"): "tests.executors.test_executor_loader.FakeExecutor"}): executor = ExecutorLoader.get_default_executor() @@ -124,7 +104,7 @@ def test_should_support_custom_path(self): ), # Core executors and custom module path executor and plugin ( - f"CeleryExecutor, LocalExecutor, tests.executors.test_executor_loader.FakeExecutor, {TEST_PLUGIN_NAME}.FakeExecutor", + "CeleryExecutor, LocalExecutor, tests.executors.test_executor_loader.FakeExecutor", [ ExecutorName( "airflow.providers.celery.executors.celery_executor.CeleryExecutor", @@ -138,17 +118,12 @@ def test_should_support_custom_path(self): "tests.executors.test_executor_loader.FakeExecutor", None, ), - ExecutorName( - f"{TEST_PLUGIN_NAME}.FakeExecutor", - None, - ), ], ), # Core executors and custom module path executor and plugin with aliases ( ( - "CeleryExecutor, LocalExecutor, fake_exec:tests.executors.test_executor_loader.FakeExecutor, " - f"plugin_exec:{TEST_PLUGIN_NAME}.FakeExecutor" + "CeleryExecutor, LocalExecutor, fake_exec:tests.executors.test_executor_loader.FakeExecutor" ), [ ExecutorName( @@ -163,10 +138,6 @@ def test_should_support_custom_path(self): "tests.executors.test_executor_loader.FakeExecutor", "fake_exec", ), - ExecutorName( - f"{TEST_PLUGIN_NAME}.FakeExecutor", - "plugin_exec", - ), ], ), ], @@ -194,8 +165,6 @@ def test_init_executors(self): "CeleryExecutor, my.module.path, my.module.path", "CeleryExecutor, my_alias:my.module.path, my.module.path", "CeleryExecutor, my_alias:my.module.path, other_alias:my.module.path", - f"CeleryExecutor, {TEST_PLUGIN_NAME}.FakeExecutor, {TEST_PLUGIN_NAME}.FakeExecutor", - f"my_alias:{TEST_PLUGIN_NAME}.FakeExecutor, other_alias:{TEST_PLUGIN_NAME}.FakeExecutor", ], ) def test_get_hybrid_executors_from_config_duplicates_should_fail(self, executor_config): @@ -239,21 +208,6 @@ def test_should_support_import_executor_from_core(self, executor_config, expecte assert expected_value == executor.__name__ assert import_source == ConnectorSource.CORE - @mock.patch("airflow.plugins_manager.plugins", [FakePlugin()]) - @mock.patch("airflow.plugins_manager.executors_modules", None) - @pytest.mark.parametrize( - ("executor_config"), - [ - (f"{TEST_PLUGIN_NAME}.FakeExecutor"), - (f"my_cool_alias:{TEST_PLUGIN_NAME}.FakeExecutor, CeleryExecutor"), - ], - ) - def test_should_support_import_plugins(self, executor_config): - with conf_vars({("core", "executor"): executor_config}): - executor, import_source = ExecutorLoader.import_default_executor_cls() - assert "FakeExecutor" == executor.__name__ - assert import_source == ConnectorSource.PLUGIN - @pytest.mark.parametrize( "executor_config", [ diff --git a/tests/plugins/test_plugin.py b/tests/plugins/test_plugin.py index 01b18c48a63fa..98f64e75456f5 100644 --- a/tests/plugins/test_plugin.py +++ b/tests/plugins/test_plugin.py @@ -21,8 +21,6 @@ from flask import Blueprint from flask_appbuilder import BaseView as AppBuilderBaseView, expose -from airflow.executors.base_executor import BaseExecutor - # Importing base classes that we need to derive from airflow.hooks.base import BaseHook @@ -49,11 +47,6 @@ class PluginHook(BaseHook): pass -# Will show up under airflow.executors.test_plugin.PluginExecutor -class PluginExecutor(BaseExecutor): - pass - - # Will show up under airflow.macros.test_plugin.plugin_macro def plugin_macro(): pass @@ -123,7 +116,6 @@ def get_weight(self, ti): class AirflowTestPlugin(AirflowPlugin): name = "test_plugin" hooks = [PluginHook] - executors = [PluginExecutor] macros = [plugin_macro] flask_blueprints = [bp] fastapi_apps = [app_with_metadata] diff --git a/tests_common/test_utils/mock_plugins.py b/tests_common/test_utils/mock_plugins.py index 3e50f1b413ebc..875a9abbd3a0f 100644 --- a/tests_common/test_utils/mock_plugins.py +++ b/tests_common/test_utils/mock_plugins.py @@ -25,7 +25,6 @@ "plugins", "registered_hooks", "macros_modules", - "executors_modules", "admin_views", "flask_blueprints", "fastapi_apps", @@ -44,7 +43,6 @@ "plugins", "registered_hooks", "macros_modules", - "executors_modules", "admin_views", "flask_blueprints", "menu_links", From d725cf683ed25955f739909d7176899224dca13a Mon Sep 17 00:00:00 2001 From: Bohdan Udovenko Date: Wed, 23 Oct 2024 18:03:33 +0300 Subject: [PATCH 081/258] Fix instruction for docker compose(tested on Mac M1) (#43119) * Fix instruction for docker compose(tested on Mac M1) Make fixes that allows to run airflow dags in pycharm debugger on Mac M1 * fix static checks * fix linter error * fix unexpected identation --------- Co-authored-by: Shahar Epstein <60007259+shahar1@users.noreply.github.com> --- .../howto/docker-compose/index.rst | 7 ++++++- .../img/docker-compose-pycharm.png | Bin 0 -> 59560 bytes 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/apache-airflow/img/docker-compose-pycharm.png diff --git a/docs/apache-airflow/howto/docker-compose/index.rst b/docs/apache-airflow/howto/docker-compose/index.rst index b617ba1cc903f..96ec9b53e3a9f 100644 --- a/docs/apache-airflow/howto/docker-compose/index.rst +++ b/docs/apache-airflow/howto/docker-compose/index.rst @@ -379,7 +379,7 @@ Steps: environment: <<: *airflow-common-env user: "50000:0" - entrypoint: ["bash"] + entrypoint: [ "/bin/bash", "-c" ] .. note:: @@ -398,6 +398,11 @@ Steps: :alt: Configuring the container's Python interpreter in PyCharm, step diagram Building the interpreter index might take some time. +3) Add ``exec`` to docker-compose/command and actions in python service + +.. image:: /img/docker-compose-pycharm.png + :alt: Configuring the container's Python interpreter in PyCharm, step diagram + Once configured, you can debug your Airflow code within the container environment, mimicking your local setup. diff --git a/docs/apache-airflow/img/docker-compose-pycharm.png b/docs/apache-airflow/img/docker-compose-pycharm.png new file mode 100644 index 0000000000000000000000000000000000000000..30e459ddb4f558c7278e7ec29bd9ec487f02228c GIT binary patch literal 59560 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

    Yd%Z9Znyhq~zt#xf-i3#q02sGT?)=#h)m(_PqIT Di;v^E literal 0 HcmV?d00001 From 69af18592ffcb666b180fa56215200e545428bf3 Mon Sep 17 00:00:00 2001 From: David Blain Date: Wed, 23 Oct 2024 17:12:17 +0200 Subject: [PATCH 082/258] refactor: OdbcHook must use it's own connection when creating a sqlalchemy engine (#43145) Co-authored-by: David Blain --- providers/src/airflow/providers/odbc/hooks/odbc.py | 13 +++++++++++++ providers/tests/odbc/hooks/test_odbc.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/providers/src/airflow/providers/odbc/hooks/odbc.py b/providers/src/airflow/providers/odbc/hooks/odbc.py index 48dada49f88ba..aa3f9ce50fa80 100644 --- a/providers/src/airflow/providers/odbc/hooks/odbc.py +++ b/providers/src/airflow/providers/odbc/hooks/odbc.py @@ -180,6 +180,19 @@ def connect_kwargs(self) -> dict: return merged_connect_kwargs + def get_sqlalchemy_engine(self, engine_kwargs=None): + """ + Get an sqlalchemy_engine object. + + :param engine_kwargs: Kwargs used in :func:`~sqlalchemy.create_engine`. + :return: the created engine. + """ + if engine_kwargs is None: + engine_kwargs = {} + engine_kwargs["creator"] = self.get_conn + + return super().get_sqlalchemy_engine(engine_kwargs) + def get_conn(self) -> Connection: """Return ``pyodbc`` connection object.""" conn = connect(self.odbc_connection_string, **self.connect_kwargs) diff --git a/providers/tests/odbc/hooks/test_odbc.py b/providers/tests/odbc/hooks/test_odbc.py index 8f749aa4f765d..5d2e195dcc640 100644 --- a/providers/tests/odbc/hooks/test_odbc.py +++ b/providers/tests/odbc/hooks/test_odbc.py @@ -19,6 +19,7 @@ import json import logging +import sqlite3 from dataclasses import dataclass from unittest import mock from unittest.mock import patch @@ -340,3 +341,11 @@ def test_query_no_handler_return_none(self): hook = mock_hook(OdbcHook) result = hook.run("SQL") assert result is None + + def test_get_sqlalchemy_engine_verify_creator_is_being_used(self): + hook = mock_hook(OdbcHook, conn_params={"extra": {"sqlalchemy_scheme": "sqlite"}}) + + with sqlite3.connect(":memory:") as connection: + hook.get_conn = lambda: connection + engine = hook.get_sqlalchemy_engine() + assert engine.connect().connection.connection == connection From cc762293ca5ce20d475af787822ea48b6d3c874f Mon Sep 17 00:00:00 2001 From: Kacper Muda Date: Wed, 23 Oct 2024 17:21:56 +0200 Subject: [PATCH 083/258] feat: add Hook Level Lineage support for GCSHook (#42507) Signed-off-by: Kacper Muda --- generated/provider_dependencies.json | 2 +- .../google/{datasets => assets}/__init__.py | 0 .../google/{datasets => assets}/bigquery.py | 0 .../airflow/providers/google/assets/gcs.py | 45 +++++ .../providers/google/cloud/hooks/gcs.py | 52 ++++- .../airflow/providers/google/provider.yaml | 14 +- .../tests/google/assets/test_bigquery.py | 2 +- providers/tests/google/assets/test_gcs.py | 74 +++++++ .../tests/google/cloud/hooks/test_gcs.py | 190 ++++++++++++++++++ 9 files changed, 372 insertions(+), 7 deletions(-) rename providers/src/airflow/providers/google/{datasets => assets}/__init__.py (100%) rename providers/src/airflow/providers/google/{datasets => assets}/bigquery.py (100%) create mode 100644 providers/src/airflow/providers/google/assets/gcs.py create mode 100644 providers/tests/google/assets/test_gcs.py diff --git a/generated/provider_dependencies.json b/generated/provider_dependencies.json index c483d38c55e3e..2f284cc4de648 100644 --- a/generated/provider_dependencies.json +++ b/generated/provider_dependencies.json @@ -625,7 +625,7 @@ "google": { "deps": [ "PyOpenSSL>=23.0.0", - "apache-airflow-providers-common-compat>=1.1.0", + "apache-airflow-providers-common-compat>=1.2.1", "apache-airflow-providers-common-sql>=1.7.2", "apache-airflow>=2.8.0", "asgiref>=3.5.2", diff --git a/providers/src/airflow/providers/google/datasets/__init__.py b/providers/src/airflow/providers/google/assets/__init__.py similarity index 100% rename from providers/src/airflow/providers/google/datasets/__init__.py rename to providers/src/airflow/providers/google/assets/__init__.py diff --git a/providers/src/airflow/providers/google/datasets/bigquery.py b/providers/src/airflow/providers/google/assets/bigquery.py similarity index 100% rename from providers/src/airflow/providers/google/datasets/bigquery.py rename to providers/src/airflow/providers/google/assets/bigquery.py diff --git a/providers/src/airflow/providers/google/assets/gcs.py b/providers/src/airflow/providers/google/assets/gcs.py new file mode 100644 index 0000000000000..4df6995787ecc --- /dev/null +++ b/providers/src/airflow/providers/google/assets/gcs.py @@ -0,0 +1,45 @@ +# 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 TYPE_CHECKING + +from airflow.providers.common.compat.assets import Asset +from airflow.providers.google.cloud.hooks.gcs import _parse_gcs_url + +if TYPE_CHECKING: + from urllib.parse import SplitResult + + from airflow.providers.common.compat.openlineage.facet import Dataset as OpenLineageDataset + + +def create_asset(*, bucket: str, key: str, extra: dict | None = None) -> Asset: + return Asset(uri=f"gs://{bucket}/{key}", extra=extra) + + +def sanitize_uri(uri: SplitResult) -> SplitResult: + if not uri.netloc: + raise ValueError("URI format gs:// must contain a bucket name") + return uri + + +def convert_asset_to_openlineage(asset: Asset, lineage_context) -> OpenLineageDataset: + """Translate Asset with valid AIP-60 uri to OpenLineage with assistance from the hook.""" + from airflow.providers.common.compat.openlineage.facet import Dataset as OpenLineageDataset + + bucket, key = _parse_gcs_url(asset.uri) + return OpenLineageDataset(namespace=f"gs://{bucket}", name=key if key else "/") diff --git a/providers/src/airflow/providers/google/cloud/hooks/gcs.py b/providers/src/airflow/providers/google/cloud/hooks/gcs.py index fb48fcd190609..995418f183489 100644 --- a/providers/src/airflow/providers/google/cloud/hooks/gcs.py +++ b/providers/src/airflow/providers/google/cloud/hooks/gcs.py @@ -43,6 +43,7 @@ from requests import Session from airflow.exceptions import AirflowException, AirflowProviderDeprecationWarning +from airflow.providers.common.compat.lineage.hook import get_hook_lineage_collector from airflow.providers.google.cloud.utils.helpers import normalize_directory_path from airflow.providers.google.common.consts import CLIENT_INFO from airflow.providers.google.common.hooks.base_google import ( @@ -214,6 +215,16 @@ def copy( destination_object = source_bucket.copy_blob( # type: ignore[attr-defined] blob=source_object, destination_bucket=destination_bucket, new_name=destination_object ) + get_hook_lineage_collector().add_input_asset( + context=self, + scheme="gs", + asset_kwargs={"bucket": source_bucket.name, "key": source_object.name}, # type: ignore[attr-defined] + ) + get_hook_lineage_collector().add_output_asset( + context=self, + scheme="gs", + asset_kwargs={"bucket": destination_bucket.name, "key": destination_object.name}, # type: ignore[union-attr] + ) self.log.info( "Object %s in bucket %s copied to object %s in bucket %s", @@ -267,6 +278,16 @@ def rewrite( ).rewrite(source=source_object, token=token) self.log.info("Total Bytes: %s | Bytes Written: %s", total_bytes, bytes_rewritten) + get_hook_lineage_collector().add_input_asset( + context=self, + scheme="gs", + asset_kwargs={"bucket": source_bucket.name, "key": source_object.name}, # type: ignore[attr-defined] + ) + get_hook_lineage_collector().add_output_asset( + context=self, + scheme="gs", + asset_kwargs={"bucket": destination_bucket.name, "key": destination_object}, # type: ignore[attr-defined] + ) self.log.info( "Object %s in bucket %s rewritten to object %s in bucket %s", source_object.name, # type: ignore[attr-defined] @@ -345,9 +366,18 @@ def download( if filename: blob.download_to_filename(filename, timeout=timeout) + get_hook_lineage_collector().add_input_asset( + context=self, scheme="gs", asset_kwargs={"bucket": bucket.name, "key": blob.name} + ) + get_hook_lineage_collector().add_output_asset( + context=self, scheme="file", asset_kwargs={"path": filename} + ) self.log.info("File downloaded to %s", filename) return filename else: + get_hook_lineage_collector().add_input_asset( + context=self, scheme="gs", asset_kwargs={"bucket": bucket.name, "key": blob.name} + ) return blob.download_as_bytes() except GoogleCloudError: @@ -555,6 +585,9 @@ def _call_with_retry(f: Callable[[], None]) -> None: _call_with_retry( partial(blob.upload_from_filename, filename=filename, content_type=mime_type, timeout=timeout) ) + get_hook_lineage_collector().add_input_asset( + context=self, scheme="file", asset_kwargs={"path": filename} + ) if gzip: os.remove(filename) @@ -576,6 +609,10 @@ def _call_with_retry(f: Callable[[], None]) -> None: else: raise ValueError("'filename' and 'data' parameter missing. One is required to upload to gcs.") + get_hook_lineage_collector().add_output_asset( + context=self, scheme="gs", asset_kwargs={"bucket": bucket.name, "key": blob.name} + ) + def exists(self, bucket_name: str, object_name: str, retry: Retry = DEFAULT_RETRY) -> bool: """ Check for the existence of a file in Google Cloud Storage. @@ -691,6 +728,9 @@ def delete(self, bucket_name: str, object_name: str) -> None: bucket = client.bucket(bucket_name) blob = bucket.blob(blob_name=object_name) blob.delete() + get_hook_lineage_collector().add_input_asset( + context=self, scheme="gs", asset_kwargs={"bucket": bucket.name, "key": blob.name} + ) self.log.info("Blob %s deleted.", object_name) @@ -1198,9 +1238,17 @@ def compose(self, bucket_name: str, source_objects: List[str], destination_objec client = self.get_conn() bucket = client.bucket(bucket_name) destination_blob = bucket.blob(destination_object) - destination_blob.compose( - sources=[bucket.blob(blob_name=source_object) for source_object in source_objects] + source_blobs = [bucket.blob(blob_name=source_object) for source_object in source_objects] + destination_blob.compose(sources=source_blobs) + get_hook_lineage_collector().add_output_asset( + context=self, scheme="gs", asset_kwargs={"bucket": bucket.name, "key": destination_blob.name} ) + for single_source_blob in source_blobs: + get_hook_lineage_collector().add_input_asset( + context=self, + scheme="gs", + asset_kwargs={"bucket": bucket.name, "key": single_source_blob.name}, + ) self.log.info("Completed successfully.") diff --git a/providers/src/airflow/providers/google/provider.yaml b/providers/src/airflow/providers/google/provider.yaml index 09e7b6643cb25..5f027c13de12e 100644 --- a/providers/src/airflow/providers/google/provider.yaml +++ b/providers/src/airflow/providers/google/provider.yaml @@ -97,7 +97,7 @@ versions: dependencies: - apache-airflow>=2.8.0 - - apache-airflow-providers-common-compat>=1.1.0 + - apache-airflow-providers-common-compat>=1.2.1 - apache-airflow-providers-common-sql>=1.7.2 - asgiref>=3.5.2 - dill>=0.2.3 @@ -777,7 +777,11 @@ asset-uris: - schemes: [gcp] handler: null - schemes: [bigquery] - handler: airflow.providers.google.datasets.bigquery.sanitize_uri + handler: airflow.providers.google.assets.bigquery.sanitize_uri + - schemes: [gs] + handler: airflow.providers.google.assets.gcs.sanitize_uri + factory: airflow.providers.google.assets.gcs.create_asset + to_openlineage_converter: airflow.providers.google.assets.gcs.convert_asset_to_openlineage # dataset has been renamed to asset in Airflow 3.0 # This is kept for backward compatibility. @@ -785,7 +789,11 @@ dataset-uris: - schemes: [gcp] handler: null - schemes: [bigquery] - handler: airflow.providers.google.datasets.bigquery.sanitize_uri + handler: airflow.providers.google.assets.bigquery.sanitize_uri + - schemes: [gs] + handler: airflow.providers.google.assets.gcs.sanitize_uri + factory: airflow.providers.google.assets.gcs.create_asset + to_openlineage_converter: airflow.providers.google.assets.gcs.convert_asset_to_openlineage hooks: - integration-name: Google Ads diff --git a/providers/tests/google/assets/test_bigquery.py b/providers/tests/google/assets/test_bigquery.py index 45da4ffb1eb71..b2f416a36bed8 100644 --- a/providers/tests/google/assets/test_bigquery.py +++ b/providers/tests/google/assets/test_bigquery.py @@ -21,7 +21,7 @@ import pytest -from airflow.providers.google.datasets.bigquery import sanitize_uri +from airflow.providers.google.assets.bigquery import sanitize_uri def test_sanitize_uri_pass() -> None: diff --git a/providers/tests/google/assets/test_gcs.py b/providers/tests/google/assets/test_gcs.py new file mode 100644 index 0000000000000..e9920302b0e0a --- /dev/null +++ b/providers/tests/google/assets/test_gcs.py @@ -0,0 +1,74 @@ +# 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 + +import urllib.parse + +import pytest + +from airflow.providers.common.compat.assets import Asset +from airflow.providers.google.assets.gcs import convert_asset_to_openlineage, create_asset, sanitize_uri + + +def test_sanitize_uri(): + uri = sanitize_uri(urllib.parse.urlsplit("gs://bucket/dir/file.txt")) + result = sanitize_uri(uri) + assert result.scheme == "gs" + assert result.netloc == "bucket" + assert result.path == "/dir/file.txt" + + +def test_sanitize_uri_no_netloc(): + with pytest.raises(ValueError): + sanitize_uri(urllib.parse.urlsplit("gs://")) + + +def test_sanitize_uri_no_path(): + uri = sanitize_uri(urllib.parse.urlsplit("gs://bucket")) + result = sanitize_uri(uri) + assert result.scheme == "gs" + assert result.netloc == "bucket" + assert result.path == "" + + +def test_create_asset(): + assert create_asset(bucket="test-bucket", key="test-path") == Asset(uri="gs://test-bucket/test-path") + assert create_asset(bucket="test-bucket", key="test-dir/test-path") == Asset( + uri="gs://test-bucket/test-dir/test-path" + ) + + +def test_sanitize_uri_trailing_slash(): + uri = sanitize_uri(urllib.parse.urlsplit("gs://bucket/")) + result = sanitize_uri(uri) + assert result.scheme == "gs" + assert result.netloc == "bucket" + assert result.path == "/" + + +def test_convert_asset_to_openlineage_valid(): + uri = "gs://bucket/dir/file.txt" + ol_dataset = convert_asset_to_openlineage(asset=Asset(uri=uri), lineage_context=None) + assert ol_dataset.namespace == "gs://bucket" + assert ol_dataset.name == "dir/file.txt" + + +@pytest.mark.parametrize("uri", ("gs://bucket", "gs://bucket/")) +def test_convert_asset_to_openlineage_no_path(uri): + ol_dataset = convert_asset_to_openlineage(asset=Asset(uri=uri), lineage_context=None) + assert ol_dataset.namespace == "gs://bucket" + assert ol_dataset.name == "/" diff --git a/providers/tests/google/cloud/hooks/test_gcs.py b/providers/tests/google/cloud/hooks/test_gcs.py index 464534bd11d93..7c06990d08cab 100644 --- a/providers/tests/google/cloud/hooks/test_gcs.py +++ b/providers/tests/google/cloud/hooks/test_gcs.py @@ -36,6 +36,7 @@ from google.cloud.storage.retry import DEFAULT_RETRY from airflow.exceptions import AirflowException +from airflow.providers.common.compat.assets import Asset from airflow.providers.google.cloud.hooks import gcs from airflow.providers.google.cloud.hooks.gcs import _fallback_object_url_to_object_name_and_bucket_name from airflow.providers.google.common.consts import CLIENT_INFO @@ -43,6 +44,7 @@ from airflow.version import version from providers.tests.google.cloud.utils.base_gcp_mock import mock_base_gcp_hook_default_project_id +from tests_common.test_utils.compat import AIRFLOW_V_2_10_PLUS BASE_STRING = "airflow.providers.google.common.hooks.base_google.{}" GCS_STRING = "airflow.providers.google.cloud.hooks.gcs.{}" @@ -413,6 +415,41 @@ def test_copy_empty_source_object(self): assert str(ctx.value) == "source_bucket and source_object cannot be empty." + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch("google.cloud.storage.Bucket.copy_blob") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_copy_exposes_lineage(self, mock_service, mock_copy, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + destination_bucket_name = "test-dest-bucket" + destination_object_name = "test-dest-object" + + source_bucket = storage.Bucket(mock_service, source_bucket_name) + mock_copy.return_value = storage.Blob( + name=destination_object_name, bucket=storage.Bucket(mock_service, destination_bucket_name) + ) + mock_service.return_value.bucket.side_effect = ( + lambda name: source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) + + self.gcs_hook.copy( + source_bucket=source_bucket_name, + source_object=source_object_name, + destination_bucket=destination_bucket_name, + destination_object=destination_object_name, + ) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset( + uri=f"gs://{destination_bucket_name}/{destination_object_name}" + ) + @mock.patch("google.cloud.storage.Bucket") @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_rewrite(self, mock_service, mock_bucket): @@ -474,6 +511,40 @@ def test_rewrite_empty_source_object(self): assert str(ctx.value) == "source_bucket and source_object cannot be empty." + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_rewrite_exposes_lineage(self, mock_service, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + destination_bucket_name = "test-dest-bucket" + destination_object_name = "test-dest-object" + + dest_bucket = storage.Bucket(mock_service, destination_bucket_name) + blob = MagicMock(spec=storage.Blob) + blob.rewrite = MagicMock(return_value=(None, None, None)) + dest_bucket.blob = MagicMock(return_value=blob) + mock_service.return_value.bucket.side_effect = ( + lambda name: storage.Bucket(mock_service, source_bucket_name) + if name == source_bucket_name + else dest_bucket + ) + + self.gcs_hook.rewrite( + source_bucket=source_bucket_name, + source_object=source_object_name, + destination_bucket=destination_bucket_name, + destination_object=destination_object_name, + ) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset( + uri=f"gs://{destination_bucket_name}/{destination_object_name}" + ) + @mock.patch("google.cloud.storage.Bucket") @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_delete(self, mock_service, mock_bucket): @@ -502,6 +573,22 @@ def test_delete_nonexisting_object(self, mock_service): with pytest.raises(exceptions.NotFound): self.gcs_hook.delete(bucket_name=test_bucket, object_name=test_object) + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_delete_exposes_lineage(self, mock_service, hook_lineage_collector): + test_bucket = "test_bucket" + test_object = "test_object" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, test_bucket) + + self.gcs_hook.delete(bucket_name=test_bucket, object_name=test_object) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 0 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{test_bucket}/{test_object}" + ) + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_delete_bucket(self, mock_service): test_bucket = "test bucket" @@ -729,6 +816,33 @@ def test_compose_without_destination_object(self, mock_service): assert str(ctx.value) == "bucket_name and destination_object cannot be empty." + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_compose_exposes_lineage(self, mock_service, hook_lineage_collector): + test_bucket = "test_bucket" + source_object_names = ["test-source-object1", "test-source-object2"] + destination_object_name = "test-dest-object" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, test_bucket) + + self.gcs_hook.compose( + bucket_name=test_bucket, + source_objects=source_object_names, + destination_object=destination_object_name, + ) + + assert len(hook_lineage_collector.collected_assets.inputs) == 2 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{test_bucket}/{source_object_names[0]}" + ) + assert hook_lineage_collector.collected_assets.inputs[1].asset == Asset( + uri=f"gs://{test_bucket}/{source_object_names[1]}" + ) + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset( + uri=f"gs://{test_bucket}/{destination_object_name}" + ) + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_download_as_bytes(self, mock_service): test_bucket = "test_bucket" @@ -743,6 +857,23 @@ def test_download_as_bytes(self, mock_service): assert response == test_object_bytes download_method.assert_called_once_with() + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch("google.cloud.storage.Blob.download_as_bytes") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_download_as_bytes_exposes_lineage(self, mock_service, mock_download, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, source_bucket_name) + + self.gcs_hook.download(bucket_name=source_bucket_name, object_name=source_object_name, filename=None) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 0 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_download_to_file(self, mock_service): test_bucket = "test_bucket" @@ -766,6 +897,27 @@ def test_download_to_file(self, mock_service): assert response == test_file download_filename_method.assert_called_once_with(test_file, timeout=60) + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch("google.cloud.storage.Blob.download_to_filename") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_download_to_file_exposes_lineage(self, mock_service, mock_download, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + file_name = "test.txt" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, source_bucket_name) + + self.gcs_hook.download( + bucket_name=source_bucket_name, object_name=source_object_name, filename=file_name + ) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset(uri=f"file://{file_name}") + @mock.patch(GCS_STRING.format("NamedTemporaryFile")) @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_provide_file(self, mock_service, mock_temp_file): @@ -999,6 +1151,27 @@ def test_upload_file(self, mock_service, testdata_file): assert metadata == blob_object.return_value.metadata + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch("google.cloud.storage.Blob.upload_from_filename") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_upload_file_exposes_lineage(self, mock_service, mock_upload, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + file_name = "test.txt" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, source_bucket_name) + + self.gcs_hook.upload( + bucket_name=source_bucket_name, object_name=source_object_name, filename=file_name + ) + + assert len(hook_lineage_collector.collected_assets.inputs) == 1 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + assert hook_lineage_collector.collected_assets.inputs[0].asset == Asset(uri=f"file://{file_name}") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) def test_upload_cache_control(self, mock_service, testdata_file): test_bucket = "test_bucket" @@ -1042,6 +1215,23 @@ def test_upload_data_bytes(self, mock_service, testdata_bytes): upload_method.assert_called_once_with(testdata_bytes, content_type="text/plain", timeout=60) + @pytest.mark.skipif(not AIRFLOW_V_2_10_PLUS, reason="Hook lineage works in Airflow >= 2.10.0") + @mock.patch("google.cloud.storage.Blob.upload_from_string") + @mock.patch(GCS_STRING.format("GCSHook.get_conn")) + def test_upload_data_exposes_lineage(self, mock_service, mock_upload, hook_lineage_collector): + source_bucket_name = "test-source-bucket" + source_object_name = "test-source-object" + + mock_service.return_value.bucket.return_value = storage.Bucket(mock_service, source_bucket_name) + + self.gcs_hook.upload(bucket_name=source_bucket_name, object_name=source_object_name, data="test") + + assert len(hook_lineage_collector.collected_assets.inputs) == 0 + assert len(hook_lineage_collector.collected_assets.outputs) == 1 + assert hook_lineage_collector.collected_assets.outputs[0].asset == Asset( + uri=f"gs://{source_bucket_name}/{source_object_name}" + ) + @mock.patch(GCS_STRING.format("BytesIO")) @mock.patch(GCS_STRING.format("gz.GzipFile")) @mock.patch(GCS_STRING.format("GCSHook.get_conn")) From 0b030c562363dd924bbbee0793636be18deeabe3 Mon Sep 17 00:00:00 2001 From: Amogh Desai Date: Wed, 23 Oct 2024 21:16:47 +0530 Subject: [PATCH 084/258] Masking configuration values irrelevant to DAG author (#43040) Some configurations are irrelevant to DAG authors and hence we need to mask those to avoid it from getting logged unknowingly. Co-authored-by: adesai Co-authored-by: Ash Berlin-Taylor --- airflow/configuration.py | 15 +++++++++++++++ airflow/settings.py | 3 +++ tests/core/test_configuration.py | 15 +++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/airflow/configuration.py b/airflow/configuration.py index e59b5b5e9ec10..461723f374994 100644 --- a/airflow/configuration.py +++ b/airflow/configuration.py @@ -772,6 +772,21 @@ def _create_future_warning(name: str, section: str, current_value: Any, new_valu stacklevel=3, ) + def mask_secrets(self): + from airflow.utils.log.secrets_masker import mask_secret + + for section, key in self.sensitive_config_values: + try: + value = self.get(section, key) + except AirflowConfigException: + log.debug( + "Could not retrieve value from section %s, for key %s. Skipping redaction of this conf.", + section, + key, + ) + continue + mask_secret(value) + def _env_var_name(self, section: str, key: str) -> str: return f"{ENV_VAR_PREFIX}{section.replace('.', '_').upper()}__{key.upper()}" diff --git a/airflow/settings.py b/airflow/settings.py index a6adbbcf9ff77..57c382e2a1a1c 100644 --- a/airflow/settings.py +++ b/airflow/settings.py @@ -741,6 +741,9 @@ def initialize(): configure_orm() configure_action_logging() + # mask the sensitive_config_values + conf.mask_secrets() + # Run any custom runtime checks that needs to be executed for providers run_providers_custom_runtime_checks() diff --git a/tests/core/test_configuration.py b/tests/core/test_configuration.py index 096b55e0f8e6f..583472eb0a64d 100644 --- a/tests/core/test_configuration.py +++ b/tests/core/test_configuration.py @@ -1763,3 +1763,18 @@ def test_config_paths_is_directory(self): with pytest.raises(IsADirectoryError, match="configuration file, but got a directory"): write_default_airflow_configuration_if_needed() + + @conf_vars({("mysection1", "mykey1"): "supersecret1", ("mysection2", "mykey2"): "supersecret2"}) + @patch.object( + conf, + "sensitive_config_values", + new_callable=lambda: [("mysection1", "mykey1"), ("mysection2", "mykey2")], + ) + @patch("airflow.utils.log.secrets_masker.mask_secret") + def test_mask_conf_values(self, mock_mask_secret, mock_sensitive_config_values): + conf.mask_secrets() + + mock_mask_secret.assert_any_call("supersecret1") + mock_mask_secret.assert_any_call("supersecret2") + + assert mock_mask_secret.call_count == 2 From 9b053bc4a3e1a25d8b3200f013cacd7187f2945f Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Wed, 23 Oct 2024 17:23:50 +0100 Subject: [PATCH 085/258] Fix failing main (#43322) Test was failing here: https://github.com/apache/airflow/actions/runs/11482958870/job/31957874656?pr=43291 ``` =========================== short test summary info ============================ FAILED tests/always/test_project_structure.py::TestProjectStructure::test_providers_modules_should_have_tests - AssertionError: Detect added tests in providers module - please remove the tests from OVERLOOKED_TESTS list above assert equals failed set() set([ 'providers/tests/google/cloud /sensors/test_dataform.py', ]) = 1 failed, 12165 passed, 9445 skipped, 2 xfailed, 5 warnings in 571.47s (0:09:31) = ``` This was because the test was added in https://github.com/apache/airflow/pull/43055/ --- .../tests/system/google/cloud/dataform/example_dataform.py | 4 ++-- tests/always/test_project_structure.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/providers/tests/system/google/cloud/dataform/example_dataform.py b/providers/tests/system/google/cloud/dataform/example_dataform.py index f15e629b0f4b6..bf2a2a3eb8eda 100644 --- a/providers/tests/system/google/cloud/dataform/example_dataform.py +++ b/providers/tests/system/google/cloud/dataform/example_dataform.py @@ -179,7 +179,7 @@ # [START howto_operator_create_workflow_invocation_action_async] create_workflow_invocation_async_action = DataformCreateWorkflowInvocationOperator( - task_id="create-workflow-invocation-async", + task_id="create-workflow-invocation-async-action", project_id=PROJECT_ID, region=REGION, repository_id=REPOSITORY_ID, @@ -190,7 +190,7 @@ ) is_workflow_invocation_action_done = DataformWorkflowInvocationActionStateSensor( - task_id="is-workflow-invocation-done", + task_id="is-workflow-invocation-action-done", project_id=PROJECT_ID, region=REGION, repository_id=REPOSITORY_ID, diff --git a/tests/always/test_project_structure.py b/tests/always/test_project_structure.py index 9db0b22df84fc..38b5bccba4412 100644 --- a/tests/always/test_project_structure.py +++ b/tests/always/test_project_structure.py @@ -137,7 +137,6 @@ def test_providers_modules_should_have_tests(self): "providers/tests/google/cloud/operators/vertex_ai/test_hyperparameter_tuning_job.py", "providers/tests/google/cloud/operators/vertex_ai/test_model_service.py", "providers/tests/google/cloud/operators/vertex_ai/test_pipeline_job.py", - "providers/tests/google/cloud/sensors/test_dataform.py", "providers/tests/google/cloud/transfers/test_bigquery_to_sql.py", "providers/tests/google/cloud/transfers/test_presto_to_gcs.py", "providers/tests/google/cloud/utils/test_bigquery.py", From 2bf3af0a65631bb6b9271fca8a0c6724c996393c Mon Sep 17 00:00:00 2001 From: Kaxil Naik Date: Wed, 23 Oct 2024 17:24:51 +0100 Subject: [PATCH 086/258] Remove deprecated hook code from plugins (#43291) We had removed registering hooks via plugin in https://github.com/apache/airflow/pull/12108 for Airflow 2.0. We kept it for registering Connection form. As I understand it, we don't use that code but `ProvidersManager` (`airflow/providers_manager.py`) to register Connections from providers. --- .../core_api/openapi/v1-generated.yaml | 6 ---- .../core_api/serializers/plugins.py | 1 - airflow/plugins_manager.py | 14 ++------- .../ui/openapi-gen/requests/schemas.gen.ts | 8 ----- airflow/ui/openapi-gen/requests/types.gen.ts | 1 - .../authoring-and-scheduling/plugins.rst | 8 ----- newsfragments/43291.significant.rst | 16 ++++++++++ tests/always/test_connection.py | 29 ------------------- .../endpoints/test_plugin_endpoint.py | 6 ---- .../schemas/test_plugin_schema.py | 8 ----- tests/cli/commands/test_plugins_command.py | 20 ++++++++----- tests/plugins/test_plugin.py | 9 ------ tests/plugins/test_plugins_manager.py | 24 --------------- tests_common/test_utils/mock_plugins.py | 2 -- 14 files changed, 30 insertions(+), 122 deletions(-) create mode 100644 newsfragments/43291.significant.rst diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 3d890d4da310d..5eb7aed770532 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -2207,11 +2207,6 @@ components: name: type: string title: Name - hooks: - items: - type: string - type: array - title: Hooks macros: items: type: string @@ -2268,7 +2263,6 @@ components: type: object required: - name - - hooks - macros - flask_blueprints - fastapi_apps diff --git a/airflow/api_fastapi/core_api/serializers/plugins.py b/airflow/api_fastapi/core_api/serializers/plugins.py index 68bc8ea443c25..e16b56a6aca06 100644 --- a/airflow/api_fastapi/core_api/serializers/plugins.py +++ b/airflow/api_fastapi/core_api/serializers/plugins.py @@ -64,7 +64,6 @@ class PluginResponse(BaseModel): """Plugin serializer.""" name: str - hooks: list[str] macros: list[str] flask_blueprints: list[str] fastapi_apps: list[FastAPIAppResponse] diff --git a/airflow/plugins_manager.py b/airflow/plugins_manager.py index fc7adc5993f64..881e07a81ed67 100644 --- a/airflow/plugins_manager.py +++ b/airflow/plugins_manager.py @@ -50,7 +50,6 @@ from types import ModuleType from typing import Generator - from airflow.hooks.base import BaseHook from airflow.listeners.listener import ListenerManager from airflow.timetables.base import Timetable @@ -62,7 +61,6 @@ loaded_plugins: set[str] = set() # Plugin components to integrate as modules -registered_hooks: list[BaseHook] | None = None macros_modules: list[Any] | None = None # Plugin components to integrate directly @@ -86,7 +84,6 @@ during deserialization """ PLUGINS_ATTRIBUTES_TO_DUMP = { - "hooks", "macros", "admin_views", "flask_blueprints", @@ -151,7 +148,6 @@ class AirflowPlugin: name: str | None = None source: AirflowPluginSource | None = None - hooks: list[Any] = [] macros: list[Any] = [] admin_views: list[Any] = [] flask_blueprints: list[Any] = [] @@ -345,7 +341,7 @@ def ensure_plugins_loaded(): """ from airflow.stats import Stats - global plugins, registered_hooks + global plugins if plugins is not None: log.debug("Plugins are already loaded. Skipping.") @@ -358,7 +354,6 @@ def ensure_plugins_loaded(): with Stats.timer() as timer: plugins = [] - registered_hooks = [] load_plugins_from_plugin_directory() load_entrypoint_plugins() @@ -366,11 +361,6 @@ def ensure_plugins_loaded(): if not settings.LAZY_LOAD_PROVIDERS: load_providers_plugins() - # We don't do anything with these for now, but we want to keep track of - # them so we can integrate them in to the UI's Connection screens - for plugin in plugins: - registered_hooks.extend(plugin.hooks) - if plugins: log.debug("Loading %d plugin(s) took %.2f seconds", len(plugins), timer.duration) @@ -598,7 +588,7 @@ def get_plugin_info(attrs_to_dump: Iterable[str] | None = None) -> list[dict[str for attr in attrs_to_dump: if attr in ("global_operator_extra_links", "operator_extra_links"): info[attr] = [f"<{qualname(d.__class__)} object>" for d in getattr(plugin, attr)] - elif attr in ("macros", "timetables", "hooks", "priority_weight_strategies"): + elif attr in ("macros", "timetables", "priority_weight_strategies"): info[attr] = [qualname(d) for d in getattr(plugin, attr)] elif attr == "listeners": # listeners may be modules or class instances diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index 356814d15d53a..afd16d804091a 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1326,13 +1326,6 @@ export const $PluginResponse = { type: "string", title: "Name", }, - hooks: { - items: { - type: "string", - }, - type: "array", - title: "Hooks", - }, macros: { items: { type: "string", @@ -1411,7 +1404,6 @@ export const $PluginResponse = { type: "object", required: [ "name", - "hooks", "macros", "flask_blueprints", "fastapi_apps", diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 4005412147078..aef0f76f28e11 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -310,7 +310,6 @@ export type PluginCollectionResponse = { */ export type PluginResponse = { name: string; - hooks: Array; macros: Array; flask_blueprints: Array; fastapi_apps: Array; diff --git a/docs/apache-airflow/authoring-and-scheduling/plugins.rst b/docs/apache-airflow/authoring-and-scheduling/plugins.rst index 626f20eab1b9c..a46fe329da195 100644 --- a/docs/apache-airflow/authoring-and-scheduling/plugins.rst +++ b/docs/apache-airflow/authoring-and-scheduling/plugins.rst @@ -108,8 +108,6 @@ looks like: class AirflowPlugin: # The name of your plugin (str) name = None - # A list of class(es) derived from BaseHook - hooks = [] # A list of references to inject into the macros namespace macros = [] # A list of Blueprint object created from flask.Blueprint. For use with the flask_appbuilder based GUI @@ -182,11 +180,6 @@ definitions in Airflow. from airflow.providers.amazon.aws.transfers.gcs_to_s3 import GCSToS3Operator - # Will show up in Connections screen in a future version - class PluginHook(BaseHook): - pass - - # Will show up under airflow.macros.test_plugin.plugin_macro # and in templates through {{ macros.test_plugin.plugin_macro }} def plugin_macro(): @@ -267,7 +260,6 @@ definitions in Airflow. # Defining the plugin class class AirflowTestPlugin(AirflowPlugin): name = "test_plugin" - hooks = [PluginHook] macros = [plugin_macro] flask_blueprints = [bp] fastapi_apps = [app_with_metadata] diff --git a/newsfragments/43291.significant.rst b/newsfragments/43291.significant.rst new file mode 100644 index 0000000000000..ec7cacf6f153b --- /dev/null +++ b/newsfragments/43291.significant.rst @@ -0,0 +1,16 @@ +Support for adding Hooks via Airflow Plugins is removed + +Hooks should no longer be registered or imported via Airflow's plugin mechanism -- these types of classes +are just treated as plain Python classes by Airflow, so there is no need to register them with Airflow. + +Before: + +.. code-block:: python + + from airflow.hooks.my_plugin import MyHook + +You should instead import it as: + +.. code-block:: python + + from my_plugin import MyHook diff --git a/tests/always/test_connection.py b/tests/always/test_connection.py index 03a8e136f2a0a..3cc24b6d9ce38 100644 --- a/tests/always/test_connection.py +++ b/tests/always/test_connection.py @@ -32,7 +32,6 @@ from airflow.hooks.base import BaseHook from airflow.models import Connection, crypto from airflow.providers.sqlite.hooks.sqlite import SqliteHook -from airflow.providers_manager import HookInfo from tests_common.test_utils.config import conf_vars @@ -835,31 +834,3 @@ def test_as_json_from_connection(self, conn: Connection, expected_json, request) assert restored_conn.schema == conn.schema assert restored_conn.port == conn.port assert restored_conn.extra_dejson == conn.extra_dejson - - def test_get_hook_not_found(self): - with mock.patch("airflow.providers_manager.ProvidersManager") as m: - m.return_value.hooks = {} - with pytest.raises(AirflowException, match='Unknown hook type "awesome-test-conn-type"'): - Connection(conn_type="awesome-test-conn-type").get_hook() - - def test_get_hook_import_error(self, caplog): - with mock.patch("airflow.providers_manager.ProvidersManager") as m: - m.return_value.hooks = { - "awesome-test-conn-type": HookInfo( - hook_class_name="foo.bar.AwesomeTest", - connection_id_attribute_name="conn-id", - package_name="test.package", - hook_name="Awesome Connection", - connection_type="awesome-test-conn-type", - connection_testable=False, - ) - } - caplog.clear() - caplog.set_level("ERROR", "airflow.models.connection") - with pytest.raises(ImportError): - Connection(conn_type="awesome-test-conn-type").get_hook() - - assert caplog.messages - assert caplog.messages[0] == ( - "Could not import foo.bar.AwesomeTest when discovering Awesome Connection test.package" - ) diff --git a/tests/api_connexion/endpoints/test_plugin_endpoint.py b/tests/api_connexion/endpoints/test_plugin_endpoint.py index 487ba53a30080..2f24347bef68f 100644 --- a/tests/api_connexion/endpoints/test_plugin_endpoint.py +++ b/tests/api_connexion/endpoints/test_plugin_endpoint.py @@ -23,7 +23,6 @@ from flask import Blueprint from flask_appbuilder import BaseView -from airflow.hooks.base import BaseHook from airflow.plugins_manager import AirflowPlugin from airflow.ti_deps.deps.base_ti_dep import BaseTIDep from airflow.timetables.base import Timetable @@ -37,9 +36,6 @@ pytestmark = [pytest.mark.db_test, pytest.mark.skip_if_database_isolation_mode] -class PluginHook(BaseHook): ... - - def plugin_macro(): ... @@ -100,7 +96,6 @@ class MockPlugin(AirflowPlugin): appbuilder_menu_items = [appbuilder_menu_items] global_operator_extra_links = [MockOperatorLink()] operator_extra_links = [MockOperatorLink()] - hooks = [PluginHook] macros = [plugin_macro] ti_deps = [ti_dep] timetables = [CustomTimetable] @@ -156,7 +151,6 @@ def test_get_plugins_return_200(self): } ], "global_operator_extra_links": [f"<{qualname(MockOperatorLink().__class__)} object>"], - "hooks": [qualname(PluginHook)], "macros": [qualname(plugin_macro)], "operator_extra_links": [f"<{qualname(MockOperatorLink().__class__)} object>"], "source": None, diff --git a/tests/api_connexion/schemas/test_plugin_schema.py b/tests/api_connexion/schemas/test_plugin_schema.py index 951933e9ffc44..d722b52977aad 100644 --- a/tests/api_connexion/schemas/test_plugin_schema.py +++ b/tests/api_connexion/schemas/test_plugin_schema.py @@ -26,15 +26,11 @@ plugin_collection_schema, plugin_schema, ) -from airflow.hooks.base import BaseHook from airflow.plugins_manager import AirflowPlugin from tests_common.test_utils.compat import BaseOperatorLink -class PluginHook(BaseHook): ... - - def plugin_macro(): ... @@ -67,7 +63,6 @@ class MockPlugin(AirflowPlugin): appbuilder_menu_items = [appbuilder_menu_items] global_operator_extra_links = [MockOperatorLink()] operator_extra_links = [MockOperatorLink()] - hooks = [PluginHook] macros = [plugin_macro] @@ -91,7 +86,6 @@ def test_serialize(self): {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, ], "global_operator_extra_links": [str(MockOperatorLink())], - "hooks": [str(PluginHook)], "macros": [str(plugin_macro)], "operator_extra_links": [str(MockOperatorLink())], "source": None, @@ -117,7 +111,6 @@ def test_serialize(self): {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, ], "global_operator_extra_links": [str(MockOperatorLink())], - "hooks": [str(PluginHook)], "macros": [str(plugin_macro)], "operator_extra_links": [str(MockOperatorLink())], "source": None, @@ -134,7 +127,6 @@ def test_serialize(self): {"app": app, "name": "App name", "url_prefix": "/some_prefix"}, ], "global_operator_extra_links": [str(MockOperatorLink())], - "hooks": [str(PluginHook)], "macros": [str(plugin_macro)], "operator_extra_links": [str(MockOperatorLink())], "source": None, diff --git a/tests/cli/commands/test_plugins_command.py b/tests/cli/commands/test_plugins_command.py index c9807520e4ed3..993d4085fe1d1 100644 --- a/tests/cli/commands/test_plugins_command.py +++ b/tests/cli/commands/test_plugins_command.py @@ -25,8 +25,8 @@ from airflow.cli import cli_parser from airflow.cli.commands import plugins_command -from airflow.hooks.base import BaseHook from airflow.listeners.listener import get_listener_manager +from airflow.models.baseoperatorlink import BaseOperatorLink from airflow.plugins_manager import AirflowPlugin from tests.plugins.test_plugin import AirflowTestPlugin as ComplexAirflowPlugin @@ -35,13 +35,18 @@ pytestmark = [pytest.mark.db_test, pytest.mark.skip_if_database_isolation_mode] -class PluginHook(BaseHook): - pass +class AirflowNewLink(BaseOperatorLink): + """Operator Link for Apache Airflow Website.""" + + name = "airflowtestlink" + + def get_link(self, operator, *, ti_key): + return "https://airflow.apache.org" class TestPlugin(AirflowPlugin): name = "test-plugin-cli" - hooks = [PluginHook] + global_operator_extra_links = [AirflowNewLink()] class TestPluginsCommand: @@ -98,7 +103,6 @@ def test_should_display_one_plugin(self): "", "", ], - "hooks": ["tests.plugins.test_plugin.PluginHook"], "listeners": [ "tests.listeners.empty_listener", "tests.listeners.class_listener.ClassBasedListener", @@ -129,9 +133,9 @@ def test_should_display_one_plugins_as_table(self): # Assert that only columns with values are displayed expected_output = textwrap.dedent( """\ - name | hooks - ================+=================================================== - test-plugin-cli | tests.cli.commands.test_plugins_command.PluginHook + name | global_operator_extra_links + ================+================================================================ + test-plugin-cli | """ ) assert stdout == expected_output diff --git a/tests/plugins/test_plugin.py b/tests/plugins/test_plugin.py index 98f64e75456f5..531085ffba586 100644 --- a/tests/plugins/test_plugin.py +++ b/tests/plugins/test_plugin.py @@ -21,9 +21,6 @@ from flask import Blueprint from flask_appbuilder import BaseView as AppBuilderBaseView, expose -# Importing base classes that we need to derive -from airflow.hooks.base import BaseHook - # This is the class you derive to create a plugin from airflow.plugins_manager import AirflowPlugin from airflow.task.priority_strategy import PriorityWeightStrategy @@ -42,11 +39,6 @@ ) -# Will show up under airflow.hooks.test_plugin.PluginHook -class PluginHook(BaseHook): - pass - - # Will show up under airflow.macros.test_plugin.plugin_macro def plugin_macro(): pass @@ -115,7 +107,6 @@ def get_weight(self, ti): # Defining the plugin class class AirflowTestPlugin(AirflowPlugin): name = "test_plugin" - hooks = [PluginHook] macros = [plugin_macro] flask_blueprints = [bp] fastapi_apps = [app_with_metadata] diff --git a/tests/plugins/test_plugins_manager.py b/tests/plugins/test_plugins_manager.py index 158e2040abf7a..e6777901b8041 100644 --- a/tests/plugins/test_plugins_manager.py +++ b/tests/plugins/test_plugins_manager.py @@ -28,7 +28,6 @@ import pytest -from airflow.hooks.base import BaseHook from airflow.listeners.listener import get_listener_manager from airflow.plugins_manager import AirflowPlugin from airflow.utils.module_loading import qualname @@ -181,29 +180,6 @@ def test_no_log_when_no_plugins(self, caplog): assert caplog.record_tuples == [] - def test_should_load_plugins_from_property(self, caplog): - class AirflowTestPropertyPlugin(AirflowPlugin): - name = "test_property_plugin" - - @property - def hooks(self): - class TestPropertyHook(BaseHook): - pass - - return [TestPropertyHook] - - with mock_plugin_manager(plugins=[AirflowTestPropertyPlugin()]): - from airflow import plugins_manager - - caplog.set_level(logging.DEBUG, "airflow.plugins_manager") - plugins_manager.ensure_plugins_loaded() - - assert "AirflowTestPropertyPlugin" in str(plugins_manager.plugins) - assert "TestPropertyHook" in str(plugins_manager.registered_hooks) - - assert caplog.records[-1].levelname == "DEBUG" - assert caplog.records[-1].msg == "Loading %d plugin(s) took %.2f seconds" - def test_loads_filesystem_plugins(self, caplog): from airflow import plugins_manager diff --git a/tests_common/test_utils/mock_plugins.py b/tests_common/test_utils/mock_plugins.py index 875a9abbd3a0f..0a5e199fcc089 100644 --- a/tests_common/test_utils/mock_plugins.py +++ b/tests_common/test_utils/mock_plugins.py @@ -23,7 +23,6 @@ PLUGINS_MANAGER_NULLABLE_ATTRIBUTES = [ "plugins", - "registered_hooks", "macros_modules", "admin_views", "flask_blueprints", @@ -41,7 +40,6 @@ PLUGINS_MANAGER_NULLABLE_ATTRIBUTES_V2_10 = [ "plugins", - "registered_hooks", "macros_modules", "admin_views", "flask_blueprints", From 813963a0e6966ca7f8adb017135620e47481905e Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Wed, 23 Oct 2024 15:32:59 -0400 Subject: [PATCH 087/258] Rename DAG to Dag in new UI (#43325) --- airflow/ui/src/components/SearchBar.tsx | 2 +- airflow/ui/src/layouts/Nav/Nav.tsx | 4 ++-- airflow/ui/src/pages/DagsList/DagsList.tsx | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/airflow/ui/src/components/SearchBar.tsx b/airflow/ui/src/components/SearchBar.tsx index ad50a65b4311b..c0d8d6f32d53f 100644 --- a/airflow/ui/src/components/SearchBar.tsx +++ b/airflow/ui/src/components/SearchBar.tsx @@ -52,7 +52,7 @@ export const SearchBar = ({ { } title="Home" to="/" /> } - title="DAGs" + title="Dags" to="dags" /> { } isDisabled - title="DAG Runs" + title="Dag Runs" to="dag_runs" /> > = [ { accessorKey: "dag_id", cell: ({ row }) => row.original.dag_display_name, - header: "DAG", + header: "Dag", }, { accessorKey: "timetable_description", @@ -88,7 +88,7 @@ const columns: Array> = [

    {{ page_title }}

    return false; } - {% if scarf_url %} - - {% endif %} {% endblock %} diff --git a/airflow/www/views.py b/airflow/www/views.py index c153cc80597f5..d50d7bb2e78e1 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -117,7 +117,7 @@ from airflow.timetables._cron import CronMixin from airflow.timetables.base import DataInterval, TimeRestriction from airflow.timetables.simple import ContinuousTimetable -from airflow.utils import json as utils_json, timezone, usage_data_collection, yaml +from airflow.utils import json as utils_json, timezone, yaml from airflow.utils.airflow_flask_app import get_airflow_app from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.dag_edges import dag_edges @@ -219,45 +219,6 @@ def get_safe_url(url): return redirect_url.geturl() -def build_scarf_url(dags_count: int) -> str: - """ - Build the URL for the Scarf usage data collection. - - :meta private: - """ - if not settings.is_usage_data_collection_enabled(): - return "" - - scarf_domain = "https://apacheairflow.gateway.scarf.sh" - platform_sys, platform_arch = usage_data_collection.get_platform_info() - db_version = usage_data_collection.get_database_version() - db_name = usage_data_collection.get_database_name() - executor = usage_data_collection.get_executor() - python_version = usage_data_collection.get_python_version() - plugin_counts = usage_data_collection.get_plugin_counts() - plugins_count = plugin_counts["plugins"] - flask_blueprints_count = plugin_counts["flask_blueprints"] - appbuilder_views_count = plugin_counts["appbuilder_views"] - appbuilder_menu_items_count = plugin_counts["appbuilder_menu_items"] - timetables_count = plugin_counts["timetables"] - dag_bucket = usage_data_collection.to_bucket(dags_count) - plugins_bucket = usage_data_collection.to_bucket(plugins_count) - timetable_bucket = usage_data_collection.to_bucket(timetables_count) - - # Path Format: - # /{version}/{python_version}/{platform}/{arch}/{database}/{db_version}/{executor}/{num_dags}/{plugin_count}/{flask_blueprint_count}/{appbuilder_view_count}/{appbuilder_menu_item_count}/{timetables} - # - # This path redirects to a Pixel tracking URL - scarf_url = ( - f"{scarf_domain}/webserver" - f"/{version}/{python_version}" - f"/{platform_sys}/{platform_arch}/{db_name}/{db_version}/{executor}/{dag_bucket}" - f"/{plugins_bucket}/{flask_blueprints_count}/{appbuilder_views_count}/{appbuilder_menu_items_count}/{timetable_bucket}" - ) - - return scarf_url - - def get_date_time_num_runs_dag_runs_form_data(www_request, session, dag): """Get Execution Data, Base Date & Number of runs from a Request.""" date_time = www_request.args.get("execution_date") @@ -1125,11 +1086,6 @@ def _iter_parsed_moved_data_table_names(): "warning", ) - try: - scarf_url = build_scarf_url(dags_count=all_dags_count) - except Exception: - scarf_url = "" - return self.render_template( "airflow/dags.html", dags=dags, @@ -1169,7 +1125,6 @@ def _iter_parsed_moved_data_table_names(): sorting_direction=arg_sorting_direction, auto_refresh_interval=conf.getint("webserver", "auto_refresh_interval"), asset_triggered_next_run_info=asset_triggered_next_run_info, - scarf_url=scarf_url, file_tokens=file_tokens, ) diff --git a/docs/apache-airflow/faq.rst b/docs/apache-airflow/faq.rst index 0b2c76765e704..2f60446c7badb 100644 --- a/docs/apache-airflow/faq.rst +++ b/docs/apache-airflow/faq.rst @@ -545,6 +545,3 @@ The telemetry data collected is limited to the following: - Operating system & machine architecture - Executor - Metadata DB type & its version -- Number of DAGs -- Number of Airflow plugins -- Number of timetables, Flask blueprints, Flask AppBuilder views, and Flask Appbuilder menu items from Airflow plugins diff --git a/tests/utils/test_usage_data_collection.py b/tests/utils/test_usage_data_collection.py index b104d1bfe365a..bc973672089c9 100644 --- a/tests/utils/test_usage_data_collection.py +++ b/tests/utils/test_usage_data_collection.py @@ -27,7 +27,6 @@ from airflow.utils.usage_data_collection import ( get_database_version, get_python_version, - to_bucket, usage_data_collection, ) @@ -101,20 +100,3 @@ def test_get_database_version(version_info, expected_version): def test_get_python_version(version_info, expected_version): with mock.patch("platform.python_version", return_value=version_info): assert get_python_version() == expected_version - - -@pytest.mark.parametrize( - "counter, expected_bucket", - [ - (0, "0"), - (1, "1-5"), - (5, "1-5"), - (6, "6-10"), - (11, "11-20"), - (20, "11-20"), - (21, "21-50"), - (10000, "2000+"), - ], -) -def test_to_bucket(counter, expected_bucket): - assert to_bucket(counter) == expected_bucket diff --git a/tests/www/views/test_views.py b/tests/www/views/test_views.py index fc2f0b7565747..e447746b6305c 100644 --- a/tests/www/views/test_views.py +++ b/tests/www/views/test_views.py @@ -25,7 +25,6 @@ import pytest from markupsafe import Markup -from airflow import __version__ as airflow_version from airflow.configuration import ( initialize_config, write_default_airflow_configuration_if_needed, @@ -36,7 +35,6 @@ from airflow.utils.task_group import TaskGroup from airflow.www.views import ( ProviderView, - build_scarf_url, get_key_paths, get_safe_url, get_task_stats_from_query, @@ -608,39 +606,3 @@ def test_invalid_dates(app, admin_client, url, content): assert resp.status_code == 400 assert re.search(content, resp.get_data().decode()) - - -@pytest.mark.parametrize("enabled", [False, True]) -@patch("airflow.utils.usage_data_collection.get_platform_info", return_value=("Linux", "x86_64")) -@patch("airflow.utils.usage_data_collection.get_database_version", return_value="12.3") -@patch("airflow.utils.usage_data_collection.get_database_name", return_value="postgres") -@patch("airflow.utils.usage_data_collection.get_executor", return_value="SequentialExecutor") -@patch("airflow.utils.usage_data_collection.get_python_version", return_value="3.9") -@patch("airflow.utils.usage_data_collection.get_plugin_counts") -def test_build_scarf_url( - get_plugin_counts, - get_python_version, - get_executor, - get_database_name, - get_database_version, - get_platform_info, - enabled, -): - get_plugin_counts.return_value = { - "plugins": 10, - "flask_blueprints": 15, - "appbuilder_views": 20, - "appbuilder_menu_items": 25, - "timetables": 30, - } - with patch("airflow.settings.is_usage_data_collection_enabled", return_value=enabled): - result = build_scarf_url(5) - expected_url = ( - "https://apacheairflow.gateway.scarf.sh/webserver/" - f"{airflow_version}/3.9/Linux/x86_64/postgres/12.3/SequentialExecutor/1-5" - f"/6-10/15/20/25/21-50" - ) - if enabled: - assert result == expected_url - else: - assert result == "" diff --git a/tests/www/views/test_views_home.py b/tests/www/views/test_views_home.py index c7068f0191bc8..44684cdb9ca76 100644 --- a/tests/www/views/test_views_home.py +++ b/tests/www/views/test_views_home.py @@ -459,20 +459,6 @@ def test_sorting_home_view(url, lower_key, greater_key, user_client, _working_da assert lower_index < greater_index -@pytest.mark.parametrize("is_enabled, should_have_pixel", [(False, False), (True, True)]) -def test_analytics_pixel(user_client, is_enabled, should_have_pixel): - """ - Test that the analytics pixel is not included when the feature is disabled - """ - with mock.patch("airflow.settings.is_usage_data_collection_enabled", return_value=is_enabled): - resp = user_client.get("home", follow_redirects=True) - - if should_have_pixel: - check_content_in_response("apacheairflow.gateway.scarf.sh", resp) - else: - check_content_not_in_response("apacheairflow.gateway.scarf.sh", resp) - - @pytest.mark.parametrize( "url, filter_tags_cookie_val, filter_lastrun_cookie_val, expected_filter_tags, expected_filter_lastrun", [ From 27f3be666cb3aed6668e509ef1a9a84782eacb5d Mon Sep 17 00:00:00 2001 From: Omkar P <45419097+omkar-foss@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:25:15 +0530 Subject: [PATCH 100/258] Migrate public endpoint Get Airflow Version to FastAPI (#43312) * Migrate public endpoint Get Airflow Version to FastAPI * Run static checks --- .../core_api/openapi/v1-generated.yaml | 30 +++++++++++ .../core_api/routes/public/__init__.py | 2 + .../core_api/routes/public/version.py | 34 +++++++++++++ .../core_api/serializers/version.py | 26 ++++++++++ airflow/ui/openapi-gen/queries/common.ts | 13 +++++ airflow/ui/openapi-gen/queries/prefetch.ts | 12 +++++ airflow/ui/openapi-gen/queries/queries.ts | 20 ++++++++ airflow/ui/openapi-gen/queries/suspense.ts | 20 ++++++++ .../ui/openapi-gen/requests/schemas.gen.ts | 24 +++++++++ .../ui/openapi-gen/requests/services.gen.ts | 16 ++++++ airflow/ui/openapi-gen/requests/types.gen.ts | 20 ++++++++ .../core_api/routes/public/test_version.py | 51 +++++++++++++++++++ 12 files changed, 268 insertions(+) create mode 100644 airflow/api_fastapi/core_api/routes/public/version.py create mode 100644 airflow/api_fastapi/core_api/serializers/version.py create mode 100644 tests/api_fastapi/core_api/routes/public/test_version.py diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 5eb7aed770532..3a3afbab95dd9 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1358,6 +1358,20 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /public/version/: + get: + tags: + - Version + summary: Get Version + description: Get version information. + operationId: get_version + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VersionInfo' components: schemas: AppBuilderMenuItemResponse: @@ -2574,3 +2588,19 @@ components: - value title: VariableResponse description: Variable serializer for responses. + VersionInfo: + properties: + version: + type: string + title: Version + git_version: + anyOf: + - type: string + - type: 'null' + title: Git Version + type: object + required: + - version + - git_version + title: VersionInfo + description: Version information serializer for responses. diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py b/airflow/api_fastapi/core_api/routes/public/__init__.py index 89d216e438f84..ab307409add0a 100644 --- a/airflow/api_fastapi/core_api/routes/public/__init__.py +++ b/airflow/api_fastapi/core_api/routes/public/__init__.py @@ -26,6 +26,7 @@ from airflow.api_fastapi.core_api.routes.public.pools import pools_router from airflow.api_fastapi.core_api.routes.public.providers import providers_router from airflow.api_fastapi.core_api.routes.public.variables import variables_router +from airflow.api_fastapi.core_api.routes.public.version import version_router public_router = AirflowRouter(prefix="/public") @@ -38,3 +39,4 @@ public_router.include_router(pools_router) public_router.include_router(providers_router) public_router.include_router(plugins_router) +public_router.include_router(version_router) diff --git a/airflow/api_fastapi/core_api/routes/public/version.py b/airflow/api_fastapi/core_api/routes/public/version.py new file mode 100644 index 0000000000000..218e0b90702dd --- /dev/null +++ b/airflow/api_fastapi/core_api/routes/public/version.py @@ -0,0 +1,34 @@ +# 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 + +import airflow +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.serializers.version import VersionInfo +from airflow.utils.platform import get_airflow_git_version + +version_router = AirflowRouter(tags=["Version"], prefix="/version") + + +@version_router.get("/") +async def get_version() -> VersionInfo: + """Get version information.""" + airflow_version = airflow.__version__ + git_version = get_airflow_git_version() + version_info = VersionInfo(version=airflow_version, git_version=git_version) + return VersionInfo.model_validate(version_info) diff --git a/airflow/api_fastapi/core_api/serializers/version.py b/airflow/api_fastapi/core_api/serializers/version.py new file mode 100644 index 0000000000000..01c4c45376f70 --- /dev/null +++ b/airflow/api_fastapi/core_api/serializers/version.py @@ -0,0 +1,26 @@ +# 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 VersionInfo(BaseModel): + """Version information serializer for responses.""" + + version: str + git_version: str | None diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index c621a3fdb54a7..b5e730822ffdd 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -12,6 +12,7 @@ import { PoolService, ProviderService, VariableService, + VersionService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; @@ -345,6 +346,18 @@ export const UsePluginServiceGetPluginsKeyFn = ( } = {}, queryKey?: Array, ) => [usePluginServiceGetPluginsKey, ...(queryKey ?? [{ limit, offset }])]; +export type VersionServiceGetVersionDefaultResponse = Awaited< + ReturnType +>; +export type VersionServiceGetVersionQueryResult< + TData = VersionServiceGetVersionDefaultResponse, + TError = unknown, +> = UseQueryResult; +export const useVersionServiceGetVersionKey = "VersionServiceGetVersion"; +export const UseVersionServiceGetVersionKeyFn = (queryKey?: Array) => [ + useVersionServiceGetVersionKey, + ...(queryKey ?? []), +]; export type VariableServicePostVariableMutationResult = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index 36a6c251cb944..3f681a4a13b60 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -12,6 +12,7 @@ import { PoolService, ProviderService, VariableService, + VersionService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; import * as Common from "./common"; @@ -429,3 +430,14 @@ export const prefetchUsePluginServiceGetPlugins = ( queryKey: Common.UsePluginServiceGetPluginsKeyFn({ limit, offset }), queryFn: () => PluginService.getPlugins({ limit, offset }), }); +/** + * Get Version + * Get version information. + * @returns VersionInfo Successful Response + * @throws ApiError + */ +export const prefetchUseVersionServiceGetVersion = (queryClient: QueryClient) => + queryClient.prefetchQuery({ + queryKey: Common.UseVersionServiceGetVersionKeyFn(), + queryFn: () => VersionService.getVersion(), + }); diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index 7141ac00011de..31d9e94d6172f 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -17,6 +17,7 @@ import { PoolService, ProviderService, VariableService, + VersionService, } from "../requests/services.gen"; import { DAGPatchBody, @@ -562,6 +563,25 @@ export const usePluginServiceGetPlugins = < queryFn: () => PluginService.getPlugins({ limit, offset }) as TData, ...options, }); +/** + * Get Version + * Get version information. + * @returns VersionInfo Successful Response + * @throws ApiError + */ +export const useVersionServiceGetVersion = < + TData = Common.VersionServiceGetVersionDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useQuery({ + queryKey: Common.UseVersionServiceGetVersionKeyFn(queryKey), + queryFn: () => VersionService.getVersion() as TData, + ...options, + }); /** * Post Variable * Create a variable. diff --git a/airflow/ui/openapi-gen/queries/suspense.ts b/airflow/ui/openapi-gen/queries/suspense.ts index 8fd858a985c4b..eb91e8f1ba936 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -12,6 +12,7 @@ import { PoolService, ProviderService, VariableService, + VersionService, } from "../requests/services.gen"; import { DagRunState } from "../requests/types.gen"; import * as Common from "./common"; @@ -552,3 +553,22 @@ export const usePluginServiceGetPluginsSuspense = < queryFn: () => PluginService.getPlugins({ limit, offset }) as TData, ...options, }); +/** + * Get Version + * Get version information. + * @returns VersionInfo Successful Response + * @throws ApiError + */ +export const useVersionServiceGetVersionSuspense = < + TData = Common.VersionServiceGetVersionDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useSuspenseQuery({ + queryKey: Common.UseVersionServiceGetVersionKeyFn(queryKey), + queryFn: () => VersionService.getVersion() as TData, + ...options, + }); diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index afd16d804091a..2f3eb390b4856 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1856,3 +1856,27 @@ export const $VariableResponse = { title: "VariableResponse", description: "Variable serializer for responses.", } as const; + +export const $VersionInfo = { + properties: { + version: { + type: "string", + title: "Version", + }, + git_version: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Git Version", + }, + }, + type: "object", + required: ["version", "git_version"], + title: "VersionInfo", + description: "Version information serializer for responses.", +} as const; diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index c4b0c987c4324..b9d9f52655100 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -54,6 +54,7 @@ import type { GetProvidersResponse, GetPluginsData, GetPluginsResponse, + GetVersionResponse, } from "./types.gen"; export class AssetService { @@ -807,3 +808,18 @@ export class PluginService { }); } } + +export class VersionService { + /** + * Get Version + * Get version information. + * @returns VersionInfo Successful Response + * @throws ApiError + */ + public static getVersion(): CancelablePromise { + return __request(OpenAPI, { + method: "GET", + url: "/public/version/", + }); + } +} diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index aef0f76f28e11..66f4437cec102 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -441,6 +441,14 @@ export type VariableResponse = { value: string | null; }; +/** + * Version information serializer for responses. + */ +export type VersionInfo = { + version: string; + git_version: string | null; +}; + export type NextRunAssetsData = { dagId: string; }; @@ -633,6 +641,8 @@ export type GetPluginsData = { export type GetPluginsResponse = PluginCollectionResponse; +export type GetVersionResponse = VersionInfo; + export type $OpenApiTs = { "/ui/next_run_assets/{dag_id}": { get: { @@ -1269,4 +1279,14 @@ export type $OpenApiTs = { }; }; }; + "/public/version/": { + get: { + res: { + /** + * Successful Response + */ + 200: VersionInfo; + }; + }; + }; }; diff --git a/tests/api_fastapi/core_api/routes/public/test_version.py b/tests/api_fastapi/core_api/routes/public/test_version.py new file mode 100644 index 0000000000000..2000b03ccbf04 --- /dev/null +++ b/tests/api_fastapi/core_api/routes/public/test_version.py @@ -0,0 +1,51 @@ +# 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 unittest import mock + +import pytest + +from tests_common.test_utils.db import clear_db_jobs + +pytestmark = [pytest.mark.db_test, pytest.mark.skip_if_database_isolation_mode] + + +class TestVersionEndpoint: + @pytest.fixture(autouse=True) + def setup(self) -> None: + clear_db_jobs() + + def teardown_method(self): + clear_db_jobs() + + +class TestGetVersion(TestVersionEndpoint): + @mock.patch( + "airflow.api_fastapi.core_api.routes.public.version.airflow.__version__", + "MOCK_VERSION", + ) + @mock.patch( + "airflow.api_fastapi.core_api.routes.public.version.get_airflow_git_version", + return_value="GIT_COMMIT", + ) + def test_airflow_version_info(self, mock_get_airflow_get_commit, client): + response = client().get("/public/version") + + assert 200 == response.status_code + assert {"git_version": "GIT_COMMIT", "version": "MOCK_VERSION"} == response.json() + mock_get_airflow_get_commit.assert_called_once_with() From 6070bb6c354908f6b233f3d92086476d0f62ada0 Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Thu, 24 Oct 2024 22:44:46 +0800 Subject: [PATCH 101/258] AIP-84 Post Pool (#43317) --- .../api_connexion/endpoints/pool_endpoint.py | 1 + .../core_api/openapi/v1-generated.yaml | 92 +++++++++++++++---- .../core_api/routes/public/pools.py | 18 +++- .../api_fastapi/core_api/serializers/pools.py | 12 ++- airflow/ui/openapi-gen/queries/common.ts | 3 + airflow/ui/openapi-gen/queries/queries.ts | 44 ++++++++- .../ui/openapi-gen/requests/schemas.gen.ts | 63 ++++++++++--- .../ui/openapi-gen/requests/services.gen.ts | 26 ++++++ airflow/ui/openapi-gen/requests/types.gen.ts | 51 ++++++++-- .../core_api/routes/public/test_pools.py | 49 ++++++++++ 10 files changed, 314 insertions(+), 45 deletions(-) diff --git a/airflow/api_connexion/endpoints/pool_endpoint.py b/airflow/api_connexion/endpoints/pool_endpoint.py index 497f31c21c22f..a6ccd3a4aa9b9 100644 --- a/airflow/api_connexion/endpoints/pool_endpoint.py +++ b/airflow/api_connexion/endpoints/pool_endpoint.py @@ -147,6 +147,7 @@ def patch_pool( return pool_schema.dump(pool) +@mark_fastapi_migration_done @security.requires_access_pool("POST") @action_logging @provide_session diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 3a3afbab95dd9..76c28214100f8 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -1191,7 +1191,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PoolBody' + $ref: '#/components/schemas/PoolPatchBody' responses: '200': description: Successful Response @@ -1289,6 +1289,43 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + post: + tags: + - Pool + summary: Post Pool + description: Create a Pool. + operationId: post_pool + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PoolPostBody' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PoolResponse' + '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/providers/: get: tags: @@ -2290,7 +2327,23 @@ components: - timetables title: PluginResponse description: Plugin serializer. - PoolBody: + PoolCollectionResponse: + properties: + pools: + items: + $ref: '#/components/schemas/PoolResponse' + type: array + title: Pools + total_entries: + type: integer + title: Total Entries + type: object + required: + - pools + - total_entries + title: PoolCollectionResponse + description: Pool Collection serializer for responses. + PoolPatchBody: properties: pool: anyOf: @@ -2313,24 +2366,31 @@ components: - type: 'null' title: Include Deferred type: object - title: PoolBody - description: Pool serializer for bodies. - PoolCollectionResponse: + title: PoolPatchBody + description: Pool serializer for patch bodies. + PoolPostBody: properties: - pools: - items: - $ref: '#/components/schemas/PoolResponse' - type: array - title: Pools - total_entries: + name: + type: string + title: Name + slots: type: integer - title: Total Entries + title: Slots + description: + anyOf: + - type: string + - type: 'null' + title: Description + include_deferred: + type: boolean + title: Include Deferred + default: false type: object required: - - pools - - total_entries - title: PoolCollectionResponse - description: Pool Collection serializer for responses. + - name + - slots + title: PoolPostBody + description: Pool serializer for post bodies. PoolResponse: properties: name: diff --git a/airflow/api_fastapi/core_api/routes/public/pools.py b/airflow/api_fastapi/core_api/routes/public/pools.py index c9e30a7e2504b..5690196e850a5 100644 --- a/airflow/api_fastapi/core_api/routes/public/pools.py +++ b/airflow/api_fastapi/core_api/routes/public/pools.py @@ -29,8 +29,9 @@ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.api_fastapi.core_api.serializers.pools import ( BasePool, - PoolBody, PoolCollectionResponse, + PoolPatchBody, + PoolPostBody, PoolResponse, ) from airflow.models.pool import Pool @@ -107,7 +108,7 @@ async def get_pools( @pools_router.patch("/{pool_name}", responses=create_openapi_http_exception_doc([400, 401, 403, 404])) async def patch_pool( pool_name: str, - patch_body: PoolBody, + patch_body: PoolPatchBody, session: Annotated[Session, Depends(get_session)], update_mask: list[str] | None = Query(None), ) -> PoolResponse: @@ -136,3 +137,16 @@ async def patch_pool( setattr(pool, key, value) return PoolResponse.model_validate(pool, from_attributes=True) + + +@pools_router.post("/", status_code=201, responses=create_openapi_http_exception_doc([401, 403])) +async def post_pool( + post_body: PoolPostBody, + session: Annotated[Session, Depends(get_session)], +) -> PoolResponse: + """Create a Pool.""" + pool = Pool(**post_body.model_dump()) + + session.add(pool) + + return PoolResponse.model_validate(pool, from_attributes=True) diff --git a/airflow/api_fastapi/core_api/serializers/pools.py b/airflow/api_fastapi/core_api/serializers/pools.py index dd1d6df884cc8..ef3676a8afec7 100644 --- a/airflow/api_fastapi/core_api/serializers/pools.py +++ b/airflow/api_fastapi/core_api/serializers/pools.py @@ -58,8 +58,8 @@ class PoolCollectionResponse(BaseModel): total_entries: int -class PoolBody(BaseModel): - """Pool serializer for bodies.""" +class PoolPatchBody(BaseModel): + """Pool serializer for patch bodies.""" model_config = ConfigDict(populate_by_name=True) @@ -67,3 +67,11 @@ class PoolBody(BaseModel): slots: int | None = None description: str | None = None include_deferred: bool | None = None + + +class PoolPostBody(BasePool): + """Pool serializer for post bodies.""" + + pool: str = Field(alias="name") + description: str | None = None + include_deferred: bool = False diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index b5e730822ffdd..b12c133b6c2ea 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -361,6 +361,9 @@ export const UseVersionServiceGetVersionKeyFn = (queryKey?: Array) => [ export type VariableServicePostVariableMutationResult = Awaited< ReturnType >; +export type PoolServicePostPoolMutationResult = Awaited< + ReturnType +>; export type DagServicePatchDagsMutationResult = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index 31d9e94d6172f..e3942ad84e086 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -22,7 +22,8 @@ import { import { DAGPatchBody, DagRunState, - PoolBody, + PoolPatchBody, + PoolPostBody, VariableBody, } from "../requests/types.gen"; import * as Common from "./common"; @@ -621,6 +622,43 @@ export const useVariableServicePostVariable = < }) as unknown as Promise, ...options, }); +/** + * Post Pool + * Create a Pool. + * @param data The data for the request. + * @param data.requestBody + * @returns PoolResponse Successful Response + * @throws ApiError + */ +export const usePoolServicePostPool = < + TData = Common.PoolServicePostPoolMutationResult, + TError = unknown, + TContext = unknown, +>( + options?: Omit< + UseMutationOptions< + TData, + TError, + { + requestBody: PoolPostBody; + }, + TContext + >, + "mutationFn" + >, +) => + useMutation< + TData, + TError, + { + requestBody: PoolPostBody; + }, + TContext + >({ + mutationFn: ({ requestBody }) => + PoolService.postPool({ requestBody }) as unknown as Promise, + ...options, + }); /** * Patch Dags * Patch multiple DAGs. @@ -822,7 +860,7 @@ export const usePoolServicePatchPool = < TError, { poolName: string; - requestBody: PoolBody; + requestBody: PoolPatchBody; updateMask?: string[]; }, TContext @@ -835,7 +873,7 @@ export const usePoolServicePatchPool = < TError, { poolName: string; - requestBody: PoolBody; + requestBody: PoolPatchBody; updateMask?: string[]; }, TContext diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index 2f3eb390b4856..b940646a69a77 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1420,7 +1420,27 @@ export const $PluginResponse = { description: "Plugin serializer.", } as const; -export const $PoolBody = { +export const $PoolCollectionResponse = { + properties: { + pools: { + items: { + $ref: "#/components/schemas/PoolResponse", + }, + type: "array", + title: "Pools", + }, + total_entries: { + type: "integer", + title: "Total Entries", + }, + }, + type: "object", + required: ["pools", "total_entries"], + title: "PoolCollectionResponse", + description: "Pool Collection serializer for responses.", +} as const; + +export const $PoolPatchBody = { properties: { pool: { anyOf: [ @@ -1468,28 +1488,41 @@ export const $PoolBody = { }, }, type: "object", - title: "PoolBody", - description: "Pool serializer for bodies.", + title: "PoolPatchBody", + description: "Pool serializer for patch bodies.", } as const; -export const $PoolCollectionResponse = { +export const $PoolPostBody = { properties: { - pools: { - items: { - $ref: "#/components/schemas/PoolResponse", - }, - type: "array", - title: "Pools", + name: { + type: "string", + title: "Name", }, - total_entries: { + slots: { type: "integer", - title: "Total Entries", + title: "Slots", + }, + description: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Description", + }, + include_deferred: { + type: "boolean", + title: "Include Deferred", + default: false, }, }, type: "object", - required: ["pools", "total_entries"], - title: "PoolCollectionResponse", - description: "Pool Collection serializer for responses.", + required: ["name", "slots"], + title: "PoolPostBody", + description: "Pool serializer for post bodies.", } as const; export const $PoolResponse = { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index b9d9f52655100..bfe1d2e39d361 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -50,6 +50,8 @@ import type { PatchPoolResponse, GetPoolsData, GetPoolsResponse, + PostPoolData, + PostPoolResponse, GetProvidersData, GetProvidersResponse, GetPluginsData, @@ -754,6 +756,30 @@ export class PoolService { }, }); } + + /** + * Post Pool + * Create a Pool. + * @param data The data for the request. + * @param data.requestBody + * @returns PoolResponse Successful Response + * @throws ApiError + */ + public static postPool( + data: PostPoolData, + ): CancelablePromise { + return __request(OpenAPI, { + method: "POST", + url: "/public/pools/", + body: data.requestBody, + mediaType: "application/json", + errors: { + 401: "Unauthorized", + 403: "Forbidden", + 422: "Validation Error", + }, + }); + } } export class ProviderService { diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 66f4437cec102..1bca12c04e777 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -324,9 +324,17 @@ export type PluginResponse = { }; /** - * Pool serializer for bodies. + * Pool Collection serializer for responses. + */ +export type PoolCollectionResponse = { + pools: Array; + total_entries: number; +}; + +/** + * Pool serializer for patch bodies. */ -export type PoolBody = { +export type PoolPatchBody = { pool?: string | null; slots?: number | null; description?: string | null; @@ -334,11 +342,13 @@ export type PoolBody = { }; /** - * Pool Collection serializer for responses. + * Pool serializer for post bodies. */ -export type PoolCollectionResponse = { - pools: Array; - total_entries: number; +export type PoolPostBody = { + name: string; + slots: number; + description?: string | null; + include_deferred?: boolean; }; /** @@ -613,7 +623,7 @@ export type GetPoolResponse = PoolResponse; export type PatchPoolData = { poolName: string; - requestBody: PoolBody; + requestBody: PoolPatchBody; updateMask?: Array | null; }; @@ -627,6 +637,12 @@ export type GetPoolsData = { export type GetPoolsResponse = PoolCollectionResponse; +export type PostPoolData = { + requestBody: PoolPostBody; +}; + +export type PostPoolResponse = PoolResponse; + export type GetProvidersData = { limit?: number; offset?: number; @@ -1248,6 +1264,27 @@ export type $OpenApiTs = { 422: HTTPValidationError; }; }; + post: { + req: PostPoolData; + res: { + /** + * Successful Response + */ + 201: PoolResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; }; "/public/providers/": { get: { diff --git a/tests/api_fastapi/core_api/routes/public/test_pools.py b/tests/api_fastapi/core_api/routes/public/test_pools.py index e2a61fedd2026..da9ac0c046e89 100644 --- a/tests/api_fastapi/core_api/routes/public/test_pools.py +++ b/tests/api_fastapi/core_api/routes/public/test_pools.py @@ -281,3 +281,52 @@ def test_should_respond_200( del error["url"] assert body == expected_response + + +class TestPostPool(TestPoolsEndpoint): + @pytest.mark.parametrize( + "body, expected_status_code, expected_response", + [ + ( + {"name": "my_pool", "slots": 11}, + 201, + { + "name": "my_pool", + "slots": 11, + "description": None, + "include_deferred": False, + "occupied_slots": 0, + "running_slots": 0, + "queued_slots": 0, + "scheduled_slots": 0, + "open_slots": 11, + "deferred_slots": 0, + }, + ), + ( + {"name": "my_pool", "slots": 11, "include_deferred": True, "description": "Some description"}, + 201, + { + "name": "my_pool", + "slots": 11, + "description": "Some description", + "include_deferred": True, + "occupied_slots": 0, + "running_slots": 0, + "queued_slots": 0, + "scheduled_slots": 0, + "open_slots": 11, + "deferred_slots": 0, + }, + ), + ], + ) + def test_should_respond_200(self, test_client, session, body, expected_status_code, expected_response): + self.create_pools() + n_pools = session.query(Pool).count() + response = test_client.post("/public/pools/", json=body) + assert response.status_code == expected_status_code + + body = response.json() + assert response.json() == expected_response + assert session.query(Pool).count() == n_pools + 1 From 25ec15c0722991010ea467654f99b03ed78f201a Mon Sep 17 00:00:00 2001 From: Hung Date: Fri, 25 Oct 2024 01:00:18 +0700 Subject: [PATCH 102/258] Add Cyberdino to the list of companies using Apache Airflow (#43345) * Add Cyberdino to the list of companies using Apache Airflow * Sort INTOTHEWILD after adding Cyberdino --- INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/INTHEWILD.md b/INTHEWILD.md index 310d018b98329..bf17b90d7cda8 100644 --- a/INTHEWILD.md +++ b/INTHEWILD.md @@ -158,6 +158,7 @@ Currently, **officially** using Airflow: 1. [Cryptalizer.com](https://www.cryptalizer.com/) 1. [Currency](https://www.gocurrency.com/) [[@FCLI](https://github.com/FCLI) & [@alexbegg](https://github.com/alexbegg)] 1. [Custom Ink](https://www.customink.com/) [[@david-dalisay](https://github.com/david-dalisay), [@dmartin11](https://github.com/dmartin11) & [@mpeteuil](https://github.com/mpeteuil)] +1. [Cyberdino](https://www.cyberdino.io) [[@cyberdino-io](https://github.com/cyberdino-io)] 1. [Cyscale](https://cyscale.com) [[@ocical](https://github.com/ocical)] 1. [Dailymotion](http://www.dailymotion.com/fr) [[@germaintanguy](https://github.com/germaintanguy) & [@hc](https://github.com/hc)] 1. [DANA](https://www.dana.id/) [[@imamdigmi](https://github.com/imamdigmi)] From 64336c29b1c758772ad9819e4586d0b689725a6b Mon Sep 17 00:00:00 2001 From: ambikagarg <70703123+ambika-garg@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:10:45 -0400 Subject: [PATCH 103/258] Add documentation for the PowerBIDatasetRefresh Operator. (#42754) * Add documentation for PowerBI Dataset Refresh Operator * Add the link to powerbi documentation in operator class * Refactor rst files * Add how-to-guide in provider.yaml * add files created in create-missing-init-py-files-tests hook * Fix to pass breeze unit tests * Fix to pass breeze unit tests * Fix operator documentation path * remove pre-commit generated files --- .../connections/powerbi.rst | 60 +++++++++++++++++++ .../operators/powerbi.rst | 50 ++++++++++++++++ .../microsoft/azure/operators/powerbi.py | 4 ++ .../providers/microsoft/azure/provider.yaml | 2 + 4 files changed, 116 insertions(+) create mode 100644 docs/apache-airflow-providers-microsoft-azure/connections/powerbi.rst create mode 100644 docs/apache-airflow-providers-microsoft-azure/operators/powerbi.rst diff --git a/docs/apache-airflow-providers-microsoft-azure/connections/powerbi.rst b/docs/apache-airflow-providers-microsoft-azure/connections/powerbi.rst new file mode 100644 index 0000000000000..587b78650d5c8 --- /dev/null +++ b/docs/apache-airflow-providers-microsoft-azure/connections/powerbi.rst @@ -0,0 +1,60 @@ +.. 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. + + + +.. _howto/connection:powerbi: + +Microsoft Power BI Connection +============================= + +The Microsoft Power BI connection type enables the Power BI Integrations. + +The :class:`~airflow.providers.microsoft.azure.hooks.powerbi.PowerBIHook` and :class:`~airflow.providers.microsoft.azure.operators.powerbi.PowerBIDatasetOperator` requires a connection of type ``powerbi`` to authenticate with the Power BI. + +Authenticating to Power BI +------------------------------- + +1. Use `token credentials + `_ + i.e. add specific credentials (client_id, client_secret, tenant_id) to the Airflow connection. + +Default Connection IDs +---------------------- + +All hooks and operators related to Microsoft Power BI use ``powerbi_default`` by default. + +Configuring the Connection +-------------------------- + +Client ID + Specify the ``client_id`` used for the initial connection. + This is needed for *token credentials* authentication mechanism. + + +Client Secret + Specify the ``client_secret`` used for the initial connection. + This is needed for *token credentials* authentication mechanism. + + +Tenant ID + Specify the ``tenant_id`` used for the initial connection. + This is needed for *token credentials* authentication mechanism. + +.. spelling:word-list:: + + Entra diff --git a/docs/apache-airflow-providers-microsoft-azure/operators/powerbi.rst b/docs/apache-airflow-providers-microsoft-azure/operators/powerbi.rst new file mode 100644 index 0000000000000..f5b6d4e94ed91 --- /dev/null +++ b/docs/apache-airflow-providers-microsoft-azure/operators/powerbi.rst @@ -0,0 +1,50 @@ + .. 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. + + +Microsoft Power BI Operators +============================= +`Microsoft Power BI `__, is a unified, scalable platform for self-service and enterprise business intelligence(BI). + + +Prerequisite Tasks +^^^^^^^^^^^^^^^^^^ + +.. include:: /operators/_partials/prerequisite_tasks.rst + +.. _howto/operator:PowerBIDatasetRefreshOperator: + +PowerBIDatasetRefreshOperator +---------------------------------- + +To trigger a refresh for the specified dataset from the specified workspace, use the :class:`~airflow.providers.microsoft.azure.operators.powerbi.PowerBIDatasetRefreshOperator`. + + +.. exampleinclude:: /../../providers/tests/system/microsoft/azure/example_powerbi_dataset_refresh.py + :language: python + :dedent: 0 + :start-after: [START howto_operator_powerbi_refresh_async] + :end-before: [END howto_operator_powerbi_refresh_async] + +Reference +--------- + +For further information, look at: + +* `Use the Microsoft Graph API `__ +* `Using the Power BI REST APIs `__ +* `Using the Fabric REST APIs `__ diff --git a/providers/src/airflow/providers/microsoft/azure/operators/powerbi.py b/providers/src/airflow/providers/microsoft/azure/operators/powerbi.py index fc812e852d90b..b80966f541fc6 100644 --- a/providers/src/airflow/providers/microsoft/azure/operators/powerbi.py +++ b/providers/src/airflow/providers/microsoft/azure/operators/powerbi.py @@ -52,6 +52,10 @@ class PowerBIDatasetRefreshOperator(BaseOperator): """ Refreshes a Power BI dataset. + .. seealso:: + For more information on how to use this operator, take a look at the guide: + :ref:`howto/operator:PowerBIDatasetRefreshOperator` + :param dataset_id: The dataset id. :param group_id: The workspace id. :param conn_id: Airflow Connection ID that contains the connection information for the Power BI account used for authentication. diff --git a/providers/src/airflow/providers/microsoft/azure/provider.yaml b/providers/src/airflow/providers/microsoft/azure/provider.yaml index b1839b10fd0ce..dde01c6f27875 100644 --- a/providers/src/airflow/providers/microsoft/azure/provider.yaml +++ b/providers/src/airflow/providers/microsoft/azure/provider.yaml @@ -190,6 +190,8 @@ integrations: tags: [azure] - integration-name: Microsoft Power BI external-doc-url: https://learn.microsoft.com/en-us/rest/api/power-bi/ + how-to-guide: + - /docs/apache-airflow-providers-microsoft-azure/operators/powerbi.rst tags: [azure] operators: From 7a158493941ba80d9a5795c0378d41e1b257eb6f Mon Sep 17 00:00:00 2001 From: olegkachur-e Date: Thu, 24 Oct 2024 21:18:57 +0200 Subject: [PATCH 104/258] Update system test apache-beam[gcp] sdk to the latest supported version (#43256) Latest gcp supported version and known issues doc: https://cloud.google.com/dataflow/docs/support/sdk-version-support-status. Co-authored-by: Oleg Kachur --- providers/tests/system/apache/beam/example_python.py | 10 +++++----- .../tests/system/apache/beam/example_python_async.py | 10 +++++----- .../system/apache/beam/example_python_dataflow.py | 2 +- .../cloud/dataflow/example_dataflow_native_python.py | 4 ++-- .../dataflow/example_dataflow_native_python_async.py | 2 +- .../dataflow/example_dataflow_sensors_deferrable.py | 2 +- .../dataflow/example_dataflow_streaming_python.py | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/providers/tests/system/apache/beam/example_python.py b/providers/tests/system/apache/beam/example_python.py index 4b4b9c2c05b36..4632fde21406d 100644 --- a/providers/tests/system/apache/beam/example_python.py +++ b/providers/tests/system/apache/beam/example_python.py @@ -48,7 +48,7 @@ task_id="start_python_pipeline_local_direct_runner", py_file="apache_beam.examples.wordcount", py_options=["-m"], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, ) @@ -60,7 +60,7 @@ py_file=GCS_PYTHON, py_options=[], pipeline_options={"output": GCS_OUTPUT}, - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, ) @@ -77,7 +77,7 @@ "output": GCS_OUTPUT, }, py_options=[], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config=DataflowConfiguration( @@ -91,7 +91,7 @@ py_file="apache_beam.examples.wordcount", runner="SparkRunner", py_options=["-m"], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, ) @@ -104,7 +104,7 @@ pipeline_options={ "output": "/tmp/start_python_pipeline_local_flink_runner", }, - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, ) diff --git a/providers/tests/system/apache/beam/example_python_async.py b/providers/tests/system/apache/beam/example_python_async.py index 0419a13cced23..a1b5e9d58b3dc 100644 --- a/providers/tests/system/apache/beam/example_python_async.py +++ b/providers/tests/system/apache/beam/example_python_async.py @@ -48,7 +48,7 @@ task_id="start_python_pipeline_local_direct_runner", py_file="apache_beam.examples.wordcount", py_options=["-m"], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, deferrable=True, @@ -61,7 +61,7 @@ py_file=GCS_PYTHON, py_options=[], pipeline_options={"output": GCS_OUTPUT}, - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, deferrable=True, @@ -79,7 +79,7 @@ "output": GCS_OUTPUT, }, py_options=[], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config=DataflowConfiguration( @@ -95,7 +95,7 @@ py_file="apache_beam.examples.wordcount", runner="SparkRunner", py_options=["-m"], - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, deferrable=True, @@ -111,7 +111,7 @@ pipeline_options={ "output": "/tmp/start_python_pipeline_local_flink_runner", }, - py_requirements=["apache-beam[gcp]==2.46.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, deferrable=True, diff --git a/providers/tests/system/apache/beam/example_python_dataflow.py b/providers/tests/system/apache/beam/example_python_dataflow.py index 8d1295ed29a0e..696800dac0793 100644 --- a/providers/tests/system/apache/beam/example_python_dataflow.py +++ b/providers/tests/system/apache/beam/example_python_dataflow.py @@ -56,7 +56,7 @@ "output": GCS_OUTPUT, }, py_options=[], - py_requirements=["apache-beam[gcp]==2.26.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config=DataflowConfiguration( diff --git a/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python.py b/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python.py index 70cd5db9e9dd4..4d9e92b2217a7 100644 --- a/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python.py +++ b/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python.py @@ -68,7 +68,7 @@ pipeline_options={ "output": GCS_OUTPUT, }, - py_requirements=["apache-beam[gcp]==2.47.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config={"location": LOCATION, "job_name": "start_python_job"}, @@ -82,7 +82,7 @@ pipeline_options={ "output": GCS_OUTPUT, }, - py_requirements=["apache-beam[gcp]==2.47.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, ) diff --git a/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python_async.py b/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python_async.py index 9bca9b02871b9..b8016a96bf4b1 100644 --- a/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python_async.py +++ b/providers/tests/system/google/cloud/dataflow/example_dataflow_native_python_async.py @@ -78,7 +78,7 @@ pipeline_options={ "output": GCS_OUTPUT, }, - py_requirements=["apache-beam[gcp]==2.47.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config={ diff --git a/providers/tests/system/google/cloud/dataflow/example_dataflow_sensors_deferrable.py b/providers/tests/system/google/cloud/dataflow/example_dataflow_sensors_deferrable.py index c6303d74231be..974ca6835c679 100644 --- a/providers/tests/system/google/cloud/dataflow/example_dataflow_sensors_deferrable.py +++ b/providers/tests/system/google/cloud/dataflow/example_dataflow_sensors_deferrable.py @@ -76,7 +76,7 @@ pipeline_options={ "output": GCS_OUTPUT, }, - py_requirements=["apache-beam[gcp]==2.47.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config={ diff --git a/providers/tests/system/google/cloud/dataflow/example_dataflow_streaming_python.py b/providers/tests/system/google/cloud/dataflow/example_dataflow_streaming_python.py index 4e7a7ccebdd1b..e87ee258bb881 100644 --- a/providers/tests/system/google/cloud/dataflow/example_dataflow_streaming_python.py +++ b/providers/tests/system/google/cloud/dataflow/example_dataflow_streaming_python.py @@ -82,7 +82,7 @@ "output_topic": f"projects/{PROJECT_ID}/topics/{TOPIC_ID}", "streaming": True, }, - py_requirements=["apache-beam[gcp]==2.47.0"], + py_requirements=["apache-beam[gcp]==2.59.0"], py_interpreter="python3", py_system_site_packages=False, dataflow_config={"location": LOCATION, "job_name": "start_python_job_streaming"}, From 98fcc994fbec30611bb307095d0d6aa2354d7d06 Mon Sep 17 00:00:00 2001 From: yangyulely Date: Fri, 25 Oct 2024 04:13:58 +0800 Subject: [PATCH 105/258] Move scheduler health unit test to utils folder (#43294) --- tests/cli/commands/test_scheduler_command.py | 52 +------------ tests/utils/test_scheduler_health.py | 79 ++++++++++++++++++++ 2 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 tests/utils/test_scheduler_health.py diff --git a/tests/cli/commands/test_scheduler_command.py b/tests/cli/commands/test_scheduler_command.py index 4fc795add90c8..d7e141a26018d 100644 --- a/tests/cli/commands/test_scheduler_command.py +++ b/tests/cli/commands/test_scheduler_command.py @@ -17,17 +17,15 @@ # under the License. from __future__ import annotations -from http.server import BaseHTTPRequestHandler from importlib import reload from unittest import mock -from unittest.mock import MagicMock import pytest from airflow.cli import cli_parser from airflow.cli.commands import scheduler_command from airflow.executors import executor_loader -from airflow.utils.scheduler_health import HealthServer, serve_health_check +from airflow.utils.scheduler_health import serve_health_check from airflow.utils.serve_logs import serve_logs from tests_common.test_utils.config import conf_vars @@ -224,51 +222,3 @@ def test_run_job_exception_handling( ) mock_process.assert_called_once_with(target=serve_logs) mock_process().terminate.assert_called_once_with() - - -# Creating MockServer subclass of the HealthServer handler so that we can test the do_GET logic -class MockServer(HealthServer): - def __init__(self): - # Overriding so we don't need to initialize with BaseHTTPRequestHandler.__init__ params - pass - - def do_GET(self, path): - self.path = path - super().do_GET() - - -class TestSchedulerHealthServer: - def setup_method(self) -> None: - self.mock_server = MockServer() - - @mock.patch.object(BaseHTTPRequestHandler, "send_error") - def test_incorrect_endpoint(self, mock_send_error): - self.mock_server.do_GET("/incorrect") - mock_send_error.assert_called_with(404) - - @mock.patch.object(BaseHTTPRequestHandler, "end_headers") - @mock.patch.object(BaseHTTPRequestHandler, "send_response") - @mock.patch("airflow.utils.scheduler_health.create_session") - def test_healthy_scheduler(self, mock_session, mock_send_response, mock_end_headers): - mock_scheduler_job = MagicMock() - mock_scheduler_job.is_alive.return_value = True - mock_session.return_value.__enter__.return_value.query.return_value = mock_scheduler_job - self.mock_server.do_GET("/health") - mock_send_response.assert_called_once_with(200) - mock_end_headers.assert_called_once() - - @mock.patch.object(BaseHTTPRequestHandler, "send_error") - @mock.patch("airflow.utils.scheduler_health.create_session") - def test_unhealthy_scheduler(self, mock_session, mock_send_error): - mock_scheduler_job = MagicMock() - mock_scheduler_job.is_alive.return_value = False - mock_session.return_value.__enter__.return_value.query.return_value = mock_scheduler_job - self.mock_server.do_GET("/health") - mock_send_error.assert_called_with(503) - - @mock.patch.object(BaseHTTPRequestHandler, "send_error") - @mock.patch("airflow.utils.scheduler_health.create_session") - def test_missing_scheduler(self, mock_session, mock_send_error): - mock_session.return_value.__enter__.return_value.query.return_value = None - self.mock_server.do_GET("/health") - mock_send_error.assert_called_with(503) diff --git a/tests/utils/test_scheduler_health.py b/tests/utils/test_scheduler_health.py new file mode 100644 index 0000000000000..4bc74da3e468e --- /dev/null +++ b/tests/utils/test_scheduler_health.py @@ -0,0 +1,79 @@ +# +# 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 http.server import BaseHTTPRequestHandler +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from airflow.utils.scheduler_health import HealthServer + +pytestmark = pytest.mark.db_test + + +class MockServer(HealthServer): + def __init__(self): + # Overriding so we don't need to initialize with BaseHTTPRequestHandler.__init__ params + pass + + def do_GET(self, path): + self.path = path + super().do_GET() + + +class TestSchedulerHealthServer: + def setup_method(self) -> None: + self.mock_server = MockServer() + + # This test is to ensure that the server responds correctly to a GET request on the correct endpoint. + @mock.patch.object(BaseHTTPRequestHandler, "send_error") + def test_incorrect_endpoint(self, mock_send_error): + self.mock_server.do_GET("/incorrect") + mock_send_error.assert_called_with(404) + + # This test is to ensure that if the scheduler is healthy, it returns 200 status code. + @mock.patch.object(BaseHTTPRequestHandler, "end_headers") + @mock.patch.object(BaseHTTPRequestHandler, "send_response") + @mock.patch("airflow.utils.scheduler_health.create_session") + def test_healthy_scheduler(self, mock_session, mock_send_response, mock_end_headers): + mock_scheduler_job = MagicMock() + mock_scheduler_job.is_alive.return_value = True + mock_session.return_value.__enter__.return_value.query.return_value = mock_scheduler_job + self.mock_server.do_GET("/health") + mock_send_response.assert_called_once_with(200) + mock_end_headers.assert_called_once() + + # This test is to ensure that if the scheduler is unhealthy, it returns 503 error code. + @mock.patch.object(BaseHTTPRequestHandler, "send_error") + @mock.patch("airflow.utils.scheduler_health.create_session") + def test_unhealthy_scheduler(self, mock_session, mock_send_error): + mock_scheduler_job = MagicMock() + mock_scheduler_job.is_alive.return_value = False + mock_session.return_value.__enter__.return_value.query.return_value = mock_scheduler_job + self.mock_server.do_GET("/health") + mock_send_error.assert_called_with(503) + + # This test is to ensure that if there's no scheduler job running, it returns 503 error code. + @mock.patch.object(BaseHTTPRequestHandler, "send_error") + @mock.patch("airflow.utils.scheduler_health.create_session") + def test_missing_scheduler(self, mock_session, mock_send_error): + mock_session.return_value.__enter__.return_value.query.return_value = None + self.mock_server.do_GET("/health") + mock_send_error.assert_called_with(503) From 75ecaffa5fa6884729243083a487f33187ed3c66 Mon Sep 17 00:00:00 2001 From: Bowrna Date: Fri, 25 Oct 2024 01:52:23 +0530 Subject: [PATCH 106/258] adding support for snippet type in slack api (#43305) * adding support for snippet type in slack api * static fix --- .../src/airflow/providers/slack/hooks/slack.py | 8 +++++--- .../airflow/providers/slack/operators/slack.py | 5 +++++ providers/tests/slack/hooks/test_slack.py | 14 ++++++++++---- providers/tests/slack/operators/test_slack.py | 17 +++++++++++++++-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/providers/src/airflow/providers/slack/hooks/slack.py b/providers/src/airflow/providers/slack/hooks/slack.py index f0193a437e30d..3c696230131c1 100644 --- a/providers/src/airflow/providers/slack/hooks/slack.py +++ b/providers/src/airflow/providers/slack/hooks/slack.py @@ -203,6 +203,7 @@ def send_file( filetype: str | None = None, initial_comment: str | None = None, title: str | None = None, + **kwargs, ) -> SlackResponse: """ Create or upload an existing file. @@ -295,7 +296,8 @@ def send_file_v1_to_v2( filename: str | None = None, initial_comment: str | None = None, title: str | None = None, - filetype: str | None = None, + snippet_type: str | None = None, + **kwargs, ) -> list[SlackResponse]: """ Smooth transition between ``send_file`` and ``send_file_v2`` methods. @@ -308,7 +310,7 @@ def send_file_v1_to_v2( :param filename: Displayed filename. :param initial_comment: The message text introducing the file in specified ``channels``. :param title: Title of the file. - :param filetype: A file type identifier. + :param snippet_type: Syntax type for the content being uploaded. """ if not exactly_one(file, content): raise ValueError("Either `file` or `content` must be provided, not both.") @@ -318,7 +320,7 @@ def send_file_v1_to_v2( else: file_uploads = {"content": content, "filename": filename} - file_uploads.update({"title": title, "snippet_type": filetype}) + file_uploads.update({"title": title, "snippet_type": snippet_type}) if channels: if isinstance(channels, str): diff --git a/providers/src/airflow/providers/slack/operators/slack.py b/providers/src/airflow/providers/slack/operators/slack.py index 64be693af67e4..88644726e27a6 100644 --- a/providers/src/airflow/providers/slack/operators/slack.py +++ b/providers/src/airflow/providers/slack/operators/slack.py @@ -209,6 +209,7 @@ class SlackAPIFileOperator(SlackAPIOperator): :param filetype: slack filetype. (templated) See: https://api.slack.com/types/file#file_types :param content: file content. (templated) :param title: title of file. (templated) + :param snippet_type: Syntax type for the snippet being uploaded.(templated) :param method_version: The version of the method of Slack SDK Client to be used, either "v1" or "v2". """ @@ -219,6 +220,7 @@ class SlackAPIFileOperator(SlackAPIOperator): "filetype", "content", "title", + "snippet_type", ) ui_color = "#44BEDF" @@ -232,6 +234,7 @@ def __init__( title: str | None = None, method_version: Literal["v1", "v2"] = "v2", channel: str | Sequence[str] | None | ArgNotSet = NOTSET, + snippet_type: str | None = None, **kwargs, ) -> None: if channel is not NOTSET: @@ -253,6 +256,7 @@ def __init__( self.content = content self.title = title self.method_version = method_version + self.snippet_type = snippet_type @property def _method_resolver(self): @@ -269,4 +273,5 @@ def execute(self, context: Context): content=self.content, initial_comment=self.initial_comment, title=self.title, + snippet_type=self.snippet_type, ) diff --git a/providers/tests/slack/hooks/test_slack.py b/providers/tests/slack/hooks/test_slack.py index 35dfa5200b7fa..10e5a094e9d8f 100644 --- a/providers/tests/slack/hooks/test_slack.py +++ b/providers/tests/slack/hooks/test_slack.py @@ -533,7 +533,10 @@ def test_send_file_v2_channel_name(self, mocked_client, caplog): @pytest.mark.parametrize("filename", [None, "foo.bar"]) @pytest.mark.parametrize("channel", [None, "#random"]) @pytest.mark.parametrize("filetype", [None, "auto"]) - def test_send_file_v1_to_v2_content(self, initial_comment, title, filename, channel, filetype): + @pytest.mark.parametrize("snippet_type", [None, "text"]) + def test_send_file_v1_to_v2_content( + self, initial_comment, title, filename, channel, filetype, snippet_type + ): hook = SlackHook(slack_conn_id=SLACK_API_DEFAULT_CONN_ID) with mock.patch.object(SlackHook, "send_file_v2") as mocked_send_file_v2: hook.send_file_v1_to_v2( @@ -543,6 +546,7 @@ def test_send_file_v1_to_v2_content(self, initial_comment, title, filename, chan initial_comment=initial_comment, title=title, filetype=filetype, + snippet_type=snippet_type, ) mocked_send_file_v2.assert_called_once_with( channel_id=channel, @@ -550,7 +554,7 @@ def test_send_file_v1_to_v2_content(self, initial_comment, title, filename, chan "content": '{"foo": "bar"}', "filename": filename, "title": title, - "snippet_type": filetype, + "snippet_type": snippet_type, }, initial_comment=initial_comment, ) @@ -560,7 +564,8 @@ def test_send_file_v1_to_v2_content(self, initial_comment, title, filename, chan @pytest.mark.parametrize("filename", [None, "foo.bar"]) @pytest.mark.parametrize("channel", [None, "#random"]) @pytest.mark.parametrize("filetype", [None, "auto"]) - def test_send_file_v1_to_v2_file(self, initial_comment, title, filename, channel, filetype): + @pytest.mark.parametrize("snippet_type", [None, "text"]) + def test_send_file_v1_to_v2_file(self, initial_comment, title, filename, channel, filetype, snippet_type): hook = SlackHook(slack_conn_id=SLACK_API_DEFAULT_CONN_ID) with mock.patch.object(SlackHook, "send_file_v2") as mocked_send_file_v2: hook.send_file_v1_to_v2( @@ -570,6 +575,7 @@ def test_send_file_v1_to_v2_file(self, initial_comment, title, filename, channel initial_comment=initial_comment, title=title, filetype=filetype, + snippet_type=snippet_type, ) mocked_send_file_v2.assert_called_once_with( channel_id=channel, @@ -577,7 +583,7 @@ def test_send_file_v1_to_v2_file(self, initial_comment, title, filename, channel "file": "/foo/bar/spam.egg", "filename": filename or "spam.egg", "title": title, - "snippet_type": filetype, + "snippet_type": snippet_type, }, initial_comment=initial_comment, ) diff --git a/providers/tests/slack/operators/test_slack.py b/providers/tests/slack/operators/test_slack.py index f3b02839a3e55..7f2d316387b2a 100644 --- a/providers/tests/slack/operators/test_slack.py +++ b/providers/tests/slack/operators/test_slack.py @@ -201,6 +201,7 @@ def setup_method(self): self.test_content = "This is a test text file!" self.test_api_params = {"key": "value"} self.expected_method = "files.upload" + self.test_snippet_type = "text" def __construct_operator(self, test_slack_conn_id, test_api_params=None): return SlackAPIFileOperator( @@ -212,6 +213,7 @@ def __construct_operator(self, test_slack_conn_id, test_api_params=None): filetype=self.test_filetype, content=self.test_content, api_params=test_api_params, + snippet_type=self.test_snippet_type, ) def test_init_with_valid_params(self): @@ -226,6 +228,7 @@ def test_init_with_valid_params(self): assert slack_api_post_operator.filename == self.filename assert slack_api_post_operator.filetype == self.test_filetype assert slack_api_post_operator.content == self.test_content + assert slack_api_post_operator.snippet_type == self.test_snippet_type assert not hasattr(slack_api_post_operator, "token") @pytest.mark.parametrize("initial_comment", [None, "foo-bar"]) @@ -237,7 +240,10 @@ def test_init_with_valid_params(self): pytest.param("v2", "send_file_v1_to_v2", id="v2"), ], ) - def test_api_call_params_with_content_args(self, initial_comment, title, method_version, method_name): + @pytest.mark.parametrize("snippet_type", [None, "text"]) + def test_api_call_params_with_content_args( + self, initial_comment, title, method_version, method_name, snippet_type + ): op = SlackAPIFileOperator( task_id="slack", slack_conn_id=SLACK_API_TEST_CONNECTION_ID, @@ -246,6 +252,7 @@ def test_api_call_params_with_content_args(self, initial_comment, title, method_ initial_comment=initial_comment, title=title, method_version=method_version, + snippet_type=snippet_type, ) with mock.patch(f"airflow.providers.slack.operators.slack.SlackHook.{method_name}") as mock_send_file: op.execute({}) @@ -256,6 +263,7 @@ def test_api_call_params_with_content_args(self, initial_comment, title, method_ filetype=None, initial_comment=initial_comment, title=title, + snippet_type=snippet_type, ) @pytest.mark.parametrize("initial_comment", [None, "foo-bar"]) @@ -267,7 +275,10 @@ def test_api_call_params_with_content_args(self, initial_comment, title, method_ pytest.param("v2", "send_file_v1_to_v2", id="v2"), ], ) - def test_api_call_params_with_file_args(self, initial_comment, title, method_version, method_name): + @pytest.mark.parametrize("snippet_type", [None, "text"]) + def test_api_call_params_with_file_args( + self, initial_comment, title, method_version, method_name, snippet_type + ): op = SlackAPIFileOperator( task_id="slack", slack_conn_id=SLACK_API_TEST_CONNECTION_ID, @@ -276,6 +287,7 @@ def test_api_call_params_with_file_args(self, initial_comment, title, method_ver initial_comment=initial_comment, title=title, method_version=method_version, + snippet_type=snippet_type, ) with mock.patch(f"airflow.providers.slack.operators.slack.SlackHook.{method_name}") as mock_send_file: op.execute({}) @@ -286,6 +298,7 @@ def test_api_call_params_with_file_args(self, initial_comment, title, method_ver filetype=None, initial_comment=initial_comment, title=title, + snippet_type=snippet_type, ) def test_channel_deprecated(self): From 218b82576b506bbee075e0920a36556cfb82a86d Mon Sep 17 00:00:00 2001 From: Pierre Jeambrun Date: Fri, 25 Oct 2024 06:09:33 +0800 Subject: [PATCH 107/258] Change prettier config to json (#43353) --- airflow/ui/.prettierrc | 13 ++++++++++++ airflow/ui/prettier.config.js | 39 ----------------------------------- 2 files changed, 13 insertions(+), 39 deletions(-) create mode 100644 airflow/ui/.prettierrc delete mode 100644 airflow/ui/prettier.config.js diff --git a/airflow/ui/.prettierrc b/airflow/ui/.prettierrc new file mode 100644 index 0000000000000..93ba8a38a47fe --- /dev/null +++ b/airflow/ui/.prettierrc @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "endOfLine": "lf", + "importOrder": ["", "^(src|openapi)/", "^[./]"], + "importOrderSeparation": true, + "jsxSingleQuote": false, + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "printWidth": 80, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/airflow/ui/prettier.config.js b/airflow/ui/prettier.config.js deleted file mode 100644 index 7846b10ed0f7c..0000000000000 --- a/airflow/ui/prettier.config.js +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * 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. - */ - -/** - * @import { Config } from "prettier"; - * @import { PluginConfig } from "@trivago/prettier-plugin-sort-imports"; - */ - -/** - * Prettier configuration. - */ -export default /** @type {const} @satisfies {Config & PluginConfig} */ ({ - endOfLine: "lf", - importOrder: ["", "^(src|openapi)/", "^[./]"], - importOrderSeparation: true, - jsxSingleQuote: false, - plugins: ["@trivago/prettier-plugin-sort-imports"], - printWidth: 80, - singleQuote: false, - tabWidth: 2, - trailingComma: "all", - useTabs: false, -}); From 476e77d9aba492a2159b256bc7a4aa86e560dee5 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 25 Oct 2024 04:52:31 +0200 Subject: [PATCH 108/258] Fix spellcheck in main (#43369) --- docs/spelling_wordlist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 78025bfe9fe22..917d0b2634ca1 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -158,6 +158,7 @@ balancers Banco Bas BaseClient +BaseHook BaseObject BaseOperator baseOperator From 7e56dac75cb2665e0c69bd331d7cc2f3d5056bab Mon Sep 17 00:00:00 2001 From: Luca Furrer <42588460+lucafurrer@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:04:53 +0200 Subject: [PATCH 109/258] DatabricksHook: fix status property to work with ClientResponse used in async mode (#43333) * fix status property to work with ClientResponse used in async mode in Databricks Hook * add unittest for Databricks async api call --- .../providers/databricks/hooks/databricks_base.py | 2 +- .../tests/databricks/hooks/test_databricks.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/providers/src/airflow/providers/databricks/hooks/databricks_base.py b/providers/src/airflow/providers/databricks/hooks/databricks_base.py index cf80b58d42eb3..8a4a7335a435b 100644 --- a/providers/src/airflow/providers/databricks/hooks/databricks_base.py +++ b/providers/src/airflow/providers/databricks/hooks/databricks_base.py @@ -645,7 +645,7 @@ async def _a_do_api_call(self, endpoint_info: tuple[str, str], json: dict[str, A headers={**headers, **self.user_agent_header}, timeout=self.timeout_seconds, ) as response: - self.log.debug("Response Status Code: %s", response.status_code) + self.log.debug("Response Status Code: %s", response.status) self.log.debug("Response text: %s", response.text) response.raise_for_status() return await response.json() diff --git a/providers/tests/databricks/hooks/test_databricks.py b/providers/tests/databricks/hooks/test_databricks.py index e2323f548d70a..4eaeddf972edf 100644 --- a/providers/tests/databricks/hooks/test_databricks.py +++ b/providers/tests/databricks/hooks/test_databricks.py @@ -1309,6 +1309,20 @@ async def test_async_do_api_call_respects_schema(self, mock_get): mock_get.assert_called_once() assert mock_get.call_args.args == (f"http://{HOST}:7908/api/2.1/foo/bar",) + @pytest.mark.asyncio + @mock.patch("airflow.providers.databricks.hooks.databricks_base.aiohttp.ClientSession.get") + async def test_async_do_api_call_only_existing_response_properties_are_read(self, mock_get): + self.hook.log.setLevel("DEBUG") + response = mock_get.return_value.__aenter__.return_value + response.mock_add_spec(aiohttp.ClientResponse, spec_set=True) + response.json = AsyncMock(return_value={"bar": "baz"}) + async with self.hook: + run_page_url = await self.hook._a_do_api_call(("GET", "api/2.1/foo/bar")) + + assert run_page_url == {"bar": "baz"} + mock_get.assert_called_once() + assert mock_get.call_args.args == (f"http://{HOST}:7908/api/2.1/foo/bar",) + class TestRunState: def test_is_terminal_true(self): From 752f933102754f330c4ffb5a545a4f4de93eef78 Mon Sep 17 00:00:00 2001 From: AutomationDev85 <96178949+AutomationDev85@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:53:04 +0200 Subject: [PATCH 110/258] EdgeWorker support log file upload in chunks (#43374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * EdgeWorker support log file upload in chunks * Reworked default value --------- Co-authored-by: Marco Küttelwesch --- .../src/airflow/providers/edge/CHANGELOG.rst | 8 ++++++++ .../src/airflow/providers/edge/__init__.py | 2 +- .../airflow/providers/edge/cli/edge_command.py | 18 +++++++++++------- .../src/airflow/providers/edge/provider.yaml | 14 +++++++++++++- providers/tests/edge/cli/test_edge_command.py | 17 ++++++++++++++++- 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/providers/src/airflow/providers/edge/CHANGELOG.rst b/providers/src/airflow/providers/edge/CHANGELOG.rst index ce52e4463c37a..afd17c6b29695 100644 --- a/providers/src/airflow/providers/edge/CHANGELOG.rst +++ b/providers/src/airflow/providers/edge/CHANGELOG.rst @@ -27,6 +27,14 @@ Changelog --------- +0.4.0pre0 +......... + +Misc +~~~~ + +* ``Edge Worker uploads log file in chunks. Chunk size can be defined by push_log_chunk_size value in config.`` + 0.3.0pre0 ......... diff --git a/providers/src/airflow/providers/edge/__init__.py b/providers/src/airflow/providers/edge/__init__.py index dcf55bfe16016..9b565f8e89d31 100644 --- a/providers/src/airflow/providers/edge/__init__.py +++ b/providers/src/airflow/providers/edge/__init__.py @@ -29,7 +29,7 @@ __all__ = ["__version__"] -__version__ = "0.3.0pre0" +__version__ = "0.4.0pre0" if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( "2.10.0" diff --git a/providers/src/airflow/providers/edge/cli/edge_command.py b/providers/src/airflow/providers/edge/cli/edge_command.py index 1d5013703c3ba..5e900a8a2c075 100644 --- a/providers/src/airflow/providers/edge/cli/edge_command.py +++ b/providers/src/airflow/providers/edge/cli/edge_command.py @@ -238,14 +238,18 @@ def check_running_jobs(self) -> None: EdgeJob.set_state(job.edge_job.key, TaskInstanceState.FAILED) if job.logfile.exists() and job.logfile.stat().st_size > job.logsize: with job.logfile.open("r") as logfile: + push_log_chunk_size = conf.getint("edge", "push_log_chunk_size") logfile.seek(job.logsize, os.SEEK_SET) - logdata = logfile.read() - EdgeLogs.push_logs( - task=job.edge_job.key, - log_chunk_time=datetime.now(), - log_chunk_data=logdata, - ) - job.logsize += len(logdata) + while True: + logdata = logfile.read(push_log_chunk_size) + if not logdata: + break + EdgeLogs.push_logs( + task=job.edge_job.key, + log_chunk_time=datetime.now(), + log_chunk_data=logdata, + ) + job.logsize += len(logdata) def heartbeat(self) -> None: """Report liveness state of worker to central site with stats.""" diff --git a/providers/src/airflow/providers/edge/provider.yaml b/providers/src/airflow/providers/edge/provider.yaml index 667aafddf4cc6..28de7ca89b966 100644 --- a/providers/src/airflow/providers/edge/provider.yaml +++ b/providers/src/airflow/providers/edge/provider.yaml @@ -27,7 +27,7 @@ source-date-epoch: 1729683247 # note that those versions are maintained by release manager - do not update them manually versions: - - 0.3.0pre0 + - 0.4.0pre0 dependencies: - apache-airflow>=2.10.0 @@ -102,3 +102,15 @@ config: type: integer example: ~ default: "60" + push_log_chunk_size: + description: | + Edge Worker uploads log files in chunks. If the log file part which is uploaded + exceeds the chunk size it creates a new request. The application gateway can + limit the max body size see: + https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size + A HTTP 413 issue can point to this value to fix the issue. + This value must be defined in Bytes. + version_added: ~ + type: integer + example: ~ + default: "524288" diff --git a/providers/tests/edge/cli/test_edge_command.py b/providers/tests/edge/cli/test_edge_command.py index 51aad5806802e..6b31db4c8d293 100644 --- a/providers/tests/edge/cli/test_edge_command.py +++ b/providers/tests/edge/cli/test_edge_command.py @@ -21,7 +21,7 @@ from datetime import datetime from pathlib import Path from subprocess import Popen -from unittest.mock import patch +from unittest.mock import call, patch import pytest import time_machine @@ -225,6 +225,21 @@ def test_check_running_jobs_log_push_increment(self, mock_push_logs, worker_with task=job.edge_job.key, log_chunk_time=datetime.now(), log_chunk_data="world" ) + @time_machine.travel(datetime.now(), tick=False) + @patch("airflow.providers.edge.models.edge_logs.EdgeLogs.push_logs") + def test_check_running_jobs_log_push_chunks(self, mock_push_logs, worker_with_job: _EdgeWorkerCli): + job = worker_with_job.jobs[0] + job.process.generated_returncode = None + job.logfile.write_text("log1log2log3") + with conf_vars({("edge", "api_url"): "https://mock.server", ("edge", "push_log_chunk_size"): "4"}): + worker_with_job.check_running_jobs() + assert len(worker_with_job.jobs) == 1 + calls = mock_push_logs.call_args_list + len(calls) == 3 + assert calls[0] == call(task=job.edge_job.key, log_chunk_time=datetime.now(), log_chunk_data="log1") + assert calls[1] == call(task=job.edge_job.key, log_chunk_time=datetime.now(), log_chunk_data="log2") + assert calls[2] == call(task=job.edge_job.key, log_chunk_time=datetime.now(), log_chunk_data="log3") + @pytest.mark.parametrize( "drain, jobs, expected_state", [ From 14abd334e8827b7e8d412a56a228dbded00a4623 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU <68415893+jason810496@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:08:42 +0800 Subject: [PATCH 111/258] AIP-84: Add UI batch recent dag runs endpoint (#43204) * AIP-84 | add UI batch recent dag runs endpoint * AIP-84 | add UI batch recent dag runs * refactor: use public serializer for ui batch recent runs * fix: add trigger_by for dag run in public test_dag * refactor: use DAGRunResponse from public endpoint * Update code review --------- Co-authored-by: pierrejeambrun --- .../core_api/openapi/v1-generated.yaml | 272 +++++++++++++++++ .../core_api/routes/ui/__init__.py | 2 + .../api_fastapi/core_api/routes/ui/dags.py | 133 ++++++++ .../core_api/serializers/ui/__init__.py | 16 + .../core_api/serializers/ui/dags.py | 36 +++ airflow/ui/openapi-gen/queries/common.ts | 51 ++++ airflow/ui/openapi-gen/queries/prefetch.ts | 71 +++++ airflow/ui/openapi-gen/queries/queries.ts | 80 +++++ airflow/ui/openapi-gen/queries/suspense.ts | 80 +++++ .../ui/openapi-gen/requests/schemas.gen.ts | 283 ++++++++++++++++++ .../ui/openapi-gen/requests/services.gen.ts | 45 +++ airflow/ui/openapi-gen/requests/types.gen.ts | 74 +++++ .../core_api/routes/public/test_dags.py | 4 +- .../core_api/routes/ui/test_dags.py | 104 +++++++ 14 files changed, 1250 insertions(+), 1 deletion(-) create mode 100644 airflow/api_fastapi/core_api/routes/ui/dags.py create mode 100644 airflow/api_fastapi/core_api/serializers/ui/__init__.py create mode 100644 airflow/api_fastapi/core_api/serializers/ui/dags.py create mode 100644 tests/api_fastapi/core_api/routes/ui/test_dags.py diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 76c28214100f8..a6ac7ac79d28d 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -73,6 +73,103 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /ui/dags/recent_dag_runs: + get: + tags: + - Dags + summary: Recent Dag Runs + description: Get recent DAG runs. + operationId: recent_dag_runs + parameters: + - name: dag_runs_limit + in: query + required: false + schema: + type: integer + default: 10 + title: Dag Runs Limit + - name: limit + in: query + required: false + schema: + type: integer + default: 100 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + default: 0 + title: Offset + - name: tags + in: query + required: false + schema: + type: array + items: + type: string + title: Tags + - name: owners + in: query + required: false + schema: + type: array + items: + type: string + title: Owners + - name: dag_id_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Dag Id Pattern + - name: dag_display_name_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Dag Display Name Pattern + - name: only_active + in: query + required: false + schema: + type: boolean + default: true + title: Only Active + - name: paused + in: query + required: false + schema: + anyOf: + - type: boolean + - type: 'null' + title: Paused + - name: last_dag_run_state + in: query + required: false + schema: + anyOf: + - $ref: '#/components/schemas/DagRunState' + - type: 'null' + title: Last Dag Run State + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DAGWithLatestDagRunsCollectionResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /public/dags/: get: tags: @@ -2093,6 +2190,181 @@ components: - total_entries title: DAGTagCollectionResponse description: DAG Tags Collection serializer for responses. + DAGWithLatestDagRunsCollectionResponse: + properties: + total_entries: + type: integer + title: Total Entries + dags: + items: + $ref: '#/components/schemas/DAGWithLatestDagRunsResponse' + type: array + title: Dags + type: object + required: + - total_entries + - dags + title: DAGWithLatestDagRunsCollectionResponse + description: DAG with latest dag runs collection response serializer. + DAGWithLatestDagRunsResponse: + properties: + dag_id: + type: string + title: Dag Id + dag_display_name: + type: string + title: Dag Display Name + is_paused: + type: boolean + title: Is Paused + is_active: + type: boolean + title: Is Active + last_parsed_time: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Last Parsed Time + last_pickled: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Last Pickled + last_expired: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Last Expired + scheduler_lock: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Scheduler Lock + pickle_id: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Pickle Id + default_view: + anyOf: + - type: string + - type: 'null' + title: Default View + fileloc: + type: string + title: Fileloc + description: + anyOf: + - type: string + - type: 'null' + title: Description + timetable_summary: + anyOf: + - type: string + - type: 'null' + title: Timetable Summary + timetable_description: + anyOf: + - type: string + - type: 'null' + title: Timetable Description + tags: + items: + $ref: '#/components/schemas/DagTagPydantic' + type: array + title: Tags + max_active_tasks: + type: integer + title: Max Active Tasks + max_active_runs: + anyOf: + - type: integer + - type: 'null' + title: Max Active Runs + max_consecutive_failed_dag_runs: + type: integer + title: Max Consecutive Failed Dag Runs + has_task_concurrency_limits: + type: boolean + title: Has Task Concurrency Limits + has_import_errors: + type: boolean + title: Has Import Errors + next_dagrun: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Next Dagrun + next_dagrun_data_interval_start: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Next Dagrun Data Interval Start + next_dagrun_data_interval_end: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Next Dagrun Data Interval End + next_dagrun_create_after: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Next Dagrun Create After + owners: + items: + type: string + type: array + title: Owners + latest_dag_runs: + items: + $ref: '#/components/schemas/DAGRunResponse' + type: array + title: Latest Dag Runs + file_token: + type: string + title: File Token + description: Return file token. + readOnly: true + type: object + required: + - dag_id + - dag_display_name + - is_paused + - is_active + - last_parsed_time + - last_pickled + - last_expired + - scheduler_lock + - pickle_id + - default_view + - fileloc + - description + - timetable_summary + - timetable_description + - tags + - max_active_tasks + - max_active_runs + - max_consecutive_failed_dag_runs + - has_task_concurrency_limits + - has_import_errors + - next_dagrun + - next_dagrun_data_interval_start + - next_dagrun_data_interval_end + - next_dagrun_create_after + - owners + - latest_dag_runs + - file_token + title: DAGWithLatestDagRunsResponse + description: DAG with latest dag runs response serializer. DagProcessorInfoSchema: properties: status: diff --git a/airflow/api_fastapi/core_api/routes/ui/__init__.py b/airflow/api_fastapi/core_api/routes/ui/__init__.py index 9cd16fcdd16b3..b7ebf9c5c46fc 100644 --- a/airflow/api_fastapi/core_api/routes/ui/__init__.py +++ b/airflow/api_fastapi/core_api/routes/ui/__init__.py @@ -18,9 +18,11 @@ from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.routes.ui.assets import assets_router +from airflow.api_fastapi.core_api.routes.ui.dags import dags_router from airflow.api_fastapi.core_api.routes.ui.dashboard import dashboard_router ui_router = AirflowRouter(prefix="/ui") ui_router.include_router(assets_router) ui_router.include_router(dashboard_router) +ui_router.include_router(dags_router) diff --git a/airflow/api_fastapi/core_api/routes/ui/dags.py b/airflow/api_fastapi/core_api/routes/ui/dags.py new file mode 100644 index 0000000000000..665373734bb90 --- /dev/null +++ b/airflow/api_fastapi/core_api/routes/ui/dags.py @@ -0,0 +1,133 @@ +# 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 fastapi import Depends +from sqlalchemy import and_, func, select +from sqlalchemy.orm import Session +from typing_extensions import Annotated + +from airflow.api_fastapi.common.db.common import ( + get_session, + paginated_select, +) +from airflow.api_fastapi.common.parameters import ( + QueryDagDisplayNamePatternSearch, + QueryDagIdPatternSearch, + QueryLastDagRunStateFilter, + QueryLimit, + QueryOffset, + QueryOnlyActiveFilter, + QueryOwnersFilter, + QueryPausedFilter, + QueryTagsFilter, +) +from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.core_api.serializers.dag_run import DAGRunResponse +from airflow.api_fastapi.core_api.serializers.dags import DAGResponse +from airflow.api_fastapi.core_api.serializers.ui.dags import ( + DAGWithLatestDagRunsCollectionResponse, + DAGWithLatestDagRunsResponse, +) +from airflow.models import DagModel, DagRun + +dags_router = AirflowRouter(prefix="/dags", tags=["Dags"]) + + +@dags_router.get("/recent_dag_runs", include_in_schema=False, response_model_exclude_none=True) +async def recent_dag_runs( + limit: QueryLimit, + offset: QueryOffset, + tags: QueryTagsFilter, + owners: QueryOwnersFilter, + dag_id_pattern: QueryDagIdPatternSearch, + dag_display_name_pattern: QueryDagDisplayNamePatternSearch, + only_active: QueryOnlyActiveFilter, + paused: QueryPausedFilter, + last_dag_run_state: QueryLastDagRunStateFilter, + session: Annotated[Session, Depends(get_session)], + dag_runs_limit: int = 10, +) -> DAGWithLatestDagRunsCollectionResponse: + """Get recent DAG runs.""" + recent_runs_subquery = ( + select( + DagRun.dag_id, + DagRun.execution_date, + func.rank() + .over( + partition_by=DagRun.dag_id, + order_by=DagRun.execution_date.desc(), + ) + .label("rank"), + ) + .order_by(DagRun.execution_date.desc()) + .subquery() + ) + dags_with_recent_dag_runs_select = ( + select( + DagRun, + DagModel, + recent_runs_subquery.c.execution_date, + ) + .join(DagModel, DagModel.dag_id == recent_runs_subquery.c.dag_id) + .join( + DagRun, + and_( + DagRun.dag_id == DagModel.dag_id, + DagRun.execution_date == recent_runs_subquery.c.execution_date, + ), + ) + .where(recent_runs_subquery.c.rank <= dag_runs_limit) + .group_by( + DagModel.dag_id, + recent_runs_subquery.c.execution_date, + DagRun.execution_date, + DagRun.id, + ) + .order_by(recent_runs_subquery.c.execution_date.desc()) + ) + dags_with_recent_dag_runs_select_filter, _ = paginated_select( + dags_with_recent_dag_runs_select, + [only_active, paused, dag_id_pattern, dag_display_name_pattern, tags, owners, last_dag_run_state], + None, + offset, + limit, + ) + dags_with_recent_dag_runs = session.execute(dags_with_recent_dag_runs_select_filter) + # aggregate rows by dag_id + dag_runs_by_dag_id: dict[str, DAGWithLatestDagRunsResponse] = {} + + for row in dags_with_recent_dag_runs: + dag_run, dag, *_ = row + dag_id = dag.dag_id + dag_run_response = DAGRunResponse.model_validate(dag_run, from_attributes=True) + if dag_id not in dag_runs_by_dag_id: + dag_response = DAGResponse.model_validate(dag, from_attributes=True) + dag_runs_by_dag_id[dag_id] = DAGWithLatestDagRunsResponse.model_validate( + { + **dag_response.dict(), + "latest_dag_runs": [dag_run_response], + } + ) + else: + dag_runs_by_dag_id[dag_id].latest_dag_runs.append(dag_run_response) + + return DAGWithLatestDagRunsCollectionResponse( + total_entries=len(dag_runs_by_dag_id), + dags=list(dag_runs_by_dag_id.values()), + ) diff --git a/airflow/api_fastapi/core_api/serializers/ui/__init__.py b/airflow/api_fastapi/core_api/serializers/ui/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/airflow/api_fastapi/core_api/serializers/ui/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/airflow/api_fastapi/core_api/serializers/ui/dags.py b/airflow/api_fastapi/core_api/serializers/ui/dags.py new file mode 100644 index 0000000000000..f985ce99a9725 --- /dev/null +++ b/airflow/api_fastapi/core_api/serializers/ui/dags.py @@ -0,0 +1,36 @@ +# 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 + +from airflow.api_fastapi.core_api.serializers.dag_run import DAGRunResponse +from airflow.api_fastapi.core_api.serializers.dags import DAGResponse + + +class DAGWithLatestDagRunsResponse(DAGResponse): + """DAG with latest dag runs response serializer.""" + + latest_dag_runs: list[DAGRunResponse] + + +class DAGWithLatestDagRunsCollectionResponse(BaseModel): + """DAG with latest dag runs collection response serializer.""" + + total_entries: int + dags: list[DAGWithLatestDagRunsResponse] diff --git a/airflow/ui/openapi-gen/queries/common.ts b/airflow/ui/openapi-gen/queries/common.ts index b12c133b6c2ea..5fa46e4e91f09 100644 --- a/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow/ui/openapi-gen/queries/common.ts @@ -6,6 +6,7 @@ import { ConnectionService, DagRunService, DagService, + DagsService, DashboardService, MonitorService, PluginService, @@ -54,6 +55,56 @@ export const UseDashboardServiceHistoricalMetricsKeyFn = ( useDashboardServiceHistoricalMetricsKey, ...(queryKey ?? [{ endDate, startDate }]), ]; +export type DagsServiceRecentDagRunsDefaultResponse = Awaited< + ReturnType +>; +export type DagsServiceRecentDagRunsQueryResult< + TData = DagsServiceRecentDagRunsDefaultResponse, + TError = unknown, +> = UseQueryResult; +export const useDagsServiceRecentDagRunsKey = "DagsServiceRecentDagRuns"; +export const UseDagsServiceRecentDagRunsKeyFn = ( + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }: { + dagDisplayNamePattern?: string; + dagIdPattern?: string; + dagRunsLimit?: number; + lastDagRunState?: DagRunState; + limit?: number; + offset?: number; + onlyActive?: boolean; + owners?: string[]; + paused?: boolean; + tags?: string[]; + } = {}, + queryKey?: Array, +) => [ + useDagsServiceRecentDagRunsKey, + ...(queryKey ?? [ + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }, + ]), +]; export type DagServiceGetDagsDefaultResponse = Awaited< ReturnType >; diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow/ui/openapi-gen/queries/prefetch.ts index 3f681a4a13b60..72b4376751f7f 100644 --- a/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow/ui/openapi-gen/queries/prefetch.ts @@ -6,6 +6,7 @@ import { ConnectionService, DagRunService, DagService, + DagsService, DashboardService, MonitorService, PluginService, @@ -62,6 +63,76 @@ export const prefetchUseDashboardServiceHistoricalMetrics = ( }), queryFn: () => DashboardService.historicalMetrics({ endDate, startDate }), }); +/** + * Recent Dag Runs + * Get recent DAG runs. + * @param data The data for the request. + * @param data.dagRunsLimit + * @param data.limit + * @param data.offset + * @param data.tags + * @param data.owners + * @param data.dagIdPattern + * @param data.dagDisplayNamePattern + * @param data.onlyActive + * @param data.paused + * @param data.lastDagRunState + * @returns DAGWithLatestDagRunsCollectionResponse Successful Response + * @throws ApiError + */ +export const prefetchUseDagsServiceRecentDagRuns = ( + queryClient: QueryClient, + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }: { + dagDisplayNamePattern?: string; + dagIdPattern?: string; + dagRunsLimit?: number; + lastDagRunState?: DagRunState; + limit?: number; + offset?: number; + onlyActive?: boolean; + owners?: string[]; + paused?: boolean; + tags?: string[]; + } = {}, +) => + queryClient.prefetchQuery({ + queryKey: Common.UseDagsServiceRecentDagRunsKeyFn({ + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }), + queryFn: () => + DagsService.recentDagRuns({ + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }), + }); /** * Get Dags * Get all DAGs. diff --git a/airflow/ui/openapi-gen/queries/queries.ts b/airflow/ui/openapi-gen/queries/queries.ts index e3942ad84e086..ce319bc6cd676 100644 --- a/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow/ui/openapi-gen/queries/queries.ts @@ -11,6 +11,7 @@ import { ConnectionService, DagRunService, DagService, + DagsService, DashboardService, MonitorService, PluginService, @@ -86,6 +87,85 @@ export const useDashboardServiceHistoricalMetrics = < DashboardService.historicalMetrics({ endDate, startDate }) as TData, ...options, }); +/** + * Recent Dag Runs + * Get recent DAG runs. + * @param data The data for the request. + * @param data.dagRunsLimit + * @param data.limit + * @param data.offset + * @param data.tags + * @param data.owners + * @param data.dagIdPattern + * @param data.dagDisplayNamePattern + * @param data.onlyActive + * @param data.paused + * @param data.lastDagRunState + * @returns DAGWithLatestDagRunsCollectionResponse Successful Response + * @throws ApiError + */ +export const useDagsServiceRecentDagRuns = < + TData = Common.DagsServiceRecentDagRunsDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }: { + dagDisplayNamePattern?: string; + dagIdPattern?: string; + dagRunsLimit?: number; + lastDagRunState?: DagRunState; + limit?: number; + offset?: number; + onlyActive?: boolean; + owners?: string[]; + paused?: boolean; + tags?: string[]; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useQuery({ + queryKey: Common.UseDagsServiceRecentDagRunsKeyFn( + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }, + queryKey, + ), + queryFn: () => + DagsService.recentDagRuns({ + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }) as TData, + ...options, + }); /** * Get Dags * Get all DAGs. diff --git a/airflow/ui/openapi-gen/queries/suspense.ts b/airflow/ui/openapi-gen/queries/suspense.ts index eb91e8f1ba936..cd7bc95fa5b59 100644 --- a/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow/ui/openapi-gen/queries/suspense.ts @@ -6,6 +6,7 @@ import { ConnectionService, DagRunService, DagService, + DagsService, DashboardService, MonitorService, PluginService, @@ -75,6 +76,85 @@ export const useDashboardServiceHistoricalMetricsSuspense = < DashboardService.historicalMetrics({ endDate, startDate }) as TData, ...options, }); +/** + * Recent Dag Runs + * Get recent DAG runs. + * @param data The data for the request. + * @param data.dagRunsLimit + * @param data.limit + * @param data.offset + * @param data.tags + * @param data.owners + * @param data.dagIdPattern + * @param data.dagDisplayNamePattern + * @param data.onlyActive + * @param data.paused + * @param data.lastDagRunState + * @returns DAGWithLatestDagRunsCollectionResponse Successful Response + * @throws ApiError + */ +export const useDagsServiceRecentDagRunsSuspense = < + TData = Common.DagsServiceRecentDagRunsDefaultResponse, + TError = unknown, + TQueryKey extends Array = unknown[], +>( + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }: { + dagDisplayNamePattern?: string; + dagIdPattern?: string; + dagRunsLimit?: number; + lastDagRunState?: DagRunState; + limit?: number; + offset?: number; + onlyActive?: boolean; + owners?: string[]; + paused?: boolean; + tags?: string[]; + } = {}, + queryKey?: TQueryKey, + options?: Omit, "queryKey" | "queryFn">, +) => + useSuspenseQuery({ + queryKey: Common.UseDagsServiceRecentDagRunsKeyFn( + { + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }, + queryKey, + ), + queryFn: () => + DagsService.recentDagRuns({ + dagDisplayNamePattern, + dagIdPattern, + dagRunsLimit, + lastDagRunState, + limit, + offset, + onlyActive, + owners, + paused, + tags, + }) as TData, + ...options, + }); /** * Get Dags * Get all DAGs. diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts index b940646a69a77..a30712d02acea 100644 --- a/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -1122,6 +1122,289 @@ export const $DAGTagCollectionResponse = { description: "DAG Tags Collection serializer for responses.", } as const; +export const $DAGWithLatestDagRunsCollectionResponse = { + properties: { + total_entries: { + type: "integer", + title: "Total Entries", + }, + dags: { + items: { + $ref: "#/components/schemas/DAGWithLatestDagRunsResponse", + }, + type: "array", + title: "Dags", + }, + }, + type: "object", + required: ["total_entries", "dags"], + title: "DAGWithLatestDagRunsCollectionResponse", + description: "DAG with latest dag runs collection response serializer.", +} as const; + +export const $DAGWithLatestDagRunsResponse = { + properties: { + dag_id: { + type: "string", + title: "Dag Id", + }, + dag_display_name: { + type: "string", + title: "Dag Display Name", + }, + is_paused: { + type: "boolean", + title: "Is Paused", + }, + is_active: { + type: "boolean", + title: "Is Active", + }, + last_parsed_time: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Last Parsed Time", + }, + last_pickled: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Last Pickled", + }, + last_expired: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Last Expired", + }, + scheduler_lock: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Scheduler Lock", + }, + pickle_id: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Pickle Id", + }, + default_view: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Default View", + }, + fileloc: { + type: "string", + title: "Fileloc", + }, + description: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Description", + }, + timetable_summary: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Timetable Summary", + }, + timetable_description: { + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + title: "Timetable Description", + }, + tags: { + items: { + $ref: "#/components/schemas/DagTagPydantic", + }, + type: "array", + title: "Tags", + }, + max_active_tasks: { + type: "integer", + title: "Max Active Tasks", + }, + max_active_runs: { + anyOf: [ + { + type: "integer", + }, + { + type: "null", + }, + ], + title: "Max Active Runs", + }, + max_consecutive_failed_dag_runs: { + type: "integer", + title: "Max Consecutive Failed Dag Runs", + }, + has_task_concurrency_limits: { + type: "boolean", + title: "Has Task Concurrency Limits", + }, + has_import_errors: { + type: "boolean", + title: "Has Import Errors", + }, + next_dagrun: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Next Dagrun", + }, + next_dagrun_data_interval_start: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Next Dagrun Data Interval Start", + }, + next_dagrun_data_interval_end: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Next Dagrun Data Interval End", + }, + next_dagrun_create_after: { + anyOf: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + title: "Next Dagrun Create After", + }, + owners: { + items: { + type: "string", + }, + type: "array", + title: "Owners", + }, + latest_dag_runs: { + items: { + $ref: "#/components/schemas/DAGRunResponse", + }, + type: "array", + title: "Latest Dag Runs", + }, + file_token: { + type: "string", + title: "File Token", + description: "Return file token.", + readOnly: true, + }, + }, + type: "object", + required: [ + "dag_id", + "dag_display_name", + "is_paused", + "is_active", + "last_parsed_time", + "last_pickled", + "last_expired", + "scheduler_lock", + "pickle_id", + "default_view", + "fileloc", + "description", + "timetable_summary", + "timetable_description", + "tags", + "max_active_tasks", + "max_active_runs", + "max_consecutive_failed_dag_runs", + "has_task_concurrency_limits", + "has_import_errors", + "next_dagrun", + "next_dagrun_data_interval_start", + "next_dagrun_data_interval_end", + "next_dagrun_create_after", + "owners", + "latest_dag_runs", + "file_token", + ], + title: "DAGWithLatestDagRunsResponse", + description: "DAG with latest dag runs response serializer.", +} as const; + export const $DagProcessorInfoSchema = { properties: { status: { diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow/ui/openapi-gen/requests/services.gen.ts index bfe1d2e39d361..08e93457c4f67 100644 --- a/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow/ui/openapi-gen/requests/services.gen.ts @@ -7,6 +7,8 @@ import type { NextRunAssetsResponse, HistoricalMetricsData, HistoricalMetricsResponse, + RecentDagRunsData, + RecentDagRunsResponse, GetDagsData, GetDagsResponse, PatchDagsData, @@ -111,6 +113,49 @@ export class DashboardService { } } +export class DagsService { + /** + * Recent Dag Runs + * Get recent DAG runs. + * @param data The data for the request. + * @param data.dagRunsLimit + * @param data.limit + * @param data.offset + * @param data.tags + * @param data.owners + * @param data.dagIdPattern + * @param data.dagDisplayNamePattern + * @param data.onlyActive + * @param data.paused + * @param data.lastDagRunState + * @returns DAGWithLatestDagRunsCollectionResponse Successful Response + * @throws ApiError + */ + public static recentDagRuns( + data: RecentDagRunsData = {}, + ): CancelablePromise { + return __request(OpenAPI, { + method: "GET", + url: "/ui/dags/recent_dag_runs", + query: { + dag_runs_limit: data.dagRunsLimit, + limit: data.limit, + offset: data.offset, + tags: data.tags, + owners: data.owners, + dag_id_pattern: data.dagIdPattern, + dag_display_name_pattern: data.dagDisplayNamePattern, + only_active: data.onlyActive, + paused: data.paused, + last_dag_run_state: data.lastDagRunState, + }, + errors: { + 422: "Validation Error", + }, + }); + } +} + export class DagService { /** * Get Dags diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts index 1bca12c04e777..174190f9f493a 100644 --- a/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow/ui/openapi-gen/requests/types.gen.ts @@ -206,6 +206,50 @@ export type DAGTagCollectionResponse = { total_entries: number; }; +/** + * DAG with latest dag runs collection response serializer. + */ +export type DAGWithLatestDagRunsCollectionResponse = { + total_entries: number; + dags: Array; +}; + +/** + * DAG with latest dag runs response serializer. + */ +export type DAGWithLatestDagRunsResponse = { + dag_id: string; + dag_display_name: string; + is_paused: boolean; + is_active: boolean; + last_parsed_time: string | null; + last_pickled: string | null; + last_expired: string | null; + scheduler_lock: string | null; + pickle_id: string | null; + default_view: string | null; + fileloc: string; + description: string | null; + timetable_summary: string | null; + timetable_description: string | null; + tags: Array; + max_active_tasks: number; + max_active_runs: number | null; + max_consecutive_failed_dag_runs: number; + has_task_concurrency_limits: boolean; + has_import_errors: boolean; + next_dagrun: string | null; + next_dagrun_data_interval_start: string | null; + next_dagrun_data_interval_end: string | null; + next_dagrun_create_after: string | null; + owners: Array; + latest_dag_runs: Array; + /** + * Return file token. + */ + readonly file_token: string; +}; + /** * Schema for DagProcessor info. */ @@ -474,6 +518,21 @@ export type HistoricalMetricsData = { export type HistoricalMetricsResponse = HistoricalMetricDataResponse; +export type RecentDagRunsData = { + dagDisplayNamePattern?: string | null; + dagIdPattern?: string | null; + dagRunsLimit?: number; + lastDagRunState?: DagRunState | null; + limit?: number; + offset?: number; + onlyActive?: boolean; + owners?: Array; + paused?: boolean | null; + tags?: Array; +}; + +export type RecentDagRunsResponse = DAGWithLatestDagRunsCollectionResponse; + export type GetDagsData = { dagDisplayNamePattern?: string | null; dagIdPattern?: string | null; @@ -696,6 +755,21 @@ export type $OpenApiTs = { }; }; }; + "/ui/dags/recent_dag_runs": { + get: { + req: RecentDagRunsData; + res: { + /** + * Successful Response + */ + 200: DAGWithLatestDagRunsCollectionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; "/public/dags/": { get: { req: GetDagsData; 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 0d3b8abbdec8c..03253dbfa7a7a 100644 --- a/tests/api_fastapi/core_api/routes/public/test_dags.py +++ b/tests/api_fastapi/core_api/routes/public/test_dags.py @@ -26,7 +26,7 @@ from airflow.operators.empty import EmptyOperator from airflow.utils.session import provide_session from airflow.utils.state import DagRunState, TaskInstanceState -from airflow.utils.types import DagRunType +from airflow.utils.types import DagRunTriggeredByType, DagRunType from tests_common.test_utils.db import clear_db_dags, clear_db_runs, clear_db_serialized_dags @@ -73,6 +73,7 @@ def _create_deactivated_paused_dag(self, session=None): 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( @@ -82,6 +83,7 @@ def _create_deactivated_paused_dag(self, session=None): 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) diff --git a/tests/api_fastapi/core_api/routes/ui/test_dags.py b/tests/api_fastapi/core_api/routes/ui/test_dags.py new file mode 100644 index 0000000000000..7258476bf163f --- /dev/null +++ b/tests/api_fastapi/core_api/routes/ui/test_dags.py @@ -0,0 +1,104 @@ +# 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 import DagRun +from airflow.utils.session import provide_session +from airflow.utils.state import DagRunState +from airflow.utils.types import DagRunTriggeredByType, DagRunType + +from tests.api_fastapi.core_api.routes.public.test_dags import ( + DAG1_ID, + DAG2_ID, + DAG3_ID, + DAG4_ID, + DAG5_ID, + TestDagEndpoint as TestPublicDagEndpoint, +) + +pytestmark = pytest.mark.db_test + + +class TestRecentDagRuns(TestPublicDagEndpoint): + @pytest.fixture(autouse=True) + @provide_session + def setup_dag_runs(self, session=None) -> None: + # Create DAG Runs + for dag_id in [DAG1_ID, DAG2_ID, DAG3_ID, DAG4_ID, DAG5_ID]: + dag_runs_count = 5 if dag_id in [DAG1_ID, DAG2_ID] else 2 + for i in range(dag_runs_count): + start_date = datetime(2021 + i, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + dag_run = DagRun( + dag_id=dag_id, + run_id=f"run_id_{i+1}", + run_type=DagRunType.MANUAL, + start_date=start_date, + execution_date=start_date, + state=(DagRunState.FAILED if i % 2 == 0 else DagRunState.SUCCESS), + triggered_by=DagRunTriggeredByType.TEST, + ) + dag_run.end_date = dag_run.start_date + pendulum.duration(hours=1) + session.add(dag_run) + session.commit() + + @pytest.mark.parametrize( + "query_params, expected_ids,expected_total_dag_runs", + [ + # Filters + ({}, [DAG1_ID, DAG2_ID], 11), + ({"limit": 1}, [DAG1_ID], 2), + ({"offset": 1}, [DAG1_ID, DAG2_ID], 11), + ({"tags": ["example"]}, [DAG1_ID], 6), + ({"only_active": False}, [DAG1_ID, DAG2_ID, DAG3_ID], 15), + ({"paused": True, "only_active": False}, [DAG3_ID], 4), + ({"paused": False}, [DAG1_ID, DAG2_ID], 11), + ({"owners": ["airflow"]}, [DAG1_ID, DAG2_ID], 11), + ({"owners": ["test_owner"], "only_active": False}, [DAG3_ID], 4), + ({"last_dag_run_state": "success", "only_active": False}, [DAG1_ID, DAG2_ID, DAG3_ID], 6), + ({"last_dag_run_state": "failed", "only_active": False}, [DAG1_ID, DAG2_ID, DAG3_ID], 9), + # Search + ({"dag_id_pattern": "1"}, [DAG1_ID], 6), + ({"dag_display_name_pattern": "test_dag2"}, [DAG2_ID], 5), + ], + ) + def test_recent_dag_runs(self, test_client, query_params, expected_ids, expected_total_dag_runs): + response = test_client.get("/ui/dags/recent_dag_runs", params=query_params) + assert response.status_code == 200 + body = response.json() + assert body["total_entries"] == len(expected_ids) + required_dag_run_key = [ + "run_id", + "dag_id", + "state", + "logical_date", + ] + for recent_dag_runs in body["dags"]: + dag_runs = recent_dag_runs["latest_dag_runs"] + # check date ordering + previous_execution_date = None + for dag_run in dag_runs: + # validate the response + for key in required_dag_run_key: + assert key in dag_run + if previous_execution_date: + assert previous_execution_date > dag_run["logical_date"] + previous_execution_date = dag_run["logical_date"] From 9e8fb40ba71d9bd07ae40503d5e7aa93ef042bb0 Mon Sep 17 00:00:00 2001 From: yangyulely Date: Fri, 25 Oct 2024 18:51:47 +0800 Subject: [PATCH 112/258] Improve the cursor type definition of mysql (#43376) --- .../src/airflow/providers/mysql/hooks/mysql.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/providers/src/airflow/providers/mysql/hooks/mysql.py b/providers/src/airflow/providers/mysql/hooks/mysql.py index a6e40ad32b87c..678c680c6706c 100644 --- a/providers/src/airflow/providers/mysql/hooks/mysql.py +++ b/providers/src/airflow/providers/mysql/hooks/mysql.py @@ -127,12 +127,17 @@ def _get_conn_config_mysql_client(self, conn: Connection) -> dict: if conn.extra_dejson.get("cursor", False): import MySQLdb.cursors - if (conn.extra_dejson["cursor"]).lower() == "sscursor": - conn_config["cursorclass"] = MySQLdb.cursors.SSCursor - elif (conn.extra_dejson["cursor"]).lower() == "dictcursor": - conn_config["cursorclass"] = MySQLdb.cursors.DictCursor - elif (conn.extra_dejson["cursor"]).lower() == "ssdictcursor": - conn_config["cursorclass"] = MySQLdb.cursors.SSDictCursor + cursor_type = conn.extra_dejson.get("cursor", "").lower() + # Dictionary mapping cursor types to their respective classes + cursor_classes = { + "sscursor": MySQLdb.cursors.SSCursor, + "dictcursor": MySQLdb.cursors.DictCursor, + "ssdictcursor": MySQLdb.cursors.SSDictCursor, + } + # Set the cursor class in the connection configuration based on the cursor type + if cursor_type in cursor_classes: + conn_config["cursorclass"] = cursor_classes[cursor_type] + if conn.extra_dejson.get("ssl", False): # SSL parameter for MySQL has to be a dictionary and in case # of extra/dejson we can get string if extra is passed via From 1cc1a2e89d9ededb2fcef6ac57fb554158c3ae16 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Fri, 25 Oct 2024 10:06:41 -0400 Subject: [PATCH 113/258] Update theme colors in new ui (#43330) * Update theme colors in new ui * use `color.` syntax for semantic colors --- airflow/ui/src/layouts/Nav/Nav.tsx | 138 +++++++++--------- airflow/ui/src/layouts/Nav/navButtonProps.ts | 6 + airflow/ui/src/pages/DagsList/DagCard.tsx | 145 +++++++++---------- airflow/ui/src/theme.ts | 23 ++- 4 files changed, 161 insertions(+), 151 deletions(-) diff --git a/airflow/ui/src/layouts/Nav/Nav.tsx b/airflow/ui/src/layouts/Nav/Nav.tsx index edf544f8992a0..8177f8065eb0e 100644 --- a/airflow/ui/src/layouts/Nav/Nav.tsx +++ b/airflow/ui/src/layouts/Nav/Nav.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, Icon, useColorModeValue, VStack } from "@chakra-ui/react"; +import { Box, Flex, Icon, VStack } from "@chakra-ui/react"; import { motion } from "framer-motion"; import { FiBarChart2, @@ -34,73 +34,69 @@ import { DocsButton } from "./DocsButton"; import { NavButton } from "./NavButton"; import { UserSettingsButton } from "./UserSettingsButton"; -export const Nav = () => { - const navBg = useColorModeValue("blue.100", "blue.900"); - - return ( - - - - - - } title="Home" to="/" /> - } - title="Dags" - to="dags" - /> - } - isDisabled - title="Assets" - to="assets" - /> - } - isDisabled - title="Dag Runs" - to="dag_runs" - /> - } - isDisabled - title="Browse" - to="browse" - /> - } - isDisabled - title="Admin" - to="admin" - /> - - - } - title="Return to legacy UI" - to={import.meta.env.VITE_LEGACY_API_URL} - /> - - - - - ); -}; +export const Nav = () => ( + + + + + + } title="Home" to="/" /> + } + title="Dags" + to="dags" + /> + } + isDisabled + title="Assets" + to="assets" + /> + } + isDisabled + title="Dag Runs" + to="dag_runs" + /> + } + isDisabled + title="Browse" + to="browse" + /> + } + isDisabled + title="Admin" + to="admin" + /> + + + } + title="Return to legacy UI" + to={import.meta.env.VITE_LEGACY_API_URL} + /> + + + + +); diff --git a/airflow/ui/src/layouts/Nav/navButtonProps.ts b/airflow/ui/src/layouts/Nav/navButtonProps.ts index 740348bc9676b..e91db6f1e4712 100644 --- a/airflow/ui/src/layouts/Nav/navButtonProps.ts +++ b/airflow/ui/src/layouts/Nav/navButtonProps.ts @@ -19,6 +19,12 @@ import type { ButtonProps } from "@chakra-ui/react"; export const navButtonProps: ButtonProps = { + _active: { + backgroundColor: "blue.emphasized", + }, + _hover: { + backgroundColor: "blue.solid", + }, alignItems: "center", borderRadius: "none", flexDir: "column", diff --git a/airflow/ui/src/pages/DagsList/DagCard.tsx b/airflow/ui/src/pages/DagsList/DagCard.tsx index 453c22e06eaf9..da5531fedb288 100644 --- a/airflow/ui/src/pages/DagsList/DagCard.tsx +++ b/airflow/ui/src/pages/DagsList/DagCard.tsx @@ -25,7 +25,6 @@ import { SimpleGrid, Text, Tooltip, - useColorModeValue, VStack, } from "@chakra-ui/react"; import { FiCalendar, FiTag } from "react-icons/fi"; @@ -40,82 +39,76 @@ type Props = { const MAX_TAGS = 3; -export const DagCard = ({ dag }: Props) => { - const cardBorder = useColorModeValue("gray.100", "gray.700"); - const tooltipBg = useColorModeValue("gray.200", "gray.700"); - - return ( - ( + + - - - - - {dag.dag_display_name} - - - {dag.tags.length ? ( - - - {dag.tags.slice(0, MAX_TAGS).map((tag) => ( - {tag.name} - ))} - {dag.tags.length > MAX_TAGS && ( - - {dag.tags.slice(MAX_TAGS).map((tag) => ( - {tag.name} - ))} - - } - > - +{dag.tags.length - MAX_TAGS} more - - )} - - ) : undefined} - - - - - - -
    - - - Next Run + + + + {dag.dag_display_name} - {Boolean(dag.next_dagrun) ? ( + + {dag.tags.length ? ( + + + {dag.tags.slice(0, MAX_TAGS).map((tag) => ( + {tag.name} + ))} + {dag.tags.length > MAX_TAGS && ( + + {dag.tags.slice(MAX_TAGS).map((tag) => ( + {tag.name} + ))} + + } + > + +{dag.tags.length - MAX_TAGS} more + + )} + + ) : undefined} + + + + + + +
    + + + Next Run + + {Boolean(dag.next_dagrun) ? ( + + + ) : undefined} + {Boolean(dag.timetable_summary) ? ( + - - ) : undefined} - {Boolean(dag.timetable_summary) ? ( - - - {" "} - {" "} - {dag.timetable_summary} - - - ) : undefined} - -
    -
    - - - ); -}; + + ) : undefined} + +
    +
    + + +); diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/theme.ts index b348c54aed821..7809e40079424 100644 --- a/airflow/ui/src/theme.ts +++ b/airflow/ui/src/theme.ts @@ -32,11 +32,11 @@ const baseStyle = definePartsStyle(() => ({ }, "&:nth-of-type(odd)": { td: { - background: "subtle-bg", + background: "blue.minimal", }, "th, td": { borderBottomWidth: "0px", - borderColor: "subtle-bg", + borderColor: "blue.subtle", }, }, }, @@ -53,6 +53,11 @@ const baseStyle = definePartsStyle(() => ({ export const tableTheme = defineMultiStyleConfig({ baseStyle }); const theme = extendTheme({ + colors: { + blue: { + 950: "#0c142e", + }, + }, components: { Table: tableTheme, Tooltip: { @@ -67,8 +72,18 @@ const theme = extendTheme({ }, semanticTokens: { colors: { - "subtle-bg": { _dark: "gray.900", _light: "blue.50" }, - "subtle-text": { _dark: "blue.500", _light: "blue.600" }, + blue: { + /* eslint-disable perfectionist/sort-objects */ + contrast: { _dark: "blue.200", _light: "blue.600" }, + focusRing: "blue.500", + fg: { _dark: "blue.600", _light: "blue.400" }, + emphasized: { _dark: "blue.700", _light: "blue.300" }, + solid: { _dark: "blue.800", _light: "blue.200" }, + muted: { _dark: "blue.900", _light: "blue.100" }, + subtle: { _dark: "blue.950", _light: "blue.50" }, + minimal: { _dark: "gray.900", _light: "blue.50" }, + /* eslint-enable perfectionist/sort-objects */ + }, }, }, styles: { From 4618b58a8614ec9162d8f359b65a57962d5800bc Mon Sep 17 00:00:00 2001 From: Kalyan R Date: Fri, 25 Oct 2024 20:59:53 +0530 Subject: [PATCH 114/258] add min version to types- deprecated, markdown,pymysql,pyyaml (#43371) --- hatch_build.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hatch_build.py b/hatch_build.py index a337489707b2f..626156663a6ae 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -216,10 +216,10 @@ # Make sure to upgrade the mypy version in update-common-sql-api-stubs in .pre-commit-config.yaml # when you upgrade it here !!!! "mypy==1.9.0", - "types-Deprecated", - "types-Markdown", - "types-PyMySQL", - "types-PyYAML", + "types-Deprecated>=1.2.9.20240311", + "types-Markdown>=3.6.0.20240316", + "types-PyMySQL>=1.1.0.20240425", + "types-PyYAML>=6.0.12.20240724", "types-aiofiles", "types-certifi", "types-croniter", From 8a2943a611aae3d66fdc03e1149be385ccfcef9d Mon Sep 17 00:00:00 2001 From: Ash Berlin-Taylor Date: Fri, 25 Oct 2024 16:35:26 +0100 Subject: [PATCH 115/258] Ensure that logging tests don't depend on how pytest is run (#43380) If you use the `-s`/`--capture=no` then stdout is a tty, and the tests start failing as the `record.args` gets modified to contain color escape codes `pytest tests/models/test_dag.py::test_iter_dagrun_infos_between_error` passed `pytest -s tests/models/test_dag.py::test_iter_dagrun_infos_between_error` failed This uses the existing helpers we've got to ensure that it always passes --- tests/models/test_dag.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/models/test_dag.py b/tests/models/test_dag.py index c1f03b3d7a548..86e499edb3bd7 100644 --- a/tests/models/test_dag.py +++ b/tests/models/test_dag.py @@ -2174,6 +2174,7 @@ def test_next_dagrun_info_timedelta_schedule_and_catchup_true(self): assert next_info assert next_info.logical_date == timezone.datetime(2020, 5, 4) + @pytest.mark.usefixtures("clear_all_logger_handlers") def test_next_dagrun_info_timetable_exception(self, caplog): """Test the DAG does not crash the scheduler if the timetable raises an exception.""" @@ -3257,6 +3258,7 @@ def test_iter_dagrun_infos_between(start_date, expected_infos): assert expected_infos == list(iterator) +@pytest.mark.usefixtures("clear_all_logger_handlers") def test_iter_dagrun_infos_between_error(caplog): start = pendulum.instance(DEFAULT_DATE - datetime.timedelta(hours=1)) end = pendulum.instance(DEFAULT_DATE) From 2e9c549824d932b88812f9db91ac93698c48c18a Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Fri, 25 Oct 2024 11:53:38 -0400 Subject: [PATCH 116/258] Fix accessibility with sorting tables and zIndex with tags selector (#43365) --- .../ui/src/components/DataTable/CardList.tsx | 16 +--- .../ui/src/components/DataTable/TableList.tsx | 79 ++++++++++++------- airflow/ui/src/pages/DagsList/DagsFilters.tsx | 4 + 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/airflow/ui/src/components/DataTable/CardList.tsx b/airflow/ui/src/components/DataTable/CardList.tsx index ddebff81b2495..e6397a94d1db5 100644 --- a/airflow/ui/src/components/DataTable/CardList.tsx +++ b/airflow/ui/src/components/DataTable/CardList.tsx @@ -17,26 +17,19 @@ * under the License. */ import { Box, SimpleGrid, Skeleton } from "@chakra-ui/react"; -import { - type CoreRow, - flexRender, - type Table as TanStackTable, -} from "@tanstack/react-table"; -import type { SyntheticEvent } from "react"; +import { flexRender, type Table as TanStackTable } from "@tanstack/react-table"; import type { CardDef } from "./types"; type DataTableProps = { readonly cardDef: CardDef; readonly isLoading?: boolean; - readonly onRowClick?: (e: SyntheticEvent, row: CoreRow) => void; readonly table: TanStackTable; }; export const CardList = ({ cardDef, isLoading, - onRowClick, table, }: DataTableProps) => { const defaultGridProps = { column: { base: 1 }, spacing: 2 }; @@ -45,12 +38,7 @@ export const CardList = ({ {table.getRowModel().rows.map((row) => ( - onRowClick(event, row) : undefined} - title={onRowClick ? "View details" : undefined} - > + {Boolean(isLoading) && (cardDef.meta?.customSkeleton ?? ( ({ ({ colSpan, column, getContext, id, isPlaceholder }) => { const sort = column.getIsSorted(); const canSort = column.getCanSort(); + const text = flexRender(column.columnDef.header, getContext()); - return ( - - {isPlaceholder ? undefined : ( - <>{flexRender(column.columnDef.header, getContext())} - )} - {canSort && sort === false ? ( + let rightIcon; + + if (canSort) { + if (sort === "desc") { + rightIcon = ( + + ); + } else if (sort === "asc") { + rightIcon = ( + + ); + } else { + rightIcon = ( - ) : undefined} - {canSort && sort !== false ? ( - sort === "desc" ? ( - - ) : ( - - ) - ) : undefined} + ); + } + + return ( + + {isPlaceholder ? undefined : ( + + )} + + ); + } + + return ( + + {isPlaceholder ? undefined : text} ); }, diff --git a/airflow/ui/src/pages/DagsList/DagsFilters.tsx b/airflow/ui/src/pages/DagsList/DagsFilters.tsx index f2b1baae8610f..0b2ff79fb0e0c 100644 --- a/airflow/ui/src/pages/DagsList/DagsFilters.tsx +++ b/airflow/ui/src/pages/DagsList/DagsFilters.tsx @@ -163,6 +163,10 @@ export const DagsFilters = () => { ...provided, minWidth: 64, }), + menu: (provided) => ({ + ...provided, + zIndex: 2, + }), }} isClearable isMulti From 2c394e3c85d77a3a0331687186dfcee89e286035 Mon Sep 17 00:00:00 2001 From: David Blain Date: Fri, 25 Oct 2024 18:41:17 +0200 Subject: [PATCH 117/258] Feature: Added fast_executemany parameter to insert_rows of DbApiHook (#43357) * refactor: Added the fast_executemany parameter to the insert_rows method of the DbApiHook * refactor: Added unit test using the fast_executemany parameter in the DbApiHook * docs: Put fast_executemany and executemany between single quotes to avoid spelling check --------- Co-authored-by: David Blain --- .../airflow/providers/common/sql/hooks/sql.py | 12 ++++++++++++ providers/tests/common/sql/hooks/test_dbapi.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/providers/src/airflow/providers/common/sql/hooks/sql.py b/providers/src/airflow/providers/common/sql/hooks/sql.py index afb66ddd13a0b..60c659e340feb 100644 --- a/providers/src/airflow/providers/common/sql/hooks/sql.py +++ b/providers/src/airflow/providers/common/sql/hooks/sql.py @@ -620,6 +620,7 @@ def insert_rows( replace=False, *, executemany=False, + fast_executemany=False, autocommit=False, **kwargs, ): @@ -638,6 +639,8 @@ def insert_rows( :param executemany: If True, all rows are inserted at once in chunks defined by the commit_every parameter. This only works if all rows have same number of column names, but leads to better performance. + :param fast_executemany: If True, the `fast_executemany` parameter will be set on the + cursor used by `executemany` which leads to better performance, if supported by driver. :param autocommit: What to set the connection's autocommit setting to before executing the query. """ @@ -646,6 +649,15 @@ def insert_rows( conn.commit() with closing(conn.cursor()) as cur: if self.supports_executemany or executemany: + if fast_executemany: + with contextlib.suppress(AttributeError): + # Try to set the fast_executemany attribute + cur.fast_executemany = True + self.log.info( + "Fast_executemany is enabled for conn_id '%s'!", + self.get_conn_id(), + ) + for chunked_rows in chunked(rows, commit_every): values = list( map( diff --git a/providers/tests/common/sql/hooks/test_dbapi.py b/providers/tests/common/sql/hooks/test_dbapi.py index d94b817eeeb82..a47b2856eb2fa 100644 --- a/providers/tests/common/sql/hooks/test_dbapi.py +++ b/providers/tests/common/sql/hooks/test_dbapi.py @@ -49,6 +49,7 @@ class TestDbApiHook: def setup_method(self, **kwargs): self.cur = mock.MagicMock( rowcount=0, + fast_executemany=False, spec=Cursor, ) self.conn = mock.MagicMock() @@ -188,6 +189,21 @@ def test_insert_rows_executemany(self): self.db_hook.insert_rows(table, rows, executemany=True) assert self.conn.close.call_count == 1 + assert not self.cur.fast_executemany + assert self.cur.close.call_count == 1 + assert self.conn.commit.call_count == 2 + + sql = f"INSERT INTO {table} VALUES (%s)" + self.cur.executemany.assert_any_call(sql, rows) + + def test_insert_rows_fast_executemany(self): + table = "table" + rows = [("hello",), ("world",)] + + self.db_hook.insert_rows(table, rows, executemany=True, fast_executemany=True) + + assert self.conn.close.call_count == 1 + assert self.cur.fast_executemany assert self.cur.close.call_count == 1 assert self.conn.commit.call_count == 2 From 5abdb7bab7821b6f9d39eebdb006710a14874df6 Mon Sep 17 00:00:00 2001 From: perry2of5 Date: Fri, 25 Oct 2024 10:24:10 -0700 Subject: [PATCH 118/258] Provide default for EXCLUDED_PROVIDERS lookup. (#43385) The current code throws a KeyError when the environment variable EXCLUDED_PROVIDERS is not defind. This commit adds an empty map as a default to prevent this failure. --- scripts/in_container/is_provider_excluded.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/in_container/is_provider_excluded.py b/scripts/in_container/is_provider_excluded.py index 2eac93dac065b..fe0e0da4871b6 100755 --- a/scripts/in_container/is_provider_excluded.py +++ b/scripts/in_container/is_provider_excluded.py @@ -23,7 +23,7 @@ from in_container_utils import console if __name__ == "__main__": - excluded_providers = json.loads(os.environ["EXCLUDED_PROVIDERS"]) + excluded_providers = json.loads(os.environ.get("EXCLUDED_PROVIDERS", "{}")) extra = os.environ["EXTRA"] console.print(f"[bright_blue]Check if provider {extra} is excluded in {excluded_providers}") python_version = os.environ["PYTHON_MAJOR_MINOR_VERSION"] From 5294276b48e49ab37ba9261f4e17c28be85e633b Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Fri, 25 Oct 2024 14:41:36 -0400 Subject: [PATCH 119/258] Add ability to select a dag from dags list (#43324) Fix event propagation and onRowClicked type Simplify error page Remove div links and only show error stack trace in dev useLocalStorage hook instead of manualy manipulation Remove deserializer --- airflow/ui/package.json | 3 +- airflow/ui/pnpm-lock.yaml | 19 +++ airflow/ui/src/App.test.tsx | 124 ------------------ .../src/context/timezone/TimezoneProvider.tsx | 26 ++-- airflow/ui/src/layouts/BaseLayout.tsx | 5 +- airflow/ui/src/main.tsx | 21 ++- .../{App.tsx => pages/DagsList/Dag/Dag.tsx} | 19 +-- airflow/ui/src/pages/DagsList/Dag/index.tsx | 20 +++ airflow/ui/src/pages/DagsList/DagCard.tsx | 16 ++- airflow/ui/src/pages/DagsList/DagsList.tsx | 22 +++- airflow/ui/src/pages/Error.tsx | 99 ++++++++++++++ airflow/ui/src/router.tsx | 54 ++++++++ airflow/ui/src/theme.ts | 5 - 13 files changed, 250 insertions(+), 183 deletions(-) delete mode 100644 airflow/ui/src/App.test.tsx rename airflow/ui/src/{App.tsx => pages/DagsList/Dag/Dag.tsx} (66%) create mode 100644 airflow/ui/src/pages/DagsList/Dag/index.tsx create mode 100644 airflow/ui/src/pages/Error.tsx create mode 100644 airflow/ui/src/router.tsx diff --git a/airflow/ui/package.json b/airflow/ui/package.json index 3ca8d1a06f412..97f3a2ee63e31 100644 --- a/airflow/ui/package.json +++ b/airflow/ui/package.json @@ -30,7 +30,8 @@ "react-dom": "^18.3.1", "react-icons": "^5.3.0", "react-router-dom": "^6.26.2", - "use-debounce": "^10.0.3" + "use-debounce": "^10.0.3", + "usehooks-ts": "^3.1.0" }, "devDependencies": { "@7nohe/openapi-react-query-codegen": "^1.6.0", diff --git a/airflow/ui/pnpm-lock.yaml b/airflow/ui/pnpm-lock.yaml index 3ceee513bb134..45858f664a62c 100644 --- a/airflow/ui/pnpm-lock.yaml +++ b/airflow/ui/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: use-debounce: specifier: ^10.0.3 version: 10.0.3(react@18.3.1) + usehooks-ts: + specifier: ^3.1.0 + version: 3.1.0(react@18.3.1) devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.0 @@ -2464,6 +2467,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3255,6 +3261,12 @@ packages: '@types/react': optional: true + usehooks-ts@3.1.0: + resolution: {integrity: sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -6113,6 +6125,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -6927,6 +6941,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.5 + usehooks-ts@3.1.0(react@18.3.1): + dependencies: + lodash.debounce: 4.0.8 + react: 18.3.1 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 diff --git a/airflow/ui/src/App.test.tsx b/airflow/ui/src/App.test.tsx deleted file mode 100644 index 38b90d1c4983c..0000000000000 --- a/airflow/ui/src/App.test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/*! - * 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. - */ -import type { QueryObserverSuccessResult } from "@tanstack/react-query"; -import { render } from "@testing-library/react"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; - -import * as openapiQueriesModule from "openapi/queries"; -import type { DAGCollectionResponse } from "openapi/requests/types.gen"; - -import { App } from "./App"; -import { Wrapper } from "./utils/Wrapper"; - -// The null fields actually have to be null instead of undefined -/* eslint-disable unicorn/no-null */ - -const mockListDags: DAGCollectionResponse = { - dags: [ - { - dag_display_name: "nested_groups", - dag_id: "nested_groups", - default_view: "grid", - description: null, - file_token: - "Ii9maWxlcy9kYWdzL25lc3RlZF90YXNrX2dyb3Vwcy5weSI.G3EkdxmDUDQsVb7AIZww1TSGlFE", - fileloc: "/files/dags/nested_task_groups.py", - has_import_errors: false, - has_task_concurrency_limits: false, - is_active: true, - is_paused: false, - last_expired: null, - last_parsed_time: "2024-08-22T13:50:10.372238+00:00", - last_pickled: null, - max_active_runs: 16, - max_active_tasks: 16, - max_consecutive_failed_dag_runs: 0, - next_dagrun: "2024-08-22T00:00:00+00:00", - next_dagrun_create_after: "2024-08-23T00:00:00+00:00", - next_dagrun_data_interval_end: "2024-08-23T00:00:00+00:00", - next_dagrun_data_interval_start: "2024-08-22T00:00:00+00:00", - owners: ["airflow"], - pickle_id: null, - scheduler_lock: null, - tags: [], - timetable_description: "", - timetable_summary: "", - }, - { - dag_display_name: "simple_bash_operator", - dag_id: "simple_bash_operator", - default_view: "grid", - description: null, - file_token: - "Ii9maWxlcy9kYWdzL3NpbXBsZV9iYXNoX29wZXJhdG9yLnB5Ig.RteaxTC78ceHlgMkfU3lfznlcLI", - fileloc: "/files/dags/simple_bash_operator.py", - has_import_errors: false, - has_task_concurrency_limits: false, - is_active: true, - is_paused: false, - last_expired: null, - last_parsed_time: "2024-08-22T13:50:10.368561+00:00", - last_pickled: null, - max_active_runs: 16, - max_active_tasks: 16, - max_consecutive_failed_dag_runs: 0, - next_dagrun: "2024-08-22T00:00:00+00:00", - next_dagrun_create_after: "2024-08-23T00:00:00+00:00", - next_dagrun_data_interval_end: "2024-08-23T00:00:00+00:00", - next_dagrun_data_interval_start: "2024-08-22T00:00:00+00:00", - owners: ["airflow"], - pickle_id: null, - scheduler_lock: null, - tags: [ - { - dag_id: "dag", - name: "example2", - }, - { - dag_id: "dag", - name: "example", - }, - ], - timetable_description: "At 00:00", - timetable_summary: "sum", - }, - ], - total_entries: 2, -}; - -beforeEach(() => { - const returnValue = { - data: mockListDags, - isLoading: false, - } as QueryObserverSuccessResult; - - vi.spyOn(openapiQueriesModule, "useDagServiceGetDags").mockImplementation( - () => returnValue, - ); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -describe("App", () => { - it("App component should render", () => { - render(, { wrapper: Wrapper }); - }); -}); diff --git a/airflow/ui/src/context/timezone/TimezoneProvider.tsx b/airflow/ui/src/context/timezone/TimezoneProvider.tsx index dfe40f6976706..74c0c2462b390 100644 --- a/airflow/ui/src/context/timezone/TimezoneProvider.tsx +++ b/airflow/ui/src/context/timezone/TimezoneProvider.tsx @@ -16,12 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { - createContext, - useState, - useMemo, - type PropsWithChildren, -} from "react"; +import { createContext, useMemo, type PropsWithChildren } from "react"; +import { useLocalStorage } from "usehooks-ts"; export type TimezoneContextType = { selectedTimezone: string; @@ -35,20 +31,14 @@ export const TimezoneContext = createContext( const TIMEZONE_KEY = "timezone"; export const TimezoneProvider = ({ children }: PropsWithChildren) => { - const [selectedTimezone, setSelectedTimezone] = useState(() => { - const timezone = localStorage.getItem(TIMEZONE_KEY); - - return timezone ?? "UTC"; - }); - - const selectTimezone = (tz: string) => { - localStorage.setItem(TIMEZONE_KEY, tz); - setSelectedTimezone(tz); - }; + const [selectedTimezone, setSelectedTimezone] = useLocalStorage( + TIMEZONE_KEY, + "UTC", + ); const value = useMemo( - () => ({ selectedTimezone, setSelectedTimezone: selectTimezone }), - [selectedTimezone], + () => ({ selectedTimezone, setSelectedTimezone }), + [selectedTimezone, setSelectedTimezone], ); return ( diff --git a/airflow/ui/src/layouts/BaseLayout.tsx b/airflow/ui/src/layouts/BaseLayout.tsx index 4aa7a74de6fc1..c0d1b6c869bd4 100644 --- a/airflow/ui/src/layouts/BaseLayout.tsx +++ b/airflow/ui/src/layouts/BaseLayout.tsx @@ -17,15 +17,16 @@ * under the License. */ import { Box } from "@chakra-ui/react"; +import type { PropsWithChildren } from "react"; import { Outlet } from "react-router-dom"; import { Nav } from "./Nav"; -export const BaseLayout = () => ( +export const BaseLayout = ({ children }: PropsWithChildren) => ( <>

>4r|5-)^ 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%0AKb#tePn;G!E3Ovw~lKLXf=-IhRp&H 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