pierrejeambrun commented on code in PR #62583:
URL: https://github.com/apache/airflow/pull/62583#discussion_r3023041560


##########
airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py:
##########
@@ -310,3 +312,207 @@ def test_should_response_401(self, 
unauthenticated_test_client):
     def test_should_response_403(self, unauthorized_test_client):
         response = 
unauthorized_test_client.get(f"/dags/{DAG_ID}/dagRuns/{RUN_EMPTY}/deadlines")
         assert response.status_code == 403
+
+
+class TestGetDeadlines:
+    """Tests for GET /dags/{dag_id}/dagRuns/{dag_run_id}/deadlines with ~ 
wildcards."""
+
+    # ------------------------------------------------------------------
+    # 200 – happy paths
+    # ------------------------------------------------------------------
+
+    def test_returns_all_deadlines(self, test_client):
+        """All deadlines across all runs are returned when both dag_id and 
dag_run_id are ~."""
+        response = test_client.get("/dags/~/dagRuns/~/deadlines")
+        assert response.status_code == 200
+        data = response.json()
+        # Fixture creates: 1 (run_single) + 1 (run_missed) + 1 (run_alert) + 3 
(run_multi) + 1 (run_other) = 7
+        assert data["total_entries"] == 7
+        assert len(data["deadlines"]) == 7
+
+    def test_response_includes_dag_and_run_ids(self, test_client):
+        """Each deadline includes dag_id and dag_run_id for linking."""
+        response = test_client.get(f"/dags/{DAG_ID}/dagRuns/~/deadlines", 
params={"limit": 1})
+        assert response.status_code == 200
+        dl = response.json()["deadlines"][0]
+        assert dl["dag_id"] == DAG_ID
+        assert "dag_run_id" in dl
+        assert "id" in dl
+        assert "deadline_time" in dl
+        assert "missed" in dl

Review Comment:
   merge this with previous test. No need to have 1 test for payload 1 test for 
number of responses etc...



##########
airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py:
##########
@@ -44,3 +44,58 @@ class DeadlineCollectionResponse(BaseModel):
 
     deadlines: Iterable[DeadlineResponse]
     total_entries: int
+
+
+class DeadlineWithDagRunResponse(BaseModel):
+    """Deadline serializer for responses that includes DAG and DAG run 
identifiers."""
+
+    id: UUID
+    deadline_time: datetime
+    missed: bool
+    created_at: datetime
+    dag_id: str = Field(validation_alias=AliasPath("dagrun", "dag_id"))
+    dag_run_id: str = Field(validation_alias=AliasPath("dagrun", "run_id"))
+    alert_name: str | None = 
Field(validation_alias=AliasPath("deadline_alert", "name"), default=None)
+    alert_description: str | None = Field(
+        validation_alias=AliasPath("deadline_alert", "description"), 
default=None
+    )

Review Comment:
   Why do we duplicate the serializer ? Can't we just adds necessary stuff to 
`DeadlineResponse` and use that only?



##########
airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py:
##########
@@ -310,3 +312,207 @@ def test_should_response_401(self, 
unauthenticated_test_client):
     def test_should_response_403(self, unauthorized_test_client):
         response = 
unauthorized_test_client.get(f"/dags/{DAG_ID}/dagRuns/{RUN_EMPTY}/deadlines")
         assert response.status_code == 403
+
+
+class TestGetDeadlines:
+    """Tests for GET /dags/{dag_id}/dagRuns/{dag_run_id}/deadlines with ~ 
wildcards."""
+
+    # ------------------------------------------------------------------
+    # 200 – happy paths
+    # ------------------------------------------------------------------
+
+    def test_returns_all_deadlines(self, test_client):
+        """All deadlines across all runs are returned when both dag_id and 
dag_run_id are ~."""
+        response = test_client.get("/dags/~/dagRuns/~/deadlines")
+        assert response.status_code == 200
+        data = response.json()
+        # Fixture creates: 1 (run_single) + 1 (run_missed) + 1 (run_alert) + 3 
(run_multi) + 1 (run_other) = 7
+        assert data["total_entries"] == 7
+        assert len(data["deadlines"]) == 7

Review Comment:
   There is only 1 dag in the test. so there is no cross dag filtering. For 
this test to be relevant we need another 'dag' and dealines associatied, to 
make sure deadlines for all dags are returned 



##########
airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py:
##########
@@ -310,3 +312,207 @@ def test_should_response_401(self, 
unauthenticated_test_client):
     def test_should_response_403(self, unauthorized_test_client):
         response = 
unauthorized_test_client.get(f"/dags/{DAG_ID}/dagRuns/{RUN_EMPTY}/deadlines")
         assert response.status_code == 403
+
+
+class TestGetDeadlines:
+    """Tests for GET /dags/{dag_id}/dagRuns/{dag_run_id}/deadlines with ~ 
wildcards."""
+
+    # ------------------------------------------------------------------
+    # 200 – happy paths
+    # ------------------------------------------------------------------
+

Review Comment:
   Nit, remove all similar comments. (401, 403 etc....)



##########
airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py:
##########
@@ -44,3 +44,58 @@ class DeadlineCollectionResponse(BaseModel):
 
     deadlines: Iterable[DeadlineResponse]
     total_entries: int
+
+
+class DeadlineWithDagRunResponse(BaseModel):
+    """Deadline serializer for responses that includes DAG and DAG run 
identifiers."""
+
+    id: UUID
+    deadline_time: datetime
+    missed: bool
+    created_at: datetime
+    dag_id: str = Field(validation_alias=AliasPath("dagrun", "dag_id"))
+    dag_run_id: str = Field(validation_alias=AliasPath("dagrun", "run_id"))
+    alert_name: str | None = 
Field(validation_alias=AliasPath("deadline_alert", "name"), default=None)
+    alert_description: str | None = Field(
+        validation_alias=AliasPath("deadline_alert", "description"), 
default=None
+    )
+
+
+class DeadlineWithDagRunCollectionResponse(BaseModel):
+    """Deadline Collection serializer for responses that includes DAG and DAG 
run identifiers."""
+
+    deadlines: Iterable[DeadlineWithDagRunResponse]
+    total_entries: int
+
+
+class DeadlineAlertResponse(BaseModel):
+    """DeadlineAlert serializer for responses."""
+
+    id: UUID
+    name: str | None = None
+    description: str | None = None
+    reference_type: str
+    interval: float

Review Comment:
   Nit: interval Field Unit is Ambiguous
   The DeadlineAlertResponse.interval field is a float with no documentation of 
its unit. The test asserts alert["interval"] == 3600.0 which suggests seconds, 
but the API contract doesn't state this. A docstring or field description 
(e.g., Field(description="Interval in seconds")) would clarify the contract for 
API consumers.



##########
airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/deadline.py:
##########
@@ -44,3 +44,58 @@ class DeadlineCollectionResponse(BaseModel):
 
     deadlines: Iterable[DeadlineResponse]
     total_entries: int
+
+
+class DeadlineWithDagRunResponse(BaseModel):
+    """Deadline serializer for responses that includes DAG and DAG run 
identifiers."""
+
+    id: UUID
+    deadline_time: datetime
+    missed: bool
+    created_at: datetime
+    dag_id: str = Field(validation_alias=AliasPath("dagrun", "dag_id"))
+    dag_run_id: str = Field(validation_alias=AliasPath("dagrun", "run_id"))
+    alert_name: str | None = 
Field(validation_alias=AliasPath("deadline_alert", "name"), default=None)
+    alert_description: str | None = Field(
+        validation_alias=AliasPath("deadline_alert", "description"), 
default=None
+    )
+
+
+class DeadlineWithDagRunCollectionResponse(BaseModel):
+    """Deadline Collection serializer for responses that includes DAG and DAG 
run identifiers."""
+
+    deadlines: Iterable[DeadlineWithDagRunResponse]
+    total_entries: int
+
+
+class DeadlineAlertResponse(BaseModel):
+    """DeadlineAlert serializer for responses."""
+
+    id: UUID
+    name: str | None = None
+    description: str | None = None
+    reference_type: str
+    interval: float
+    created_at: datetime
+
+    @model_validator(mode="before")
+    @classmethod
+    def extract_reference_type(cls, data):
+        """Extract reference_type from the JSON reference column when 
validating from an ORM object."""
+        if not isinstance(data, dict) and hasattr(data, "reference"):
+            return {
+                "id": data.id,
+                "name": data.name,
+                "description": data.description,
+                "reference_type": data.reference.get("reference_type", "") if 
data.reference else "",
+                "interval": data.interval,
+                "created_at": data.created_at,
+            }
+        return data

Review Comment:
   A more robust approach would be to use a computed field or a single 
@field_validator for just reference_type, letting Pydantic's from_attributes 
handle the other fields normally. For example, using 
validation_alias=AliasPath("reference", "reference_type") (which is how the 
existing DeadlineWithDagRunResponse handles nested fields like dag_id).



##########
airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py:
##########
@@ -49,4 +48,5 @@
 ui_router.include_router(grid_router)
 ui_router.include_router(gantt_router)
 ui_router.include_router(calendar_router)
+ui_router.include_router(partitioned_dag_runs_router)

Review Comment:
   Why this re-order?  This creates a bigger code diff. Is there route 
shadowing?



##########
airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_deadlines.py:
##########
@@ -310,3 +312,207 @@ def test_should_response_401(self, 
unauthenticated_test_client):
     def test_should_response_403(self, unauthorized_test_client):
         response = 
unauthorized_test_client.get(f"/dags/{DAG_ID}/dagRuns/{RUN_EMPTY}/deadlines")
         assert response.status_code == 403
+
+
+class TestGetDeadlines:
+    """Tests for GET /dags/{dag_id}/dagRuns/{dag_run_id}/deadlines with ~ 
wildcards."""
+
+    # ------------------------------------------------------------------
+    # 200 – happy paths
+    # ------------------------------------------------------------------
+
+    def test_returns_all_deadlines(self, test_client):
+        """All deadlines across all runs are returned when both dag_id and 
dag_run_id are ~."""
+        response = test_client.get("/dags/~/dagRuns/~/deadlines")
+        assert response.status_code == 200
+        data = response.json()
+        # Fixture creates: 1 (run_single) + 1 (run_missed) + 1 (run_alert) + 3 
(run_multi) + 1 (run_other) = 7
+        assert data["total_entries"] == 7
+        assert len(data["deadlines"]) == 7
+
+    def test_response_includes_dag_and_run_ids(self, test_client):
+        """Each deadline includes dag_id and dag_run_id for linking."""
+        response = test_client.get(f"/dags/{DAG_ID}/dagRuns/~/deadlines", 
params={"limit": 1})
+        assert response.status_code == 200
+        dl = response.json()["deadlines"][0]
+        assert dl["dag_id"] == DAG_ID
+        assert "dag_run_id" in dl
+        assert "id" in dl
+        assert "deadline_time" in dl
+        assert "missed" in dl
+
+    def test_filter_by_dag_id(self, test_client):
+        """Specifying a dag_id path param (with ~ for dag_run_id) returns only 
that DAG's deadlines."""
+        response = test_client.get(f"/dags/{DAG_ID}/dagRuns/~/deadlines")
+        assert response.status_code == 200
+        data = response.json()
+        assert data["total_entries"] == 7
+        for dl in data["deadlines"]:
+            assert dl["dag_id"] == DAG_ID

Review Comment:
   We need a second dag with it's set of deadlines to make sure filtering is 
working.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to