This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new b979debb51 AIP-84 Patch Variable (#42929)
b979debb51 is described below
commit b979debb511119ba4a0ee709e1b2809d4041dad3
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Oct 14 23:08:15 2024 +0800
AIP-84 Patch Variable (#42929)
---
.../api_connexion/endpoints/variable_endpoint.py | 1 +
airflow/api_fastapi/openapi/v1-generated.yaml | 94 +++++++++++++++++++++-
airflow/api_fastapi/serializers/dags.py | 2 +-
airflow/api_fastapi/serializers/variables.py | 17 +++-
airflow/api_fastapi/views/public/variables.py | 28 ++++++-
airflow/ui/openapi-gen/queries/common.ts | 3 +
airflow/ui/openapi-gen/queries/queries.ts | 49 ++++++++++-
airflow/ui/openapi-gen/requests/schemas.gen.ts | 41 +++++++++-
airflow/ui/openapi-gen/requests/services.gen.ts | 36 +++++++++
airflow/ui/openapi-gen/requests/types.gen.ts | 50 +++++++++++-
tests/api_fastapi/views/public/test_variables.py | 88 ++++++++++++++++++++
11 files changed, 394 insertions(+), 15 deletions(-)
diff --git a/airflow/api_connexion/endpoints/variable_endpoint.py
b/airflow/api_connexion/endpoints/variable_endpoint.py
index 8efddb5841..b1d9e2f5c8 100644
--- a/airflow/api_connexion/endpoints/variable_endpoint.py
+++ b/airflow/api_connexion/endpoints/variable_endpoint.py
@@ -95,6 +95,7 @@ def get_variables(
)
+@mark_fastapi_migration_done
@security.requires_access_variable("PUT")
@provide_session
@action_logging(
diff --git a/airflow/api_fastapi/openapi/v1-generated.yaml
b/airflow/api_fastapi/openapi/v1-generated.yaml
index 6c35ca212a..759ab7fdd8 100644
--- a/airflow/api_fastapi/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/openapi/v1-generated.yaml
@@ -629,6 +629,72 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ patch:
+ tags:
+ - Variable
+ summary: Patch Variable
+ description: Update a variable by key.
+ operationId: patch_variable
+ parameters:
+ - name: variable_key
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Variable Key
+ - 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/VariableBody'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VariableResponse'
+ '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/dags/{dag_id}/dagRuns/{dag_run_id}:
get:
tags:
@@ -1045,7 +1111,7 @@ components:
required:
- is_paused
title: DAGPatchBody
- description: Dag Serializer for updatable body.
+ description: Dag Serializer for updatable bodies.
DAGResponse:
properties:
dag_id:
@@ -1492,25 +1558,47 @@ components:
- msg
- type
title: ValidationError
- VariableResponse:
+ VariableBody:
properties:
key:
type: string
title: Key
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
value:
anyOf:
- type: string
- type: 'null'
title: Value
+ type: object
+ required:
+ - key
+ - description
+ - value
+ title: VariableBody
+ description: Variable serializer for bodies.
+ VariableResponse:
+ properties:
+ key:
+ type: string
+ title: Key
description:
anyOf:
- type: string
- type: 'null'
title: Description
+ value:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Value
type: object
required:
- key
- - value
- description
+ - value
title: VariableResponse
description: Variable serializer for responses.
diff --git a/airflow/api_fastapi/serializers/dags.py
b/airflow/api_fastapi/serializers/dags.py
index 9879badf25..c9d48aac22 100644
--- a/airflow/api_fastapi/serializers/dags.py
+++ b/airflow/api_fastapi/serializers/dags.py
@@ -95,7 +95,7 @@ class DAGResponse(BaseModel):
class DAGPatchBody(BaseModel):
- """Dag Serializer for updatable body."""
+ """Dag Serializer for updatable bodies."""
is_paused: bool
diff --git a/airflow/api_fastapi/serializers/variables.py
b/airflow/api_fastapi/serializers/variables.py
index ded268432b..1ecc87425a 100644
--- a/airflow/api_fastapi/serializers/variables.py
+++ b/airflow/api_fastapi/serializers/variables.py
@@ -25,15 +25,20 @@ from typing_extensions import Self
from airflow.utils.log.secrets_masker import redact
-class VariableResponse(BaseModel):
- """Variable serializer for responses."""
+class VariableBase(BaseModel):
+ """Base Variable serializer."""
model_config = ConfigDict(populate_by_name=True)
key: str
- val: str | None = Field(alias="value")
description: str | None
+
+class VariableResponse(VariableBase):
+ """Variable serializer for responses."""
+
+ val: str | None = Field(alias="value")
+
@model_validator(mode="after")
def redact_val(self) -> Self:
if self.val is None:
@@ -47,3 +52,9 @@ class VariableResponse(BaseModel):
# value is not a serialized string representation of a dict.
self.val = redact(self.val, self.key)
return self
+
+
+class VariableBody(VariableBase):
+ """Variable serializer for bodies."""
+
+ value: str | None
diff --git a/airflow/api_fastapi/views/public/variables.py
b/airflow/api_fastapi/views/public/variables.py
index e6cbb136f1..b4c07e23de 100644
--- a/airflow/api_fastapi/views/public/variables.py
+++ b/airflow/api_fastapi/views/public/variables.py
@@ -16,14 +16,14 @@
# under the License.
from __future__ import annotations
-from fastapi import Depends, HTTPException
+from fastapi import Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.orm import Session
from typing_extensions import Annotated
from airflow.api_fastapi.db.common import get_session
from airflow.api_fastapi.openapi.exceptions import
create_openapi_http_exception_doc
-from airflow.api_fastapi.serializers.variables import VariableResponse
+from airflow.api_fastapi.serializers.variables import VariableBody,
VariableResponse
from airflow.api_fastapi.views.router import AirflowRouter
from airflow.models.variable import Variable
@@ -56,3 +56,27 @@ async def get_variable(
raise HTTPException(404, f"The Variable with key: `{variable_key}` was
not found")
return VariableResponse.model_validate(variable, from_attributes=True)
+
+
+@variables_router.patch("/{variable_key}",
responses=create_openapi_http_exception_doc([400, 401, 403, 404]))
+async def patch_variable(
+ variable_key: str,
+ patch_body: VariableBody,
+ session: Annotated[Session, Depends(get_session)],
+ update_mask: list[str] | None = Query(None),
+) -> VariableResponse:
+ """Update a variable by key."""
+ if patch_body.key != variable_key:
+ raise HTTPException(400, "Invalid body, key from request body doesn't
match uri parameter")
+ non_update_fields = {"key"}
+ variable =
session.scalar(select(Variable).filter_by(key=variable_key).limit(1))
+ if not variable:
+ raise HTTPException(404, f"The Variable with key: `{variable_key}` was
not found")
+ if update_mask:
+ data = patch_body.dict(include=set(update_mask) - non_update_fields)
+ else:
+ data = patch_body.dict(exclude=non_update_fields)
+ for key, val in data.items():
+ setattr(variable, key, val)
+ session.add(variable)
+ return variable
diff --git a/airflow/ui/openapi-gen/queries/common.ts
b/airflow/ui/openapi-gen/queries/common.ts
index 393fad520a..e3c0ef3ab4 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -191,6 +191,9 @@ export type DagServicePatchDagsMutationResult = Awaited<
export type DagServicePatchDagMutationResult = Awaited<
ReturnType<typeof DagService.patchDag>
>;
+export type VariableServicePatchVariableMutationResult = Awaited<
+ ReturnType<typeof VariableService.patchVariable>
+>;
export type ConnectionServiceDeleteConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.deleteConnection>
>;
diff --git a/airflow/ui/openapi-gen/queries/queries.ts
b/airflow/ui/openapi-gen/queries/queries.ts
index 01e919cce5..b4c8cf9fea 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -14,7 +14,7 @@ import {
DashboardService,
VariableService,
} from "../requests/services.gen";
-import { DAGPatchBody, DagRunState } from "../requests/types.gen";
+import { DAGPatchBody, DagRunState, VariableBody } from
"../requests/types.gen";
import * as Common from "./common";
/**
@@ -428,6 +428,53 @@ export const useDagServicePatchDag = <
}) as unknown as Promise<TData>,
...options,
});
+/**
+ * Patch Variable
+ * Update a variable by key.
+ * @param data The data for the request.
+ * @param data.variableKey
+ * @param data.requestBody
+ * @param data.updateMask
+ * @returns VariableResponse Successful Response
+ * @throws ApiError
+ */
+export const useVariableServicePatchVariable = <
+ TData = Common.VariableServicePatchVariableMutationResult,
+ TError = unknown,
+ TContext = unknown,
+>(
+ options?: Omit<
+ UseMutationOptions<
+ TData,
+ TError,
+ {
+ requestBody: VariableBody;
+ updateMask?: string[];
+ variableKey: string;
+ },
+ TContext
+ >,
+ "mutationFn"
+ >,
+) =>
+ useMutation<
+ TData,
+ TError,
+ {
+ requestBody: VariableBody;
+ updateMask?: string[];
+ variableKey: string;
+ },
+ TContext
+ >({
+ mutationFn: ({ requestBody, updateMask, variableKey }) =>
+ VariableService.patchVariable({
+ requestBody,
+ updateMask,
+ variableKey,
+ }) as unknown as Promise<TData>,
+ ...options,
+ });
/**
* Delete Connection
* Delete a connection entry.
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 18df528465..e42a3f6572 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -526,7 +526,7 @@ export const $DAGPatchBody = {
type: "object",
required: ["is_paused"],
title: "DAGPatchBody",
- description: "Dag Serializer for updatable body.",
+ description: "Dag Serializer for updatable bodies.",
} as const;
export const $DAGResponse = {
@@ -1182,12 +1182,23 @@ export const $ValidationError = {
title: "ValidationError",
} as const;
-export const $VariableResponse = {
+export const $VariableBody = {
properties: {
key: {
type: "string",
title: "Key",
},
+ description: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Description",
+ },
value: {
anyOf: [
{
@@ -1199,6 +1210,19 @@ export const $VariableResponse = {
],
title: "Value",
},
+ },
+ type: "object",
+ required: ["key", "description", "value"],
+ title: "VariableBody",
+ description: "Variable serializer for bodies.",
+} as const;
+
+export const $VariableResponse = {
+ properties: {
+ key: {
+ type: "string",
+ title: "Key",
+ },
description: {
anyOf: [
{
@@ -1210,9 +1234,20 @@ export const $VariableResponse = {
],
title: "Description",
},
+ value: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Value",
+ },
},
type: "object",
- required: ["key", "value", "description"],
+ required: ["key", "description", "value"],
title: "VariableResponse",
description: "Variable 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 f0cbc09937..43d9e8d940 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -25,6 +25,8 @@ import type {
DeleteVariableResponse,
GetVariableData,
GetVariableResponse,
+ PatchVariableData,
+ PatchVariableResponse,
GetDagRunData,
GetDagRunResponse,
DeleteDagRunData,
@@ -364,6 +366,40 @@ export class VariableService {
},
});
}
+
+ /**
+ * Patch Variable
+ * Update a variable by key.
+ * @param data The data for the request.
+ * @param data.variableKey
+ * @param data.requestBody
+ * @param data.updateMask
+ * @returns VariableResponse Successful Response
+ * @throws ApiError
+ */
+ public static patchVariable(
+ data: PatchVariableData,
+ ): CancelablePromise<PatchVariableResponse> {
+ return __request(OpenAPI, {
+ method: "PATCH",
+ url: "/public/variables/{variable_key}",
+ path: {
+ variable_key: data.variableKey,
+ },
+ 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",
+ },
+ });
+ }
}
export class DagRunService {
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow/ui/openapi-gen/requests/types.gen.ts
index 5fe3615c7d..3deba451fc 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -79,7 +79,7 @@ export type DAGDetailsResponse = {
};
/**
- * Dag Serializer for updatable body.
+ * Dag Serializer for updatable bodies.
*/
export type DAGPatchBody = {
is_paused: boolean;
@@ -250,13 +250,22 @@ export type ValidationError = {
type: string;
};
+/**
+ * Variable serializer for bodies.
+ */
+export type VariableBody = {
+ key: string;
+ description: string | null;
+ value: string | null;
+};
+
/**
* Variable serializer for responses.
*/
export type VariableResponse = {
key: string;
- value: string | null;
description: string | null;
+ value: string | null;
};
export type NextRunAssetsData = {
@@ -348,6 +357,14 @@ export type GetVariableData = {
export type GetVariableResponse = VariableResponse;
+export type PatchVariableData = {
+ requestBody: VariableBody;
+ updateMask?: Array<string> | null;
+ variableKey: string;
+};
+
+export type PatchVariableResponse = VariableResponse;
+
export type GetDagRunData = {
dagId: string;
dagRunId: string;
@@ -637,6 +654,35 @@ export type $OpenApiTs = {
422: HTTPValidationError;
};
};
+ patch: {
+ req: PatchVariableData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: VariableResponse;
+ /**
+ * Bad Request
+ */
+ 400: HTTPExceptionResponse;
+ /**
+ * Unauthorized
+ */
+ 401: HTTPExceptionResponse;
+ /**
+ * Forbidden
+ */
+ 403: HTTPExceptionResponse;
+ /**
+ * Not Found
+ */
+ 404: HTTPExceptionResponse;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
};
"/public/dags/{dag_id}/dagRuns/{dag_run_id}": {
get: {
diff --git a/tests/api_fastapi/views/public/test_variables.py
b/tests/api_fastapi/views/public/test_variables.py
index 7957d0bf22..cf5c78fd56 100644
--- a/tests/api_fastapi/views/public/test_variables.py
+++ b/tests/api_fastapi/views/public/test_variables.py
@@ -135,3 +135,91 @@ class TestGetVariable(TestVariableEndpoint):
assert response.status_code == 404
body = response.json()
assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found"
== body["detail"]
+
+
+class TestPatchVariable(TestVariableEndpoint):
+ @pytest.mark.enable_redact
+ @pytest.mark.parametrize(
+ "key, body, params, expected_response",
+ [
+ (
+ TEST_VARIABLE_KEY,
+ {
+ "key": TEST_VARIABLE_KEY,
+ "value": "The new value",
+ "description": "The new description",
+ },
+ None,
+ {
+ "key": TEST_VARIABLE_KEY,
+ "value": "The new value",
+ "description": "The new description",
+ },
+ ),
+ (
+ TEST_VARIABLE_KEY,
+ {
+ "key": TEST_VARIABLE_KEY,
+ "value": "The new value",
+ "description": "The new description",
+ },
+ {"update_mask": ["value"]},
+ {
+ "key": TEST_VARIABLE_KEY,
+ "value": "The new value",
+ "description": TEST_VARIABLE_DESCRIPTION,
+ },
+ ),
+ (
+ TEST_VARIABLE_KEY2,
+ {
+ "key": TEST_VARIABLE_KEY2,
+ "value": "some_other_value",
+ "description": TEST_VARIABLE_DESCRIPTION2,
+ },
+ None,
+ {
+ "key": TEST_VARIABLE_KEY2,
+ "value": "***",
+ "description": TEST_VARIABLE_DESCRIPTION2,
+ },
+ ),
+ (
+ TEST_VARIABLE_KEY3,
+ {
+ "key": TEST_VARIABLE_KEY3,
+ "value": '{"password": "new_password"}',
+ "description": "new description",
+ },
+ {"update_mask": ["value", "description"]},
+ {
+ "key": TEST_VARIABLE_KEY3,
+ "value": '{"password": "***"}',
+ "description": "new description",
+ },
+ ),
+ ],
+ )
+ def test_patch_should_respond_200(self, test_client, session, key, body,
params, expected_response):
+ self.create_variable()
+ response = test_client.patch(f"/public/variables/{key}", json=body,
params=params)
+ assert response.status_code == 200
+ assert response.json() == expected_response
+
+ def test_patch_should_respond_400(self, test_client):
+ response = test_client.patch(
+ f"/public/variables/{TEST_VARIABLE_KEY}",
+ json={"key": "different_key", "value": "some_value",
"description": None},
+ )
+ assert response.status_code == 400
+ body = response.json()
+ assert "Invalid body, key from request body doesn't match uri
parameter" == body["detail"]
+
+ def test_patch_should_respond_404(self, test_client):
+ response = test_client.patch(
+ f"/public/variables/{TEST_VARIABLE_KEY}",
+ json={"key": TEST_VARIABLE_KEY, "value": "some_value",
"description": None},
+ )
+ assert response.status_code == 404
+ body = response.json()
+ assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found"
== body["detail"]