Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all \x3Cpre>\x3Ccode> blocks (function() { function addCopyButtons() { document.querySelectorAll('pre code').forEach(function(codeBlock) { if (codeBlock.parentElement.hasAttribute('data-copy-added')) return; codeBlock.parentElement.setAttribute('data-copy-added', 'true'); var btn = document.createElement('button'); btn.textContent = 'Copy'; btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;'; btn.onmouseover = function() { this.style.opacity = '1'; }; btn.onmouseout = function() { this.style.opacity = '0.7'; }; btn.onclick = function() { navigator.clipboard.writeText(codeBlock.textContent).then(function() { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); }); }; codeBlock.parentElement.style.position = 'relative'; codeBlock.parentElement.appendChild(btn); }); } addCopyButtons(); // Re-run on dynamic content var observer = new MutationObserver(addCopyButtons); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); AIP-84 Add ability to update dag run note in PATCH dag_run endpoint by rawwar · Pull Request #43508 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1318,9 +1318,9 @@ paths:
patch:
tags:
- DagRun
summary: Patch Dag Run State
summary: Patch Dag Run
description: Modify a DAG Run.
operationId: patch_dag_run_state
operationId: patch_dag_run
parameters:
- name: dag_id
in: path
Expand DownExpand Up@@ -3694,10 +3694,16 @@ components:
DAGRunPatchBody:
properties:
state:
$ref: '#/components/schemas/DAGRunPatchStates'
anyOf:
- $ref: '#/components/schemas/DAGRunPatchStates'
- type: 'null'
note:
anyOf:
- type: string
maxLength: 1000
- type: 'null'
title: Note
type: object
required:
- state
title: DAGRunPatchBody
description: DAG Run Serializer for PATCH requests.
DAGRunPatchStates:
Expand Down
34 changes: 20 additions & 14 deletions airflow/api_fastapi/core_api/routes/public/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ def delete_dag_run(dag_id: str, dag_run_id: str, session: Annotated[Session, Dep
]
),
)
def patch_dag_run_state(
def patch_dag_run(
dag_id: str,
dag_run_id: str,
patch_body: DAGRunPatchBody,
Expand All@@ -121,23 +121,29 @@ def patch_dag_run_state(
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")

if update_mask:
if update_mask != ["state"]:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Only `state` field can be updated through the REST API"
)
data = patch_body.model_dump(include=set(update_mask))
else:
update_mask = ["state"]
data = patch_body.model_dump()

for attr_namein update_mask:
for attr_name, attr_value in data.items():
if attr_name == "state":
state = getattr(patch_body, attr_name)
if state == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True)
elif state == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True)
attr_value = getattr(patch_body, "state")
if attr_value == DAGRunPatchStates.SUCCESS:
set_dag_run_state_to_success(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.QUEUED:
set_dag_run_state_to_queued(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_value == DAGRunPatchStates.FAILED:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True, session=session)
elif attr_name == "note":
# Once Authentication is implemented in this FastAPI app,
# user id will be added when updating dag run note
# Refer to https://github.com/apache/airflow/issues/43534
dag_run = session.get(DagRun, dag_run.id)
if dag_run.dag_run_note is None:
dag_run.note = (attr_value, None)
else:
set_dag_run_state_to_failed(dag=dag, run_id=dag_run.run_id, commit=True)
dag_run.dag_run_note.content = attr_value
Comment thread
pierrejeambrun marked this conversation as resolved.

session.refresh(dag_run)
dag_run = session.get(DagRun, dag_run.id)

return DAGRunResponse.model_validate(dag_run, from_attributes=True)
3 changes: 2 additions & 1 deletion airflow/api_fastapi/core_api/serializers/dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,8 @@ class DAGRunPatchStates(str, Enum):
class DAGRunPatchBody(BaseModel):
"""DAG Run Serializer for PATCH requests."""

state: DAGRunPatchStates
state: DAGRunPatchStates | None = None
note: str | None = Field(None, max_length=1000)


class DAGRunResponse(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions airflow/ui/openapi-gen/queries/common.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -935,8 +935,8 @@ export type DagServicePatchDagMutationResult = Awaited<
export type ConnectionServicePatchConnectionMutationResult = Awaited<
ReturnType<typeof ConnectionService.patchConnection>
>;
export type DagRunServicePatchDagRunStateMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRunState>
export type DagRunServicePatchDagRunMutationResult = Awaited<
ReturnType<typeof DagRunService.patchDagRun>
>;
export type PoolServicePatchPoolMutationResult = Awaited<
ReturnType<typeof PoolService.patchPool>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/openapi-gen/queries/queries.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1912,7 +1912,7 @@ export const useConnectionServicePatchConnection = <
...options,
});
/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -1922,8 +1922,8 @@ export const useConnectionServicePatchConnection = <
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
export const useDagRunServicePatchDagRunState = <
TData = Common.DagRunServicePatchDagRunStateMutationResult,
export const useDagRunServicePatchDagRun = <
TData = Common.DagRunServicePatchDagRunMutationResult,
TError = unknown,
TContext = unknown,
>(
Expand DownExpand Up@@ -1954,7 +1954,7 @@ export const useDagRunServicePatchDagRunState = <
TContext
>({
mutationFn: ({ dagId, dagRunId, requestBody, updateMask }) =>
DagRunService.patchDagRunState({
DagRunService.patchDagRun({
dagId,
dagRunId,
requestBody,
Expand Down
22 changes: 20 additions & 2 deletions airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -981,11 +981,29 @@ export const $DAGResponse = {
export const $DAGRunPatchBody = {
properties: {
state: {
$ref: "#/components/schemas/DAGRunPatchStates",
anyOf: [
{
$ref: "#/components/schemas/DAGRunPatchStates",
},
{
type: "null",
},
],
},
note: {
anyOf: [
{
type: "string",
maxLength: 1000,
},
{
type: "null",
},
],
title: "Note",
},
},
type: "object",
required: ["state"],
title: "DAGRunPatchBody",
description: "DAG Run Serializer for PATCH requests.",
} as const;
Expand Down
12 changes: 6 additions & 6 deletions airflow/ui/openapi-gen/requests/services.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,8 +49,8 @@ import type {
GetDagRunResponse,
DeleteDagRunData,
DeleteDagRunResponse,
PatchDagRunStateData,
PatchDagRunStateResponse,
PatchDagRunData,
PatchDagRunResponse,
GetDagSourceData,
GetDagSourceResponse,
GetEventLogData,
Expand DownExpand Up@@ -794,7 +794,7 @@ export class DagRunService {
}

/**
* Patch Dag Run State
* Patch Dag Run
* Modify a DAG Run.
* @param data The data for the request.
* @param data.dagId
Expand All@@ -804,9 +804,9 @@ export class DagRunService {
* @returns DAGRunResponse Successful Response
* @throws ApiError
*/
public static patchDagRunState(
data: PatchDagRunStateData,
): CancelablePromise<PatchDagRunStateResponse> {
public static patchDagRun(
data: PatchDagRunData,
): CancelablePromise<PatchDagRunResponse> {
return __request(OpenAPI, {
method: "PATCH",
url: "/public/dags/{dag_id}/dagRuns/{dag_run_id}",
Expand Down
9 changes: 5 additions & 4 deletions airflow/ui/openapi-gen/requests/types.gen.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,8 @@ export type DAGResponse = {
* DAG Run Serializer for PATCH requests.
*/
export type DAGRunPatchBody = {
state: DAGRunPatchStates;
state?: DAGRunPatchStates | null;
note?: string | null;
};

/**
Expand DownExpand Up@@ -932,14 +933,14 @@ export type DeleteDagRunData = {

export type DeleteDagRunResponse = void;

export type PatchDagRunStateData = {
export type PatchDagRunData = {
dagId: string;
dagRunId: string;
requestBody: DAGRunPatchBody;
updateMask?: Array<string> | null;
};

export type PatchDagRunStateResponse = DAGRunResponse;
export type PatchDagRunResponse = DAGRunResponse;

export type GetDagSourceData = {
accept?: string;
Expand DownExpand Up@@ -1775,7 +1776,7 @@ export type $OpenApiTs = {
};
};
patch: {
req: PatchDagRunStateData;
req: PatchDagRunData;
res: {
/**
* Successful Response
Expand Down
88 changes: 72 additions & 16 deletions tests/api_fastapi/core_api/routes/public/test_dag_run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@
DAG2_RUN2_TRIGGERED_BY = DagRunTriggeredByType.REST_API
START_DATE = datetime(2024, 6, 15, 0, 0, tzinfo=timezone.utc)
EXECUTION_DATE = datetime(2024, 6, 16, 0, 0, tzinfo=timezone.utc)
DAG1_NOTE = "test_note"
DAG1_RUN1_NOTE = "test_note"


@pytest.fixture(autouse=True)
Expand All@@ -66,13 +66,13 @@ def setup(dag_maker, session=None):
start_date=START_DATE,
):
EmptyOperator(task_id="task_1")
dag1 = dag_maker.create_dagrun(
dag_run1 = dag_maker.create_dagrun(
run_id=DAG1_RUN1_ID,
state=DAG1_RUN1_STATE,
run_type=DAG1_RUN1_RUN_TYPE,
triggered_by=DAG1_RUN1_TRIGGERED_BY,
)
dag1.note = (DAG1_NOTE, 1)
dag_run1.note = (DAG1_RUN1_NOTE, 1)

dag_maker.create_dagrun(
run_id=DAG1_RUN2_ID,
Expand DownExpand Up@@ -114,7 +114,14 @@ class TestGetDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, run_type, triggered_by, dag_run_note",
[
(DAG1_ID, DAG1_RUN1_ID, DAG1_RUN1_STATE, DAG1_RUN1_RUN_TYPE, DAG1_RUN1_TRIGGERED_BY, DAG1_NOTE),
(
DAG1_ID,
DAG1_RUN1_ID,
DAG1_RUN1_STATE,
DAG1_RUN1_RUN_TYPE,
DAG1_RUN1_TRIGGERED_BY,
DAG1_RUN1_NOTE,
),
(DAG1_ID, DAG1_RUN2_ID, DAG1_RUN2_STATE, DAG1_RUN2_RUN_TYPE, DAG1_RUN2_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN1_ID, DAG2_RUN1_STATE, DAG2_RUN1_RUN_TYPE, DAG2_RUN1_TRIGGERED_BY, None),
(DAG2_ID, DAG2_RUN2_ID, DAG2_RUN2_STATE, DAG2_RUN2_RUN_TYPE, DAG2_RUN2_TRIGGERED_BY, None),
Expand All@@ -140,36 +147,85 @@ def test_get_dag_run_not_found(self, test_client):

class TestPatchDagRun:
@pytest.mark.parametrize(
"dag_id, run_id, state, response_state",
"dag_id, run_id, patch_body, response_body",
[
(DAG1_ID, DAG1_RUN1_ID, DagRunState.FAILED, DagRunState.FAILED),
(DAG1_ID, DAG1_RUN2_ID, DagRunState.SUCCESS, DagRunState.SUCCESS),
(DAG2_ID, DAG2_RUN1_ID, DagRunState.QUEUED, DagRunState.QUEUED),
(
DAG1_ID,
DAG1_RUN1_ID,
{"state": DagRunState.FAILED, "note": "new_note2"},
{"state": DagRunState.FAILED, "note": "new_note2"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"state": DagRunState.SUCCESS},
{"state": DagRunState.SUCCESS, "note": None},
),
(
DAG2_ID,
DAG2_RUN1_ID,
{"state": DagRunState.QUEUED},
{"state": DagRunState.QUEUED, "note": None},
),
(
DAG1_ID,
DAG1_RUN1_ID,
{"note": "updated note"},
{"state": DagRunState.SUCCESS, "note": "updated note"},
),
(
DAG1_ID,
DAG1_RUN2_ID,
{"note": "new note", "state": DagRunState.FAILED},
{"state": DagRunState.FAILED, "note": "new note"},
),
(DAG1_ID, DAG1_RUN2_ID, {"note": None}, {"state": DagRunState.FAILED, "note": None}),
],
)
def test_patch_dag_run(self, test_client, dag_id, run_id, state, response_state):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json={"state": state})
def test_patch_dag_run(self, test_client, dag_id, run_id, patch_body, response_body):
response = test_client.patch(f"/public/dags/{dag_id}/dagRuns/{run_id}", json=patch_body)
assert response.status_code == 200
body = response.json()
assert body["dag_id"] == dag_id
assert body["run_id"] == run_id
assert body["state"] == response_state
assert body.get("state") == response_body.get("state")
assert body.get("note") == response_body.get("note")

@pytest.mark.parametrize(
"query_params, patch_body, expected_status_code",
"query_params, patch_body, response_body, expected_status_code",
[
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, 200),
({}, {"state": DagRunState.SUCCESS}, 200),
({"update_mask": ["random"]}, {"state": DagRunState.SUCCESS}, 400),
({"update_mask": ["state"]}, {"state": DagRunState.SUCCESS}, {"state": "success"}, 200),
(
{"update_mask": ["note"]},
{"state": DagRunState.FAILED, "note": "new_note1"},
{"note": "new_note1", "state": "success"},
200,
),
(
{},
{"state": DagRunState.FAILED, "note": "new_note2"},
{"note": "new_note2", "state": "failed"},
200,
),
({"update_mask": ["note"]}, {}, {"state": "success", "note": None}, 200),
(
{"update_mask": ["random"]},
{"state": DagRunState.FAILED},
{"state": "success", "note": "test_note"},
200,
),
],
)
def test_patch_dag_run_with_update_mask(
self, test_client, query_params, patch_body, expected_status_code
self, test_client, query_params, patch_body, response_body, expected_status_code
):
response = test_client.patch(
f"/public/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}", params=query_params, json=patch_body
)
response_json = response.json()
assert response.status_code == expected_status_code
for key, value in response_body.items():
assert response_json.get(key) == value

def test_patch_dag_run_not_found(self, test_client):
response = test_client.patch(
Expand Down