This is an automated email from the ASF dual-hosted git repository.
Lee-W pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 1dcdcd7dca4 Expose upstream partition key on triggering asset events
(#69115)
1dcdcd7dca4 is described below
commit 1dcdcd7dca42268cc23903733a95f0b694f58f07
Author: Wei Lee <[email protected]>
AuthorDate: Mon Jun 29 17:51:52 2026 +0800
Expose upstream partition key on triggering asset events (#69115)
---
.../execution_api/datamodels/taskinstance.py | 1 +
.../execution_api/versions/v2026_04_06.py | 10 +++-
.../versions/head/test_task_instances.py | 32 +++++++++++++
.../versions/v2026_04_06/test_task_instances.py | 54 +++++++++++++++++++++-
.../src/airflow/sdk/api/datamodels/_generated.py | 1 +
.../airflow/sdk/execution_time/schema/schema.json | 12 +++++
.../tests/task_sdk/execution_time/test_context.py | 18 ++++++++
7 files changed, 125 insertions(+), 3 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
index bf20bcbc30c..ad051b3e6d3 100644
---
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
+++
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
@@ -317,6 +317,7 @@ class AssetEventDagRunReference(StrictBaseModel):
source_map_index: int | None
source_aliases: list[AssetAliasReferenceAssetEventDagRun]
timestamp: UtcDateTime
+ partition_key: str | None = None
class DagRun(StrictBaseModel):
diff --git
a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
index 5a3b0f0f5fc..e3b995011f4 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
@@ -29,6 +29,7 @@ from airflow.api_fastapi.execution_api.datamodels.asset_event
import (
)
from airflow.api_fastapi.execution_api.datamodels.dagrun import
TriggerDAGRunPayload
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
+ AssetEventDagRunReference,
DagRun,
TIDeferredStatePayload,
TIRunContext,
@@ -43,6 +44,7 @@ class AddPartitionKeyField(VersionChange):
instructions_to_migrate_to_previous_version = (
schema(DagRun).field("partition_key").didnt_exist,
schema(AssetEventResponse).field("partition_key").didnt_exist,
+ schema(AssetEventDagRunReference).field("partition_key").didnt_exist,
schema(TriggerDAGRunPayload).field("partition_key").didnt_exist,
schema(DagRunAssetReference).field("partition_key").didnt_exist,
)
@@ -50,8 +52,12 @@ class AddPartitionKeyField(VersionChange):
@convert_response_to_previous_version_for(TIRunContext) # type:
ignore[arg-type]
def remove_partition_key_from_dag_run(response: ResponseInfo) -> None: #
type: ignore[misc]
"""Remove the `partition_key` field from the dag_run object when
converting to the previous version."""
- if "dag_run" in response.body and isinstance(response.body["dag_run"],
dict):
- response.body["dag_run"].pop("partition_key", None)
+ dag_run = response.body.get("dag_run")
+ if isinstance(dag_run, dict):
+ dag_run.pop("partition_key", None)
+ for event in dag_run.get("consumed_asset_events") or ():
+ if isinstance(event, dict):
+ event.pop("partition_key", None)
@convert_response_to_previous_version_for(AssetEventsResponse) # type:
ignore[arg-type]
def remove_partition_key_from_asset_events(response: ResponseInfo) ->
None: # type: ignore[misc]
diff --git
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
index 90325bb9c85..67e0a4ee4ae 100644
---
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
+++
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py
@@ -164,12 +164,44 @@ class TestTIRunState:
clear_db_runs()
clear_db_serialized_dags()
clear_db_dags()
+ clear_db_assets()
def teardown_method(self):
clear_db_logs()
clear_db_runs()
clear_db_serialized_dags()
clear_db_dags()
+ clear_db_assets()
+
+ def test_ti_run_context_exposes_consumed_event_partition_key(self, client,
session, create_task_instance):
+ """The partition key of each consumed asset event is returned in the
run context."""
+ ti = create_task_instance(
+ task_id="test_consumed_event_partition_key",
+ state=State.QUEUED,
+ session=session,
+ )
+ asset = AssetModel(name="upstream", uri="s3://bucket/upstream",
group="asset", extra={})
+ session.add_all([asset, AssetActive.for_asset(asset)])
+ session.flush()
+ ti.dag_run.consumed_asset_events.append(
+ AssetEvent(asset_id=asset.id, source_dag_id="src",
source_run_id="r1", partition_key="2024-01-15")
+ )
+ session.commit()
+
+ response = client.patch(
+ f"/execution/task-instances/{ti.id}/run",
+ json={
+ "state": "running",
+ "hostname": "h",
+ "unixname": "u",
+ "pid": 1,
+ "start_date": "2024-09-30T12:00:00Z",
+ },
+ )
+
+ assert response.status_code == 200
+ events = response.json()["dag_run"]["consumed_asset_events"]
+ assert [e["partition_key"] for e in events] == ["2024-01-15"]
@pytest.mark.parametrize(
("max_tries", "should_retry"),
diff --git
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
index d8552917277..4a8b59c362e 100644
---
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
+++
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
@@ -20,10 +20,11 @@ from __future__ import annotations
import pytest
from airflow._shared.timezones import timezone
+from airflow.models.asset import AssetActive, AssetEvent, AssetModel
from airflow.serialization.serialized_objects import BaseSerialization
from airflow.utils.state import DagRunState, State
-from tests_common.test_utils.db import clear_db_runs
+from tests_common.test_utils.db import clear_db_assets, clear_db_runs
from tests_common.test_utils.format_datetime import
from_datetime_to_zulu_without_ms
pytestmark = pytest.mark.db_test
@@ -253,3 +254,54 @@ class TestNextKwargsBackwardCompat:
assert response.status_code == 200
# Head version gets the plain dict directly -- no BaseSerialization
wrapping
assert response.json()["next_kwargs"] == {"cheesecake": True, "event":
"payload"}
+
+
+class TestConsumedEventPartitionKeyBackwardCompat:
+ """The partition_key on consumed asset events is stripped for
pre-2026-04-06 clients."""
+
+ @pytest.fixture(autouse=True)
+ def _freeze_time(self, time_machine):
+ time_machine.move_to(TIMESTAMP_STR, tick=False)
+
+ def setup_method(self):
+ clear_db_runs()
+ clear_db_assets()
+
+ def teardown_method(self):
+ clear_db_runs()
+ clear_db_assets()
+
+ def _create_ti_with_consumed_event(self, session, create_task_instance):
+ ti = create_task_instance(
+ task_id="test_consumed_event_partition_key_compat",
+ state=State.QUEUED,
+ session=session,
+ start_date=TIMESTAMP,
+ )
+ asset = AssetModel(name="upstream", uri="s3://bucket/upstream",
group="asset", extra={})
+ session.add_all([asset, AssetActive.for_asset(asset)])
+ session.flush()
+ ti.dag_run.consumed_asset_events.append(
+ AssetEvent(asset_id=asset.id, source_dag_id="src",
source_run_id="r1", partition_key="2024-01-15")
+ )
+ session.commit()
+ return ti
+
+ def test_old_version_strips_partition_key(self, old_ver_client, session,
create_task_instance):
+ ti = self._create_ti_with_consumed_event(session, create_task_instance)
+
+ response =
old_ver_client.patch(f"/execution/task-instances/{ti.id}/run",
json=RUN_PATCH_BODY)
+
+ assert response.status_code == 200
+ events = response.json()["dag_run"]["consumed_asset_events"]
+ assert events
+ assert all("partition_key" not in event for event in events)
+
+ def test_head_version_keeps_partition_key(self, client, session,
create_task_instance):
+ ti = self._create_ti_with_consumed_event(session, create_task_instance)
+
+ response = client.patch(f"/execution/task-instances/{ti.id}/run",
json=RUN_PATCH_BODY)
+
+ assert response.status_code == 200
+ events = response.json()["dag_run"]["consumed_asset_events"]
+ assert [event["partition_key"] for event in events] == ["2024-01-15"]
diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
index 139632fc0a0..72aebeab3e4 100644
--- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
+++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
@@ -726,6 +726,7 @@ class AssetEventDagRunReference(BaseModel):
source_map_index: Annotated[int | None, Field(title="Source Map Index")] =
None
source_aliases: Annotated[list[AssetAliasReferenceAssetEventDagRun],
Field(title="Source Aliases")]
timestamp: Annotated[AwareDatetime, Field(title="Timestamp")]
+ partition_key: Annotated[str | None, Field(title="Partition Key")] = None
class AssetEventResponse(BaseModel):
diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
index ae736e5d662..e6ce8aa3d06 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
@@ -4515,6 +4515,18 @@
"format": "date-time",
"title": "Timestamp",
"type": "string"
+ },
+ "partition_key": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Partition Key"
}
},
"required": [
diff --git a/task-sdk/tests/task_sdk/execution_time/test_context.py
b/task-sdk/tests/task_sdk/execution_time/test_context.py
index f82f4924fba..eb1ffa0013e 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_context.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_context.py
@@ -660,6 +660,24 @@ class TestTriggeringAssetEventsAccessor:
assert mock_supervisor_comms.send.mock_calls ==
[mock.call(GetAssetByUri(uri=uri))]
assert _AssetRefResolutionMixin._asset_ref_cache
+ def test_partition_key_exposed(self):
+ """A consumed asset event's partition key is reachable via
triggering_asset_events."""
+ event = {
+ "asset": {"name": "1", "uri": "1", "extra": {}},
+ "extra": {},
+ "source_task_id": "t1",
+ "source_dag_id": "d1",
+ "source_run_id": "r1",
+ "source_map_index": -1,
+ "source_aliases": [],
+ "timestamp": "2025-01-01T00:00:12Z",
+ "partition_key": "2024-01-15",
+ }
+ accessor = TriggeringAssetEventsAccessor.build(
+ [AssetEventDagRunReferenceResult.model_validate(event)]
+ )
+ assert [e.partition_key for e in accessor[Asset("1")]] ==
["2024-01-15"]
+
def test_source_task_instance_xcom_pull(self, mock_supervisor_comms,
accessor):
events = accessor[Asset("2")]
assert len(events) == 1