Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
622dc91
Type asset_expression in the REST API instead of an untyped dict
Anuragp22 Jun 2, 2026
ecc6c2c
Derive ExpressionType from generated types, drop manual aliases
Anuragp22 Jun 10, 2026
9bf4fb6
Merge main; reconcile asset_expression typing with AIP-76 partition c…
Anuragp22 Jun 10, 2026
7021adb
Remove unused NextRunEvent type from AssetExpression
Anuragp22 Jun 10, 2026
3835593
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 10, 2026
77ea8d9
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 12, 2026
5b68ad2
Apply ruff-format to partitioned_dag_runs route
Anuragp22 Jun 12, 2026
783d6ce
Degrade legacy asset_expression shapes to null instead of 500ing the API
Anuragp22 Jun 12, 2026
5b56c31
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 13, 2026
a4eface
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 15, 2026
f60e4b7
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 19, 2026
499c423
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 25, 2026
7da64e5
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 25, 2026
1e47c21
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 25, 2026
6253946
Log a warning when an unrecognized asset_expression shape is dropped
Anuragp22 Jun 25, 2026
473234a
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 26, 2026
4dd3696
Extract response dict into a named variable before model_validate
Anuragp22 Jun 26, 2026
bf7a836
Annotate extracted model_data dicts as dict[str, Any]
Anuragp22 Jun 26, 2026
91a4dd4
Add route test for legacy asset_expression served as null
Anuragp22 Jun 26, 2026
8ad3d7e
Merge branch 'main' into type-asset-expression-api
Anuragp22 Jun 26, 2026
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
123 changes: 122 additions & 1 deletion airflow-core/src/airflow/api_fastapi/core_api/datamodels/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,133 @@
from __future__ import annotations

import enum
import logging
from typing import Annotated, Any, Generic, Literal, TypeVar, Union

from pydantic import Discriminator, Field, Tag
from pydantic import BeforeValidator, Discriminator, Field, Tag, TypeAdapter, ValidationError

from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel

log = logging.getLogger(__name__)

# Asset Scheduling Expression Data Models
#
# These mirror the JSON produced by ``BaseAsset.as_expression()`` (see
# ``airflow.serialization.definitions.assets``), which is stored verbatim in
# ``DagModel.asset_expression``. Declaring them gives the REST API -- and the
# TypeScript client generated from its OpenAPI spec -- a real type instead of an
# opaque ``dict``. The shape is a recursive boolean tree whose leaves are assets,
# asset aliases, or asset references.


class AssetExpressionAssetInfo(BaseModel):
"""
Body of an ``asset`` leaf node.

``id`` is injected by ``DagModelOperation.update_dag_asset_expression`` when the expression is
persisted; ``BaseAsset.as_expression()`` itself only emits ``uri``/``name``/``group``. It is left
optional so a row persisted before id-enrichment (or migrated from the pre-3.0 dataset format)
degrades gracefully instead of failing response validation.
"""

uri: str
name: str
group: str
id: int | None = None


class AssetExpressionAliasInfo(BaseModel):
"""Body of an ``alias`` leaf node."""

name: str
group: str


class AssetExpressionAsset(BaseModel):
"""An asset leaf: ``{"asset": {"uri": ..., "name": ..., "group": ...}}``."""

asset: AssetExpressionAssetInfo


class AssetExpressionAlias(BaseModel):
"""An asset alias leaf: ``{"alias": {"name": ..., "group": ...}}``."""

alias: AssetExpressionAliasInfo


class AssetExpressionRef(BaseModel):
"""An unresolved asset reference leaf: ``{"asset_ref": {"name": ...}}`` or ``{"asset_ref": {"uri": ...}}``."""

asset_ref: dict[str, str]


class AssetExpressionAny(BaseModel):
"""An "or" node: ``{"any": [...]}`` -- satisfied when any child is satisfied."""

any: list[AssetExpression]


class AssetExpressionAll(BaseModel):
"""An "and" node: ``{"all": [...]}`` -- satisfied when all children are satisfied."""

all: list[AssetExpression]


def _asset_expression_discriminator(value: Any) -> str | None:
"""Select an expression variant by the single key that is present."""
keys = ("asset", "alias", "asset_ref", "any", "all")
if isinstance(value, dict):
present = [key for key in keys if key in value]
else:
present = [key for key in keys if getattr(value, key, None) is not None]
return present[0] if len(present) == 1 else None


AssetExpression = Annotated[
Union[
Annotated[AssetExpressionAsset, Tag("asset")],
Annotated[AssetExpressionAlias, Tag("alias")],
Annotated[AssetExpressionRef, Tag("asset_ref")],
Annotated[AssetExpressionAny, Tag("any")],
Annotated[AssetExpressionAll, Tag("all")],
],
Discriminator(_asset_expression_discriminator),
]
"""A nested asset scheduling expression; see ``BaseAsset.as_expression()``."""

AssetExpressionAny.model_rebuild()
AssetExpressionAll.model_rebuild()

_asset_expression_adapter: TypeAdapter = TypeAdapter(AssetExpression)


def _coerce_unrecognized_expression_to_none(value: Any) -> Any:
"""
Degrade an unrecognized ``asset_expression`` to ``None`` instead of failing response validation.

``DagModel.asset_expression`` is stored verbatim from ``BaseAsset.as_expression()`` and is rewritten
to the current shape whenever a Dag is parsed (``DagModelOperation.update_dag_asset_expression``). A
row written by the pre-3.0 dataset scheduler and not yet re-parsed can still hold a legacy shape --
a bare uri string, ``{"any": ["s3://..."]}``, or ``{"alias": "<name>"}`` -- that the typed model does
not recognise. Serving such a row as ``None`` reproduces the blank render the UI showed while this
field was an untyped ``dict``, rather than turning stored data into an HTTP 500.
"""
if value is None:
return None
try:
_asset_expression_adapter.validate_python(value)
except ValidationError:
log.warning("Dropping unrecognized asset_expression shape to None: %r", value)
return None
Comment thread
Anuragp22 marked this conversation as resolved.
return value


MaybeAssetExpression = Annotated[
Union[AssetExpression, None],
BeforeValidator(_coerce_unrecognized_expression_to_none),
]
"""``AssetExpression | None`` that degrades a legacy/unrecognized stored shape to ``None``."""

# Common Bulk Data Models
T = TypeVar("T")
K = TypeVar("K")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from airflow._shared.module_loading import qualname
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel, make_partial_model
from airflow.api_fastapi.core_api.datamodels.common import MaybeAssetExpression
from airflow.api_fastapi.core_api.datamodels.dag_tags import DagTagResponse
from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse
from airflow.configuration import conf
Expand Down Expand Up @@ -191,7 +192,7 @@ class DAGDetailsResponse(DAGResponse):

catchup: bool
dag_run_timeout: timedelta | None
asset_expression: dict | None
asset_expression: MaybeAssetExpression
doc_md: str | None
start_date: datetime | None
end_date: datetime | None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pydantic import Field

from airflow.api_fastapi.core_api.base import BaseModel
from airflow.api_fastapi.core_api.datamodels.common import MaybeAssetExpression


class NextRunAssetEventResponse(BaseModel):
Expand Down Expand Up @@ -49,6 +50,6 @@ class NextRunAssetEventResponse(BaseModel):
class NextRunAssetsResponse(BaseModel):
"""Response for the ``next_run_assets`` endpoint."""

asset_expression: dict | None = None
asset_expression: MaybeAssetExpression = None
events: list[NextRunAssetEventResponse]
pending_partition_count: int | None = None
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

from airflow.api_fastapi.core_api.base import BaseModel
from airflow.api_fastapi.core_api.datamodels.common import MaybeAssetExpression
from airflow.api_fastapi.core_api.datamodels.dags import DAGResponse
from airflow.api_fastapi.core_api.datamodels.hitl import HITLDetail
from airflow.api_fastapi.core_api.datamodels.ui.dag_runs import DAGRunLightResponse
Expand All @@ -26,7 +27,7 @@
class DAGWithLatestDagRunsResponse(DAGResponse):
"""DAG with latest dag runs response serializer."""

asset_expression: dict | None
asset_expression: MaybeAssetExpression
latest_dag_runs: list[DAGRunLightResponse]
pending_actions: list[HITLDetail]
is_favorite: bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

from airflow.api_fastapi.core_api.base import BaseModel
from airflow.api_fastapi.core_api.datamodels.common import MaybeAssetExpression


class PartitionedDagRunResponse(BaseModel):
Expand All @@ -37,7 +38,7 @@ class PartitionedDagRunCollectionResponse(BaseModel):

partitioned_dag_runs: list[PartitionedDagRunResponse]
total: int
asset_expressions: dict[str, dict | None] | None = None
asset_expressions: dict[str, MaybeAssetExpression] | None = None


class PartitionedDagRunAssetResponse(BaseModel):
Expand Down Expand Up @@ -75,4 +76,4 @@ class PartitionedDagRunDetailResponse(BaseModel):
assets: list[PartitionedDagRunAssetResponse]
total_required: int
total_received: int
asset_expression: dict | None = None
asset_expression: MaybeAssetExpression = None
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,123 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
AssetExpressionAlias:
properties:
alias:
$ref: '#/components/schemas/AssetExpressionAliasInfo'
type: object
required:
- alias
title: AssetExpressionAlias
description: 'An asset alias leaf: ``{"alias": {"name": ..., "group": ...}}``.'
AssetExpressionAliasInfo:
properties:
name:
type: string
title: Name
group:
type: string
title: Group
type: object
required:
- name
- group
title: AssetExpressionAliasInfo
description: Body of an ``alias`` leaf node.
AssetExpressionAll:
properties:
all:
items:
oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
type: array
title: All
type: object
required:
- all
title: AssetExpressionAll
description: 'An "and" node: ``{"all": [...]}`` -- satisfied when all children
are satisfied.'
AssetExpressionAny:
properties:
any:
items:
oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
type: array
title: Any
type: object
required:
- any
title: AssetExpressionAny
description: 'An "or" node: ``{"any": [...]}`` -- satisfied when any child is
satisfied.'
AssetExpressionAsset:
properties:
asset:
$ref: '#/components/schemas/AssetExpressionAssetInfo'
type: object
required:
- asset
title: AssetExpressionAsset
description: 'An asset leaf: ``{"asset": {"uri": ..., "name": ..., "group":
...}}``.'
AssetExpressionAssetInfo:
properties:
uri:
type: string
title: Uri
name:
type: string
title: Name
group:
type: string
title: Group
id:
anyOf:
- type: integer
- type: 'null'
title: Id
type: object
required:
- uri
- name
- group
title: AssetExpressionAssetInfo
description: 'Body of an ``asset`` leaf node.


``id`` is injected by ``DagModelOperation.update_dag_asset_expression`` when
the expression is

persisted; ``BaseAsset.as_expression()`` itself only emits ``uri``/``name``/``group``.
It is left

optional so a row persisted before id-enrichment (or migrated from the pre-3.0
dataset format)

degrades gracefully instead of failing response validation.'
AssetExpressionRef:
properties:
asset_ref:
additionalProperties:
type: string
type: object
title: Asset Ref
type: object
required:
- asset_ref
title: AssetExpressionRef
description: 'An unresolved asset reference leaf: ``{"asset_ref": {"name": ...}}``
or ``{"asset_ref": {"uri": ...}}``.'
AuthenticatedMeResponse:
properties:
id:
Expand Down Expand Up @@ -2295,8 +2412,12 @@ components:
title: Owners
asset_expression:
anyOf:
- additionalProperties: true
type: object
- oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
- type: 'null'
title: Asset Expression
latest_dag_runs:
Expand Down Expand Up @@ -3239,8 +3360,12 @@ components:
properties:
asset_expression:
anyOf:
- additionalProperties: true
type: object
- oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
- type: 'null'
title: Asset Expression
events:
Expand Down Expand Up @@ -3400,8 +3525,12 @@ components:
anyOf:
- additionalProperties:
anyOf:
- additionalProperties: true
type: object
- oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
- type: 'null'
type: object
- type: 'null'
Expand Down Expand Up @@ -3451,8 +3580,12 @@ components:
title: Total Received
asset_expression:
anyOf:
- additionalProperties: true
type: object
- oneOf:
- $ref: '#/components/schemas/AssetExpressionAsset'
- $ref: '#/components/schemas/AssetExpressionAlias'
- $ref: '#/components/schemas/AssetExpressionRef'
- $ref: '#/components/schemas/AssetExpressionAny'
- $ref: '#/components/schemas/AssetExpressionAll'
- type: 'null'
title: Asset Expression
type: object
Expand Down
Loading
Loading