This is an automated email from the ASF dual-hosted git repository.

bbovenzi 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 3dcdcb8002f API: Return asset events for Dag Runs without a start date 
(#71379)
3dcdcb8002f is described below

commit 3dcdcb8002fb4babbaa161950d038480f9fcf97e
Author: Ko HanJong <[email protected]>
AuthorDate: Wed Aug 19 03:33:23 2026 +0900

    API: Return asset events for Dag Runs without a start date (#71379)
    
    * API: Return asset events for Dag Runs without a start date
    
    Asset events should remain available when their created Dag Run has not 
started yet.
    
    * Preserve asset event client coverage
    
    The regression coverage needs to retain started and empty association 
responses while exercising unstarted Dag Runs.
    
    * Update generated API metadata
    
    The nullable asset-event response must be represented in the generated 
supervisor schema, and CI requires the current dependency metadata in the 
lockfile.
    
    * Update TypeScript SDK supervisor schema
    
    Asset events can now reference an unstarted Dag Run, so TypeScript clients 
must accept a missing start date.
---
 .../api_fastapi/core_api/datamodels/assets.py      |  2 +-
 .../core_api/openapi/v2-rest-api-generated.yaml    |  6 +++--
 .../execution_api/datamodels/asset_event.py        |  2 +-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 11 ++++++--
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  2 +-
 .../core_api/routes/public/test_assets.py          | 26 +++++++++++++++++++
 .../core_api/routes/public/test_dag_run.py         | 22 +++++++++++++---
 .../versions/head/test_asset_events.py             | 24 +++++++++++++++++
 .../src/airflowctl/api/datamodels/generated.py     |  2 +-
 .../tests/airflow_ctl/api/test_operations.py       | 22 +++++++++++++---
 .../src/airflow/sdk/api/datamodels/_generated.py   |  2 +-
 .../airflow/sdk/execution_time/schema/schema.json  | 13 +++++++---
 task-sdk/tests/task_sdk/api/test_client.py         | 30 ++++++++++++++++++++--
 ts-sdk/src/generated/supervisor.ts                 |  2 +-
 14 files changed, 145 insertions(+), 21 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
index fea14b36755..d4bb4c2ce2b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py
@@ -164,7 +164,7 @@ class DagRunAssetReference(StrictBaseModel):
     run_id: str
     dag_id: str
     logical_date: datetime | None
-    start_date: datetime
+    start_date: datetime | None
     end_date: datetime | None
     state: str
     data_interval_start: datetime | None
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 69ac00b6e37..f892c29e770 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -13940,8 +13940,10 @@ components:
           - type: 'null'
           title: Logical Date
         start_date:
-          type: string
-          format: date-time
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
           title: Start Date
         end_date:
           anyOf:
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py
index f6c3ce82669..7076a50f506 100644
--- 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py
+++ 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py
@@ -31,7 +31,7 @@ class DagRunAssetReference(StrictBaseModel):
     run_id: str
     dag_id: str
     logical_date: datetime | None
-    start_date: datetime
+    start_date: datetime | None
     end_date: datetime | None
     state: str
     data_interval_start: datetime | None
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 624c749fc6c..d636c35e678 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -4442,8 +4442,15 @@ export const $DagRunAssetReference = {
             title: 'Logical Date'
         },
         start_date: {
-            type: 'string',
-            format: 'date-time',
+            anyOf: [
+                {
+                    type: 'string',
+                    format: 'date-time'
+                },
+                {
+                    type: 'null'
+                }
+            ],
             title: 'Start Date'
         },
         end_date: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 5afe50bcd14..67b335a8b77 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1150,7 +1150,7 @@ export type DagRunAssetReference = {
     run_id: string;
     dag_id: string;
     logical_date: string | null;
-    start_date: string;
+    start_date: string | null;
     end_date: string | null;
     state: string;
     data_interval_start: string | null;
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index 4179dcb9182..80fa977ee0b 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -1039,6 +1039,32 @@ class TestGetAssetEvents(TestAssets):
         assert events[1]["created_dagruns"][0]["triggering"] is False
         assert events[2]["created_dagruns"][0]["triggering"] is True
 
+    def test_should_return_created_dag_run_without_start_date(self, 
test_client, session):
+        self.create_assets(num=1, session=session)
+        asset_event = AssetEvent(
+            asset_id=1,
+            source_dag_id="producer_dag",
+            source_run_id="producer_run",
+            timestamp=DEFAULT_DATE,
+        )
+        dag_run = DagRun(
+            dag_id="consumer_dag",
+            run_id="asset-triggered-run",
+            run_type=DagRunType.ASSET_TRIGGERED,
+            logical_date=DEFAULT_DATE,
+            start_date=None,
+            data_interval=(DEFAULT_DATE, DEFAULT_DATE),
+            state=DagRunState.QUEUED,
+        )
+        dag_run.consumed_asset_events.append(asset_event)
+        session.add(dag_run)
+        session.commit()
+
+        response = test_client.get("/assets/events")
+
+        assert response.status_code == 200
+        assert 
response.json()["asset_events"][0]["created_dagruns"][0]["start_date"] is None
+
     def test_should_respond_401(self, unauthenticated_test_client):
         response = unauthenticated_test_client.get("/assets/events")
         assert response.status_code == 401
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
index 4aef320cc63..c2a8528afa6 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
@@ -1970,7 +1970,16 @@ class TestGetDagRunAssetTriggerEvents:
         ["test_partition_key", None],
         ids=["partitioned", "non-partitioned"],
     )
-    def test_should_respond_200(self, partition_key, test_client, dag_maker, 
session):
+    @pytest.mark.parametrize(
+        ("state", "start_date_is_none"),
+        [
+            pytest.param(DagRunState.RUNNING, False, id="running"),
+            pytest.param(DagRunState.QUEUED, True, 
id="queued-without-start-date"),
+        ],
+    )
+    def test_should_respond_200(
+        self, partition_key, state, start_date_is_none, test_client, 
dag_maker, session
+    ):
         asset1 = Asset(name="ds1", uri="file:///da1")
 
         # Use PartitionedAtRuntime for partitioned cases so the partition_key 
gate does not reject the key.
@@ -2008,6 +2017,9 @@ class TestGetDagRunAssetTriggerEvents:
             # explicitly so dag_maker does not try to infer it via 
next_dagrun_info (which returns None).
             create_dagrun_kwargs["logical_date"] = None
         dr = dag_maker.create_dagrun(**create_dagrun_kwargs)
+        dr.state = state
+        if start_date_is_none:
+            dr.start_date = None
         dr.consumed_asset_events.append(event)
 
         session.commit()
@@ -2040,8 +2052,12 @@ class TestGetDagRunAssetTriggerEvents:
                             "data_interval_start": 
from_datetime_to_zulu_without_ms(dr.data_interval_start),
                             "end_date": None,
                             "logical_date": 
from_datetime_to_zulu_without_ms(dr.logical_date),
-                            "start_date": 
from_datetime_to_zulu_without_ms(dr.start_date),
-                            "state": "running",
+                            "start_date": (
+                                None
+                                if start_date_is_none
+                                else 
from_datetime_to_zulu_without_ms(dr.start_date)
+                            ),
+                            "state": state.value,
                             "partition_key": partition_key,
                             "triggering": True,
                         }
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
index da1e3705daf..8cf59a440c5 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_asset_events.py
@@ -23,6 +23,9 @@ import pytest
 
 from airflow._shared.timezones import timezone
 from airflow.models.asset import AssetActive, AssetAliasModel, AssetEvent, 
AssetModel
+from airflow.models.dagrun import DagRun
+from airflow.utils.state import DagRunState
+from airflow.utils.types import DagRunType
 
 from tests_common.test_utils.config import conf_vars
 
@@ -93,6 +96,27 @@ def test_asset_alias(session, test_asset_events, test_asset):
 
 
 class TestGetAssetEventByAsset:
+    @pytest.mark.usefixtures("test_asset")
+    def test_get_by_asset_with_created_dagrun_without_start_date(self, client, 
session, test_asset_events):
+        created_dagrun = DagRun(
+            dag_id="created_dag",
+            run_id="queued_run",
+            logical_date=DEFAULT_DATE,
+            state=DagRunState.QUEUED,
+            run_type=DagRunType.ASSET_TRIGGERED,
+            data_interval=(DEFAULT_DATE, DEFAULT_DATE),
+        )
+        test_asset_events[0].created_dagruns.append(created_dagrun)
+        session.commit()
+
+        response = client.get(
+            "/execution/asset-events/by-asset",
+            params={"name": "test_get_asset_by_name", "uri": None},
+        )
+
+        assert response.status_code == 200
+        assert 
response.json()["asset_events"][0]["created_dagruns"][0]["start_date"] is None
+
     @pytest.mark.parametrize(
         ("uri", "name"),
         [
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 9f464e8bda3..58a5369b1fb 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -527,7 +527,7 @@ class DagRunAssetReference(BaseModel):
     run_id: Annotated[str, Field(title="Run Id")]
     dag_id: Annotated[str, Field(title="Dag Id")]
     logical_date: Annotated[datetime | None, Field(title="Logical Date")]
-    start_date: Annotated[datetime, Field(title="Start Date")]
+    start_date: Annotated[datetime | None, Field(title="Start Date")]
     end_date: Annotated[datetime | None, Field(title="End Date")]
     state: Annotated[str, Field(title="State")]
     data_interval_start: Annotated[datetime | None, Field(title="Data Interval 
Start")]
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index aaad996ec99..7806fcfb5ab 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -434,14 +434,30 @@ class TestAssetsOperations:
         response = client.assets.list_aliases()
         assert response == assets_collection_response
 
-    def test_create_event(self):
+    @pytest.mark.parametrize(
+        "created_dagrun",
+        [
+            pytest.param(assets_dag_reference, id="running"),
+            pytest.param(
+                assets_dag_reference.model_copy(
+                    update={"start_date": None, "end_date": None, "state": 
"queued"}
+                ),
+                id="queued-without-start-date",
+            ),
+        ],
+    )
+    def test_create_event(self, created_dagrun):
+        asset_event_response = self.asset_event_response.model_copy(
+            update={"created_dagruns": [created_dagrun]}
+        )
+
         def handle_request(request: httpx.Request) -> httpx.Response:
             assert request.url.path == "/api/v2/assets/events"
-            return httpx.Response(200, 
json=json.loads(self.asset_event_response.model_dump_json()))
+            return httpx.Response(200, 
json=json.loads(asset_event_response.model_dump_json()))
 
         client = make_api_client(transport=httpx.MockTransport(handle_request))
         response = 
client.assets.create_event(asset_event_body=self.asset_create_event_body)
-        assert response == self.asset_event_response
+        assert response == asset_event_response
 
     def test_materialize(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py 
b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
index 201f218c3c9..f29a3488440 100644
--- a/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
+++ b/task-sdk/src/airflow/sdk/api/datamodels/_generated.py
@@ -135,7 +135,7 @@ class DagRunAssetReference(BaseModel):
     run_id: Annotated[str, Field(title="Run Id")]
     dag_id: Annotated[str, Field(title="Dag Id")]
     logical_date: Annotated[AwareDatetime | None, Field(title="Logical Date")]
-    start_date: Annotated[AwareDatetime, Field(title="Start Date")]
+    start_date: Annotated[AwareDatetime | None, Field(title="Start Date")]
     end_date: Annotated[AwareDatetime | None, Field(title="End Date")]
     state: Annotated[str, Field(title="State")]
     data_interval_start: Annotated[AwareDatetime | None, Field(title="Data 
Interval Start")]
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 4524c74ff79..85f8b7cfa4e 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
@@ -1147,9 +1147,16 @@
           "title": "Logical Date"
         },
         "start_date": {
-          "format": "date-time",
-          "title": "Start Date",
-          "type": "string"
+          "anyOf": [
+            {
+              "format": "date-time",
+              "type": "string"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "title": "Start Date"
         },
         "end_date": {
           "anyOf": [
diff --git a/task-sdk/tests/task_sdk/api/test_client.py 
b/task-sdk/tests/task_sdk/api/test_client.py
index 8527349f56b..8bd558b461b 100644
--- a/task-sdk/tests/task_sdk/api/test_client.py
+++ b/task-sdk/tests/task_sdk/api/test_client.py
@@ -1202,7 +1202,30 @@ class TestAssetEventOperations:
             ({"alias_name": "this_asset_alias"}),
         ],
     )
-    def test_by_name_get_success(self, request_params):
+    @pytest.mark.parametrize(
+        ("created_dagruns", "expected_created_dagruns"),
+        [
+            pytest.param([], 0, id="without-created-dagrun"),
+            pytest.param(
+                [
+                    {
+                        "dag_id": "created_dag",
+                        "run_id": "queued_run",
+                        "logical_date": "2023-01-01T00:00:00Z",
+                        "start_date": None,
+                        "end_date": None,
+                        "state": "queued",
+                        "data_interval_start": None,
+                        "data_interval_end": None,
+                        "partition_key": None,
+                    }
+                ],
+                1,
+                id="queued-created-dagrun-without-start-date",
+            ),
+        ],
+    )
+    def test_by_name_get_success(self, request_params, created_dagruns, 
expected_created_dagruns):
         def handle_request(request: httpx.Request) -> httpx.Response:
             params = request.url.params
             if request.url.path == "/asset-events/by-asset":
@@ -1224,7 +1247,7 @@ class TestAssetEventOperations:
                                 "uri": "s3://bucket/key",
                                 "group": "asset",
                             },
-                            "created_dagruns": [],
+                            "created_dagruns": created_dagruns,
                             "timestamp": "2023-01-01T00:00:00Z",
                         }
                     ]
@@ -1238,6 +1261,9 @@ class TestAssetEventOperations:
         assert len(result.asset_events) == 1
         assert result.asset_events[0].asset.name == "this_asset"
         assert result.asset_events[0].asset.uri == "s3://bucket/key"
+        assert len(result.asset_events[0].created_dagruns) == 
expected_created_dagruns
+        if expected_created_dagruns:
+            assert result.asset_events[0].created_dagruns[0].start_date is None
 
     def test_partition_key_exact_match_param_passed(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/ts-sdk/src/generated/supervisor.ts 
b/ts-sdk/src/generated/supervisor.ts
index e12e5d815ae..8eb27a1d815 100644
--- a/ts-sdk/src/generated/supervisor.ts
+++ b/ts-sdk/src/generated/supervisor.ts
@@ -37,7 +37,7 @@ export type Extra1 = {
 export type RunId = string;
 export type DagId = string;
 export type LogicalDate = string | null;
-export type StartDate = string;
+export type StartDate = string | null;
 export type EndDate = string | null;
 export type State = string;
 export type DataIntervalStart = string | null;

Reply via email to