Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/core_api/datamodels/task_instances.py: 98%
155 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
1# Licensed to the Apache Software Foundation (ASF) under one
2# or more contributor license agreements. See the NOTICE file
3# distributed with this work for additional information
4# regarding copyright ownership. The ASF licenses this file
5# to you under the Apache License, Version 2.0 (the
6# "License"); you may not use this file except in compliance
7# with the License. You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing,
12# software distributed under the License is distributed on an
13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14# KIND, either express or implied. See the License for the
15# specific language governing permissions and limitations
16# under the License.
17from __future__ import annotations
19from collections.abc import Iterable
20from datetime import datetime
21from typing import Annotated, Any
22from uuid import UUID
24from pydantic import (
25 AliasPath,
26 AwareDatetime,
27 BeforeValidator,
28 Discriminator,
29 Field,
30 NonNegativeInt,
31 StringConstraints,
32 Tag,
33 field_validator,
34 model_validator,
35)
37from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
38from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse
39from airflow.api_fastapi.core_api.datamodels.job import JobResponse
40from airflow.api_fastapi.core_api.datamodels.trigger import TriggerResponse
41from airflow.utils.state import TaskInstanceState
44class NewTaskResponse(BaseModel):
45 """Lightweight response for new tasks that don't have TaskInstances yet."""
47 task_id: str
48 task_display_name: str
51class TaskInstanceResponse(BaseModel):
52 """TaskInstance serializer for responses."""
54 id: UUID
55 task_id: str
56 dag_id: str
57 run_id: str = Field(alias="dag_run_id")
58 map_index: int
59 logical_date: datetime | None
60 run_after: datetime
61 start_date: datetime | None
62 end_date: datetime | None
63 duration: float | None
64 state: TaskInstanceState | None
65 try_number: int
66 max_tries: int
67 task_display_name: str
68 dag_display_name: str = Field(validation_alias=AliasPath("dag_run", "dag_model", "dag_display_name"))
69 hostname: str | None
70 unixname: str | None
71 pool: str
72 pool_slots: int
73 queue: str | None
74 priority_weight: int | None
75 operator: str | None
76 operator_name: str | None
77 queued_dttm: datetime | None = Field(alias="queued_when")
78 scheduled_dttm: datetime | None = Field(alias="scheduled_when")
79 pid: int | None
80 executor: str | None
81 executor_config: Annotated[str, BeforeValidator(str)]
82 note: str | None
83 rendered_map_index: str | None
84 rendered_fields: dict = Field(
85 validation_alias=AliasPath("rendered_task_instance_fields", "rendered_fields"),
86 default_factory=dict,
87 )
88 trigger: TriggerResponse | None
89 queued_by_job: JobResponse | None = Field(alias="triggerer_job")
90 dag_version: DagVersionResponse | None
93class TaskInstanceCollectionResponse(BaseModel):
94 """
95 Task instance collection response supporting both offset and cursor pagination.
97 A single flat model is used instead of a discriminated union
98 (``Annotated[Offset | Cursor, Field(discriminator=...)]``) because
99 the OpenAPI ``oneOf`` + ``discriminator`` construct is not handled
100 correctly by ``@hey-api/openapi-ts`` / ``@7nohe/openapi-react-query-codegen``:
101 return types degrade to ``unknown`` in JSDoc and can produce
102 incorrect TypeScript types (see hey-api/openapi-ts#1613, #3270).
103 """
105 task_instances: Iterable[TaskInstanceResponse]
106 total_entries: int | None = Field(
107 default=None,
108 description="Number of matching items. For offset pagination this is the exact total. "
109 "For cursor pagination it is capped at ``total_entries_limit``; a value equal to that "
110 "limit means at least that many items match.",
111 )
112 total_entries_limit: int | None = Field(
113 default=None,
114 description="Cap applied to ``total_entries`` under cursor pagination. ``null`` for offset "
115 "pagination, where ``total_entries`` is exact.",
116 )
117 next_cursor: str | None = Field(
118 default=None,
119 description="Token pointing to the next page. Populated for cursor pagination, "
120 "``null`` when using offset pagination or when there is no next page.",
121 )
122 previous_cursor: str | None = Field(
123 default=None,
124 description="Token pointing to the previous page. Populated for cursor pagination, "
125 "``null`` when using offset pagination or when on the first page.",
126 )
129def _task_instance_discriminator(v: Any) -> str:
130 """Discriminate between TaskInstanceResponse and NewTaskResponse in the union."""
131 if isinstance(v, NewTaskResponse): 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true
132 return "new"
133 if isinstance(v, dict): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 return "new" if "id" not in v else "full"
135 # ORM objects and TaskInstanceResponse instances
136 return "full"
139class ClearTaskInstanceCollectionResponse(BaseModel):
140 """Response for clear dag run dry run, which may contain new tasks without full TaskInstance data."""
142 task_instances: Iterable[
143 Annotated[
144 Annotated[TaskInstanceResponse, Tag("full")] | Annotated[NewTaskResponse, Tag("new")],
145 Discriminator(_task_instance_discriminator),
146 ]
147 ]
148 total_entries: int
151class TaskDependencyResponse(BaseModel):
152 """Task Dependency serializer for responses."""
154 name: str
155 reason: str
158class TaskDependencyCollectionResponse(BaseModel):
159 """Task scheduling dependencies collection serializer for responses."""
161 dependencies: list[TaskDependencyResponse]
164class TaskInstancesBatchBody(StrictBaseModel):
165 """Task Instance body for get batch."""
167 dag_ids: list[str] | None = None
168 dag_run_ids: list[str] | None = None
169 task_ids: list[str] | None = None
170 state: list[TaskInstanceState | None] | None = None
172 run_after_gte: AwareDatetime | None = None
173 run_after_gt: AwareDatetime | None = None
174 run_after_lte: AwareDatetime | None = None
175 run_after_lt: AwareDatetime | None = None
177 logical_date_gte: AwareDatetime | None = None
178 logical_date_gt: AwareDatetime | None = None
179 logical_date_lte: AwareDatetime | None = None
180 logical_date_lt: AwareDatetime | None = None
182 start_date_gte: AwareDatetime | None = None
183 start_date_gt: AwareDatetime | None = None
184 start_date_lte: AwareDatetime | None = None
185 start_date_lt: AwareDatetime | None = None
187 end_date_gte: AwareDatetime | None = None
188 end_date_gt: AwareDatetime | None = None
189 end_date_lte: AwareDatetime | None = None
190 end_date_lt: AwareDatetime | None = None
192 duration_gte: float | None = None
193 duration_gt: float | None = None
194 duration_lte: float | None = None
195 duration_lt: float | None = None
197 pool: list[str] | None = None
198 queue: list[str] | None = None
199 executor: list[str] | None = None
200 page_offset: NonNegativeInt = 0
201 page_limit: NonNegativeInt = 100
202 order_by: str | None = None
205class ClearTaskInstancesBody(StrictBaseModel):
206 """Request body for Clear Task Instances endpoint."""
208 dry_run: bool = True
209 start_date: AwareDatetime | None = None
210 end_date: AwareDatetime | None = None
211 only_failed: bool = True
212 only_running: bool = False
213 reset_dag_runs: bool = True
214 task_ids: list[str | tuple[str, int]] | None = Field(
215 default=None,
216 description="A list of `task_id` or [`task_id`, `map_index`]. "
217 "If only the `task_id` is provided for a mapped task, all of its map indices will be targeted.",
218 )
219 dag_run_id: str | None = None
220 include_upstream: bool = False
221 include_downstream: bool = False
222 include_future: bool = False
223 include_past: bool = False
224 run_on_latest_version: bool | None = Field(
225 default=None,
226 description="(Experimental) Run on the latest bundle version of the dag after "
227 "clearing the task instances. "
228 "If not specified, falls back to the DAG-level ``rerun_with_latest_version`` parameter, "
229 "then the ``[core] rerun_with_latest_version`` config option, "
230 "and finally ``False`` (the historical default for clear/rerun).",
231 )
232 prevent_running_task: bool = False
233 note: Annotated[str, StringConstraints(max_length=1000)] | None = None
235 @model_validator(mode="before")
236 @classmethod
237 def validate_model(cls, data: Any) -> Any:
238 """Validate clear task instance form."""
239 if data.get("only_failed") and data.get("only_running"):
240 raise ValueError("only_failed and only_running both are set to True")
241 if data.get("start_date") and data.get("end_date"):
242 if data.get("start_date") > data.get("end_date"):
243 raise ValueError("end_date is sooner than start_date")
244 if data.get("start_date") and data.get("end_date") and data.get("dag_run_id"):
245 raise ValueError("Exactly one of dag_run_id or (start_date and end_date) must be provided")
246 if data.get("start_date") and data.get("dag_run_id"):
247 raise ValueError("Exactly one of dag_run_id or start_date must be provided")
248 if data.get("end_date") and data.get("dag_run_id"):
249 raise ValueError("Exactly one of dag_run_id or end_date must be provided")
250 if isinstance(data.get("task_ids"), list) and len(data.get("task_ids")) < 1:
251 raise ValueError("task_ids list should have at least 1 element.")
252 return data
255class PatchTaskInstanceBody(StrictBaseModel):
256 """Request body for patching task instance state."""
258 new_state: TaskInstanceState | None = None
259 note: Annotated[str, StringConstraints(max_length=1000)] | None = None
260 include_upstream: bool = False
261 include_downstream: bool = False
262 include_future: bool = False
263 include_past: bool = False
265 @field_validator("new_state", mode="before")
266 @classmethod
267 def validate_new_state(cls, ns: str | None) -> str:
268 """Validate new_state."""
269 valid_states = [
270 vs.name.lower()
271 for vs in (TaskInstanceState.SUCCESS, TaskInstanceState.FAILED, TaskInstanceState.SKIPPED)
272 ]
273 if ns is None:
274 raise ValueError("'new_state' should not be empty")
275 ns = ns.lower()
276 if ns not in valid_states:
277 raise ValueError(f"'{ns}' is not one of {valid_states}")
278 return ns
281class BulkTaskInstanceBody(PatchTaskInstanceBody, StrictBaseModel):
282 """Request body for bulk update, and delete task instances."""
284 task_id: str
285 map_index: int | None = None
286 dag_id: str | None = None
287 dag_run_id: str | None = None