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 19a3cba6de7 Add Calendar Deadlines API Endpoint and Data Models 
(#68604)
19a3cba6de7 is described below

commit 19a3cba6de7dc86c5937d6f2332f92a0dfea0aa8
Author: Richard <[email protected]>
AuthorDate: Tue Jul 7 07:29:27 2026 -0700

    Add Calendar Deadlines API Endpoint and Data Models (#68604)
    
    - Introduced new data models: CalendarDeadlineResponse and 
CalendarDeadlineCollectionResponse for handling aggregated deadline counts.
    - Implemented the GET /ui/calendar/{dag_id}/deadlines endpoint to retrieve 
deadline data, including parameters for granularity and deadline time filters.
    - Updated CalendarService to include a method for fetching deadline 
calendar data, aggregating results by time bucket and missed status.
    - Added unit tests for the new endpoint to ensure correct functionality and 
response structure.
---
 .../api_fastapi/core_api/datamodels/ui/calendar.py |  15 ++
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 113 ++++++++++++
 .../api_fastapi/core_api/routes/ui/calendar.py     |  34 +++-
 .../api_fastapi/core_api/services/ui/calendar.py   |  55 ++++++
 .../src/airflow/ui/openapi-gen/queries/common.ts   |  11 ++
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  21 +++
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  21 +++
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  21 +++
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  21 +++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  42 +++++
 .../ui/openapi-gen/requests/services.gen.ts        |  35 +++-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  43 +++++
 .../core_api/routes/ui/test_calendar.py            | 202 ++++++++++++++++++++-
 13 files changed, 631 insertions(+), 3 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
index 9bd0359463c..09f5062a497 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
@@ -42,3 +42,18 @@ class CalendarTimeRangeCollectionResponse(BaseModel):
 
     total_entries: int
     dag_runs: list[CalendarTimeRangeResponse]
+
+
+class CalendarDeadlineResponse(BaseModel):
+    """Represents aggregated deadline counts for a specific calendar time 
bucket."""
+
+    date: datetime
+    missed: bool
+    count: int
+
+
+class CalendarDeadlineCollectionResponse(BaseModel):
+    """Response model for calendar deadline aggregation results."""
+
+    total_entries: int
+    deadlines: list[CalendarDeadlineResponse]
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 887aee12c9b..173f231a098 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -1732,6 +1732,83 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /ui/calendar/{dag_id}/deadlines:
+    get:
+      tags:
+      - Calendar
+      summary: Get Calendar Deadlines
+      description: Get aggregated deadline counts for a Dag, bucketed by 
deadline_time
+        and missed status.
+      operationId: get_calendar_deadlines
+      security:
+      - OAuth2PasswordBearer: []
+      - HTTPBearer: []
+      parameters:
+      - name: dag_id
+        in: path
+        required: true
+        schema:
+          type: string
+          title: Dag Id
+      - name: granularity
+        in: query
+        required: false
+        schema:
+          enum:
+          - hourly
+          - daily
+          type: string
+          default: daily
+          title: Granularity
+      - name: deadline_time_gte
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Deadline Time Gte
+      - name: deadline_time_gt
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Deadline Time Gt
+      - name: deadline_time_lte
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Deadline Time Lte
+      - name: deadline_time_lt
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Deadline Time Lt
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CalendarDeadlineCollectionResponse'
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
   /ui/teams:
     get:
       tags:
@@ -2064,6 +2141,42 @@ components:
       - type
       title: BaseNodeResponse
       description: Base Node serializer for responses.
+    CalendarDeadlineCollectionResponse:
+      properties:
+        total_entries:
+          type: integer
+          title: Total Entries
+        deadlines:
+          items:
+            $ref: '#/components/schemas/CalendarDeadlineResponse'
+          type: array
+          title: Deadlines
+      type: object
+      required:
+      - total_entries
+      - deadlines
+      title: CalendarDeadlineCollectionResponse
+      description: Response model for calendar deadline aggregation results.
+    CalendarDeadlineResponse:
+      properties:
+        date:
+          type: string
+          format: date-time
+          title: Date
+        missed:
+          type: boolean
+          title: Missed
+        count:
+          type: integer
+          title: Count
+      type: object
+      required:
+      - date
+      - missed
+      - count
+      title: CalendarDeadlineResponse
+      description: Represents aggregated deadline counts for a specific 
calendar time
+        bucket.
     CalendarTimeRangeCollectionResponse:
       properties:
         total_entries:
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/calendar.py
index 4f3746b669b..066a4845b82 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/calendar.py
@@ -25,10 +25,14 @@ from airflow.api_fastapi.common.dagbag import DagBagDep, 
get_latest_version_of_d
 from airflow.api_fastapi.common.db.common import SessionDep
 from airflow.api_fastapi.common.parameters import RangeFilter, 
datetime_range_filter_factory
 from airflow.api_fastapi.common.router import AirflowRouter
-from airflow.api_fastapi.core_api.datamodels.ui.calendar import 
CalendarTimeRangeCollectionResponse
+from airflow.api_fastapi.core_api.datamodels.ui.calendar import (
+    CalendarDeadlineCollectionResponse,
+    CalendarTimeRangeCollectionResponse,
+)
 from airflow.api_fastapi.core_api.security import requires_access_dag
 from airflow.api_fastapi.core_api.services.ui.calendar import CalendarService
 from airflow.models.dagrun import DagRun
+from airflow.models.deadline import Deadline
 
 calendar_router = AirflowRouter(prefix="/calendar", tags=["Calendar"])
 
@@ -71,3 +75,31 @@ def get_calendar(
         partition_date=partition_date,
         granularity=granularity,
     )
+
+
+@calendar_router.get(
+    "/{dag_id}/deadlines",
+    dependencies=[
+        Depends(
+            requires_access_dag(
+                method="GET",
+                access_entity=DagAccessEntity.RUN,
+            )
+        ),
+    ],
+)
+def get_calendar_deadlines(
+    dag_id: str,
+    session: SessionDep,
+    deadline_time: Annotated[RangeFilter, 
Depends(datetime_range_filter_factory("deadline_time", Deadline))],
+    granularity: Literal["hourly", "daily"] = "daily",
+) -> CalendarDeadlineCollectionResponse:
+    """Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status."""
+    calendar_service = CalendarService()
+
+    return calendar_service.get_deadline_calendar_data(
+        dag_id=dag_id,
+        session=session,
+        deadline_time=deadline_time,
+        granularity=granularity,
+    )
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
index ebd5a38453e..ea4f33f9149 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
@@ -30,10 +30,13 @@ from sqlalchemy.orm import InstrumentedAttribute, Session
 from airflow._shared.timezones import timezone
 from airflow.api_fastapi.common.parameters import RangeFilter
 from airflow.api_fastapi.core_api.datamodels.ui.calendar import (
+    CalendarDeadlineCollectionResponse,
+    CalendarDeadlineResponse,
     CalendarTimeRangeCollectionResponse,
     CalendarTimeRangeResponse,
 )
 from airflow.models.dagrun import DagRun
+from airflow.models.deadline import Deadline
 from airflow.serialization.definitions.dag import SerializedDAG
 from airflow.timetables._cron import CronMixin
 from airflow.timetables.base import DagRunInfo, DataInterval, TimeRestriction
@@ -357,3 +360,55 @@ class CalendarService:
             return False
 
         return True
+
+    def get_deadline_calendar_data(
+        self,
+        dag_id: str,
+        deadline_time: RangeFilter,
+        granularity: Literal["hourly", "daily"] = "daily",
+        *,
+        session: Session,
+    ) -> CalendarDeadlineCollectionResponse:
+        """
+        Get deadline calendar data for a Dag, aggregated by time bucket and 
missed status.
+
+        Args:
+            dag_id: The Dag ID
+            session: Database session
+            deadline_time: Date range filter for deadline_time
+            granularity: Time granularity ("hourly" or "daily")
+
+        Returns:
+            Aggregated deadline counts per time bucket, split by 
missed/pending status
+        """
+        dialect = get_dialect_name(session)
+        time_expression = 
self._get_time_truncation_expression(Deadline.deadline_time, granularity, 
dialect)
+
+        select_stmt = (
+            sa.select(
+                time_expression.label("datetime"),
+                Deadline.missed,
+                sa.func.count("*").label("count"),
+            )
+            .join(Deadline.dagrun)
+            .where(DagRun.dag_id == dag_id)
+            .group_by(time_expression, Deadline.missed)
+            .order_by(time_expression.asc(), Deadline.missed.asc())
+        )
+
+        select_stmt = deadline_time.to_orm(select_stmt)
+        rows = session.execute(select_stmt).all()
+
+        results = [
+            CalendarDeadlineResponse(
+                date=row.datetime,
+                missed=row.missed,
+                count=int(row._mapping["count"]),
+            )
+            for row in rows
+        ]
+
+        return CalendarDeadlineCollectionResponse(
+            total_entries=len(results),
+            deadlines=results,
+        )
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index a34ca6b0c68..9b07c3c8649 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -1035,6 +1035,17 @@ export const UseCalendarServiceGetCalendarKeyFn = ({ 
dagId, granularity, logical
   partitionDateLt?: string;
   partitionDateLte?: string;
 }, queryKey?: Array<unknown>) => [useCalendarServiceGetCalendarKey, 
...(queryKey ?? [{ dagId, granularity, logicalDateGt, logicalDateGte, 
logicalDateLt, logicalDateLte, partitionDateGt, partitionDateGte, 
partitionDateLt, partitionDateLte }])];
+export type CalendarServiceGetCalendarDeadlinesDefaultResponse = 
Awaited<ReturnType<typeof CalendarService.getCalendarDeadlines>>;
+export type CalendarServiceGetCalendarDeadlinesQueryResult<TData = 
CalendarServiceGetCalendarDeadlinesDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
+export const useCalendarServiceGetCalendarDeadlinesKey = 
"CalendarServiceGetCalendarDeadlines";
+export const UseCalendarServiceGetCalendarDeadlinesKeyFn = ({ dagId, 
deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity 
}: {
+  dagId: string;
+  deadlineTimeGt?: string;
+  deadlineTimeGte?: string;
+  deadlineTimeLt?: string;
+  deadlineTimeLte?: string;
+  granularity?: "hourly" | "daily";
+}, queryKey?: Array<unknown>) => [useCalendarServiceGetCalendarDeadlinesKey, 
...(queryKey ?? [{ dagId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, 
deadlineTimeLte, granularity }])];
 export type TeamsServiceListTeamsDefaultResponse = Awaited<ReturnType<typeof 
TeamsService.listTeams>>;
 export type TeamsServiceListTeamsQueryResult<TData = 
TeamsServiceListTeamsDefaultResponse, TError = unknown> = UseQueryResult<TData, 
TError>;
 export const useTeamsServiceListTeamsKey = "TeamsServiceListTeams";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index c4c043c93b4..501d6158503 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -2086,6 +2086,27 @@ export const ensureUseCalendarServiceGetCalendarData = 
(queryClient: QueryClient
   partitionDateLte?: string;
 }) => queryClient.ensureQueryData({ queryKey: 
Common.UseCalendarServiceGetCalendarKeyFn({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }), queryFn: () => 
CalendarService.getCalendar({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }) });
 /**
+* Get Calendar Deadlines
+* Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.granularity
+* @param data.deadlineTimeGte
+* @param data.deadlineTimeGt
+* @param data.deadlineTimeLte
+* @param data.deadlineTimeLt
+* @returns CalendarDeadlineCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseCalendarServiceGetCalendarDeadlinesData = (queryClient: 
QueryClient, { dagId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, 
deadlineTimeLte, granularity }: {
+  dagId: string;
+  deadlineTimeGt?: string;
+  deadlineTimeGte?: string;
+  deadlineTimeLt?: string;
+  deadlineTimeLte?: string;
+  granularity?: "hourly" | "daily";
+}) => queryClient.ensureQueryData({ queryKey: 
Common.UseCalendarServiceGetCalendarDeadlinesKeyFn({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }), queryFn: () 
=> CalendarService.getCalendarDeadlines({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }) });
+/**
 * List Teams
 * @param data The data for the request.
 * @param data.limit
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 7aebe21755c..d4ade79763f 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -2086,6 +2086,27 @@ export const prefetchUseCalendarServiceGetCalendar = 
(queryClient: QueryClient,
   partitionDateLte?: string;
 }) => queryClient.prefetchQuery({ queryKey: 
Common.UseCalendarServiceGetCalendarKeyFn({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }), queryFn: () => 
CalendarService.getCalendar({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }) });
 /**
+* Get Calendar Deadlines
+* Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.granularity
+* @param data.deadlineTimeGte
+* @param data.deadlineTimeGt
+* @param data.deadlineTimeLte
+* @param data.deadlineTimeLt
+* @returns CalendarDeadlineCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseCalendarServiceGetCalendarDeadlines = (queryClient: 
QueryClient, { dagId, deadlineTimeGt, deadlineTimeGte, deadlineTimeLt, 
deadlineTimeLte, granularity }: {
+  dagId: string;
+  deadlineTimeGt?: string;
+  deadlineTimeGte?: string;
+  deadlineTimeLt?: string;
+  deadlineTimeLte?: string;
+  granularity?: "hourly" | "daily";
+}) => queryClient.prefetchQuery({ queryKey: 
Common.UseCalendarServiceGetCalendarDeadlinesKeyFn({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }), queryFn: () 
=> CalendarService.getCalendarDeadlines({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }) });
+/**
 * List Teams
 * @param data The data for the request.
 * @param data.limit
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 42eed251fa4..a8cd6066357 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -2086,6 +2086,27 @@ export const useCalendarServiceGetCalendar = <TData = 
Common.CalendarServiceGetC
   partitionDateLte?: string;
 }, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseCalendarServiceGetCalendarKeyFn({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }, queryKey), queryFn: () 
=> CalendarService.getCalendar({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, par [...]
 /**
+* Get Calendar Deadlines
+* Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.granularity
+* @param data.deadlineTimeGte
+* @param data.deadlineTimeGt
+* @param data.deadlineTimeLte
+* @param data.deadlineTimeLt
+* @returns CalendarDeadlineCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useCalendarServiceGetCalendarDeadlines = <TData = 
Common.CalendarServiceGetCalendarDeadlinesDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }: {
+  dagId: string;
+  deadlineTimeGt?: string;
+  deadlineTimeGte?: string;
+  deadlineTimeLt?: string;
+  deadlineTimeLte?: string;
+  granularity?: "hourly" | "daily";
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseCalendarServiceGetCalendarDeadlinesKeyFn({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }, queryKey), 
queryFn: () => CalendarService.getCalendarDeadlines({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }) as TData, 
...options });
+/**
 * List Teams
 * @param data The data for the request.
 * @param data.limit
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index b3f2d6f4e9f..384cfb52b01 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -2086,6 +2086,27 @@ export const useCalendarServiceGetCalendarSuspense = 
<TData = Common.CalendarSer
   partitionDateLte?: string;
 }, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseCalendarServiceGetCalendarKeyFn({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDateGt, 
partitionDateGte, partitionDateLt, partitionDateLte }, queryKey), queryFn: () 
=> CalendarService.getCalendar({ dagId, granularity, logicalDateGt, 
logicalDateGte, logicalDateLt, logicalDateLte, partitionDat [...]
 /**
+* Get Calendar Deadlines
+* Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.granularity
+* @param data.deadlineTimeGte
+* @param data.deadlineTimeGt
+* @param data.deadlineTimeLte
+* @param data.deadlineTimeLt
+* @returns CalendarDeadlineCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useCalendarServiceGetCalendarDeadlinesSuspense = <TData = 
Common.CalendarServiceGetCalendarDeadlinesDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }: {
+  dagId: string;
+  deadlineTimeGt?: string;
+  deadlineTimeGte?: string;
+  deadlineTimeLt?: string;
+  deadlineTimeLte?: string;
+  granularity?: "hourly" | "daily";
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseCalendarServiceGetCalendarDeadlinesKeyFn({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }, queryKey), 
queryFn: () => CalendarService.getCalendarDeadlines({ dagId, deadlineTimeGt, 
deadlineTimeGte, deadlineTimeLt, deadlineTimeLte, granularity }) as TData, 
...options });
+/**
 * List Teams
 * @param data The data for the request.
 * @param data.limit
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 4dab4ff145c..ec5024d981e 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
@@ -8581,6 +8581,48 @@ export const $BaseNodeResponse = {
     description: 'Base Node serializer for responses.'
 } as const;
 
+export const $CalendarDeadlineCollectionResponse = {
+    properties: {
+        total_entries: {
+            type: 'integer',
+            title: 'Total Entries'
+        },
+        deadlines: {
+            items: {
+                '$ref': '#/components/schemas/CalendarDeadlineResponse'
+            },
+            type: 'array',
+            title: 'Deadlines'
+        }
+    },
+    type: 'object',
+    required: ['total_entries', 'deadlines'],
+    title: 'CalendarDeadlineCollectionResponse',
+    description: 'Response model for calendar deadline aggregation results.'
+} as const;
+
+export const $CalendarDeadlineResponse = {
+    properties: {
+        date: {
+            type: 'string',
+            format: 'date-time',
+            title: 'Date'
+        },
+        missed: {
+            type: 'boolean',
+            title: 'Missed'
+        },
+        count: {
+            type: 'integer',
+            title: 'Count'
+        }
+    },
+    type: 'object',
+    required: ['date', 'missed', 'count'],
+    title: 'CalendarDeadlineResponse',
+    description: 'Represents aggregated deadline counts for a specific 
calendar time bucket.'
+} as const;
+
 export const $CalendarTimeRangeCollectionResponse = {
     properties: {
         total_entries: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index b44b9865237..7f1229ddb9c 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -3,7 +3,7 @@
 import type { CancelablePromise } from './core/CancelablePromise';
 import { OpenAPI } from './core/OpenAPI';
 import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
 
 export class AssetService {
     /**
@@ -5137,6 +5137,39 @@ export class CalendarService {
         });
     }
     
+    /**
+     * Get Calendar Deadlines
+     * Get aggregated deadline counts for a Dag, bucketed by deadline_time and 
missed status.
+     * @param data The data for the request.
+     * @param data.dagId
+     * @param data.granularity
+     * @param data.deadlineTimeGte
+     * @param data.deadlineTimeGt
+     * @param data.deadlineTimeLte
+     * @param data.deadlineTimeLt
+     * @returns CalendarDeadlineCollectionResponse Successful Response
+     * @throws ApiError
+     */
+    public static getCalendarDeadlines(data: GetCalendarDeadlinesData): 
CancelablePromise<GetCalendarDeadlinesResponse> {
+        return __request(OpenAPI, {
+            method: 'GET',
+            url: '/ui/calendar/{dag_id}/deadlines',
+            path: {
+                dag_id: data.dagId
+            },
+            query: {
+                granularity: data.granularity,
+                deadline_time_gte: data.deadlineTimeGte,
+                deadline_time_gt: data.deadlineTimeGt,
+                deadline_time_lte: data.deadlineTimeLte,
+                deadline_time_lt: data.deadlineTimeLt
+            },
+            errors: {
+                422: 'Validation Error'
+            }
+        });
+    }
+    
 }
 
 export class TeamsService {
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 a9d002e1e3f..2d8295b54b9 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
@@ -2182,6 +2182,23 @@ export type BaseNodeResponse = {
 
 export type type = 'join' | 'task' | 'asset-condition' | 'asset' | 
'asset-alias' | 'asset-name-ref' | 'asset-uri-ref' | 'dag' | 'sensor' | 
'trigger';
 
+/**
+ * Response model for calendar deadline aggregation results.
+ */
+export type CalendarDeadlineCollectionResponse = {
+    total_entries: number;
+    deadlines: Array<CalendarDeadlineResponse>;
+};
+
+/**
+ * Represents aggregated deadline counts for a specific calendar time bucket.
+ */
+export type CalendarDeadlineResponse = {
+    date: string;
+    missed: boolean;
+    count: number;
+};
+
 /**
  * Response model for calendar time range results.
  */
@@ -4697,6 +4714,17 @@ export type GetCalendarData = {
 
 export type GetCalendarResponse = CalendarTimeRangeCollectionResponse;
 
+export type GetCalendarDeadlinesData = {
+    dagId: string;
+    deadlineTimeGt?: string | null;
+    deadlineTimeGte?: string | null;
+    deadlineTimeLt?: string | null;
+    deadlineTimeLte?: string | null;
+    granularity?: 'hourly' | 'daily';
+};
+
+export type GetCalendarDeadlinesResponse = CalendarDeadlineCollectionResponse;
+
 export type ListTeamsData = {
     limit?: number;
     offset?: number;
@@ -8551,6 +8579,21 @@ export type $OpenApiTs = {
             };
         };
     };
+    '/ui/calendar/{dag_id}/deadlines': {
+        get: {
+            req: GetCalendarDeadlinesData;
+            res: {
+                /**
+                 * Successful Response
+                 */
+                200: CalendarDeadlineCollectionResponse;
+                /**
+                 * Validation Error
+                 */
+                422: HTTPValidationError;
+            };
+        };
+    };
     '/ui/teams': {
         get: {
             req: ListTeamsData;
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
index 7bd0d69153c..e24ba1814cd 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
@@ -23,13 +23,17 @@ import pendulum
 import pytest
 from sqlalchemy.orm import Session
 
+from airflow._shared.timezones import timezone
+from airflow.models.deadline import Deadline
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.sdk import CronPartitionTimetable
+from airflow.sdk.definitions.callback import AsyncCallback
 from airflow.utils.session import NEW_SESSION, provide_session
 from airflow.utils.state import DagRunState
+from airflow.utils.types import DagRunTriggeredByType, DagRunType
 
 from tests_common.test_utils.asserts import assert_queries_count
-from tests_common.test_utils.db import clear_db_dags, clear_db_runs
+from tests_common.test_utils.db import clear_db_dags, clear_db_deadline, 
clear_db_runs
 
 pytestmark = pytest.mark.db_test
 
@@ -320,3 +324,199 @@ class TestPartitionedCalendar:
         assert response.status_code == 200
         body = response.json()
         assert body == result
+
+
+_CALLBACK_PATH = 
"tests.unit.api_fastapi.core_api.routes.ui.test_calendar._noop_callback"
+
+
+async def _noop_callback(**kwargs):
+    """No-op callback used to satisfy Deadline creation in tests."""
+
+
+def _cb() -> AsyncCallback:
+    return AsyncCallback(_CALLBACK_PATH)
+
+
+class TestCalendarDeadlines:
+    """Tests for the GET /calendar/{dag_id}/deadlines endpoint."""
+
+    DAG_NAME = "test_deadlines_calendar_dag"
+
+    @pytest.fixture(autouse=True)
+    def setup_deadlines(self, dag_maker, session) -> None:
+        clear_db_deadline()
+        clear_db_runs()
+        clear_db_dags()
+
+        with dag_maker(self.DAG_NAME, serialized=True, session=session):
+            EmptyOperator(task_id="task")
+
+        run1 = dag_maker.create_dagrun(
+            run_id="run_active",
+            state=DagRunState.SUCCESS,
+            run_type=DagRunType.SCHEDULED,
+            logical_date=timezone.datetime(2025, 1, 1),
+            triggered_by=DagRunTriggeredByType.TEST,
+        )
+        session.add(
+            Deadline(
+                deadline_time=timezone.datetime(2025, 1, 1, 12, 0, 0),
+                callback=_cb(),
+                dagrun_id=run1.id,
+                deadline_alert_id=None,
+            )
+        )
+
+        run2 = dag_maker.create_dagrun(
+            run_id="run_missed",
+            state=DagRunState.SUCCESS,
+            run_type=DagRunType.SCHEDULED,
+            logical_date=timezone.datetime(2025, 1, 2),
+            triggered_by=DagRunTriggeredByType.TEST,
+        )
+        missed_dl = Deadline(
+            deadline_time=timezone.datetime(2025, 1, 2, 8, 0, 0),
+            callback=_cb(),
+            dagrun_id=run2.id,
+            deadline_alert_id=None,
+        )
+        missed_dl.missed = True
+        session.add(missed_dl)
+
+        run3 = dag_maker.create_dagrun(
+            run_id="run_multi",
+            state=DagRunState.RUNNING,
+            run_type=DagRunType.SCHEDULED,
+            logical_date=timezone.datetime(2025, 1, 3),
+            triggered_by=DagRunTriggeredByType.TEST,
+        )
+        for hour in (6, 18):
+            session.add(
+                Deadline(
+                    deadline_time=timezone.datetime(2025, 1, 1, hour, 0, 0),
+                    callback=_cb(),
+                    dagrun_id=run3.id,
+                    deadline_alert_id=None,
+                )
+            )
+
+        run4 = dag_maker.create_dagrun(
+            run_id="run_missed_jan1",
+            state=DagRunState.FAILED,
+            run_type=DagRunType.SCHEDULED,
+            logical_date=timezone.datetime(2025, 1, 4),
+            triggered_by=DagRunTriggeredByType.TEST,
+        )
+        missed_dl_jan1 = Deadline(
+            deadline_time=timezone.datetime(2025, 1, 1, 20, 0, 0),
+            callback=_cb(),
+            dagrun_id=run4.id,
+            deadline_alert_id=None,
+        )
+        missed_dl_jan1.missed = True
+        session.add(missed_dl_jan1)
+
+        dag_maker.sync_dagbag_to_db()
+        session.commit()
+
+    def teardown_method(self) -> None:
+        clear_db_deadline()
+        clear_db_runs()
+        clear_db_dags()
+
+    @pytest.mark.parametrize(
+        ("query_params", "expected"),
+        [
+            (
+                {},
+                {
+                    "total_entries": 3,
+                    "deadlines": [
+                        {"date": "2025-01-01T00:00:00Z", "missed": False, 
"count": 3},
+                        {"date": "2025-01-01T00:00:00Z", "missed": True, 
"count": 1},
+                        {"date": "2025-01-02T00:00:00Z", "missed": True, 
"count": 1},
+                    ],
+                },
+            ),
+            (
+                {
+                    "deadline_time_gte": "2025-01-01T00:00:00Z",
+                    "deadline_time_lte": "2025-01-01T23:59:59Z",
+                },
+                {
+                    "total_entries": 2,
+                    "deadlines": [
+                        {"date": "2025-01-01T00:00:00Z", "missed": False, 
"count": 3},
+                        {"date": "2025-01-01T00:00:00Z", "missed": True, 
"count": 1},
+                    ],
+                },
+            ),
+            (
+                {
+                    "deadline_time_gte": "2025-01-02T00:00:00Z",
+                    "deadline_time_lte": "2025-01-02T23:59:59Z",
+                },
+                {
+                    "total_entries": 1,
+                    "deadlines": [
+                        {"date": "2025-01-02T00:00:00Z", "missed": True, 
"count": 1},
+                    ],
+                },
+            ),
+            (
+                {"deadline_time_gte": "2025-06-01T00:00:00Z"},
+                {
+                    "total_entries": 0,
+                    "deadlines": [],
+                },
+            ),
+        ],
+    )
+    def test_daily_deadlines(self, test_client, query_params, expected):
+        response = test_client.get(f"/calendar/{self.DAG_NAME}/deadlines", 
params=query_params)
+        assert response.status_code == 200
+        assert response.json() == expected
+
+    @pytest.mark.parametrize(
+        ("query_params", "expected"),
+        [
+            (
+                {"granularity": "hourly"},
+                {
+                    "total_entries": 5,
+                    "deadlines": [
+                        {"date": "2025-01-01T06:00:00Z", "missed": False, 
"count": 1},
+                        {"date": "2025-01-01T12:00:00Z", "missed": False, 
"count": 1},
+                        {"date": "2025-01-01T18:00:00Z", "missed": False, 
"count": 1},
+                        {"date": "2025-01-01T20:00:00Z", "missed": True, 
"count": 1},
+                        {"date": "2025-01-02T08:00:00Z", "missed": True, 
"count": 1},
+                    ],
+                },
+            ),
+            (
+                {
+                    "granularity": "hourly",
+                    "deadline_time_gte": "2025-01-01T10:00:00Z",
+                    "deadline_time_lte": "2025-01-01T20:00:00Z",
+                },
+                {
+                    "total_entries": 3,
+                    "deadlines": [
+                        {"date": "2025-01-01T12:00:00Z", "missed": False, 
"count": 1},
+                        {"date": "2025-01-01T18:00:00Z", "missed": False, 
"count": 1},
+                        {"date": "2025-01-01T20:00:00Z", "missed": True, 
"count": 1},
+                    ],
+                },
+            ),
+        ],
+    )
+    def test_hourly_deadlines(self, test_client, query_params, expected):
+        response = test_client.get(f"/calendar/{self.DAG_NAME}/deadlines", 
params=query_params)
+        assert response.status_code == 200
+        assert response.json() == expected
+
+    def test_unknown_dag_returns_empty(self, test_client):
+        response = test_client.get("/calendar/nonexistent_dag/deadlines")
+        assert response.status_code == 200
+        body = response.json()
+        assert body == {"total_entries": 0, "deadlines": []}


Reply via email to