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

pierrejeambrun 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 58ceb32391 AIP-84 Get Import Error / Get Import Errors (#43637)
58ceb32391 is described below

commit 58ceb323916d8412dafbcd03c1e99f8c36e12faa
Author: LIU ZHE YOU <[email protected]>
AuthorDate: Tue Nov 5 22:30:38 2024 +0800

    AIP-84 Get Import Error / Get Import Errors (#43637)
    
    * AIP-84 Get Import Error
    
    * Small adjustments
    
    ---------
    
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../endpoints/import_error_endpoint.py             |   3 +
 airflow/api_fastapi/common/parameters.py           |   2 +
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 138 +++++++++++++
 .../api_fastapi/core_api/routes/public/__init__.py |   2 +
 .../core_api/routes/public/import_error.py         | 105 ++++++++++
 .../core_api/serializers/import_error.py           |  39 ++++
 airflow/ui/openapi-gen/queries/common.ts           |  45 +++++
 airflow/ui/openapi-gen/queries/prefetch.ts         |  54 +++++
 airflow/ui/openapi-gen/queries/queries.ts          |  67 +++++++
 airflow/ui/openapi-gen/queries/suspense.ts         |  67 +++++++
 airflow/ui/openapi-gen/requests/schemas.gen.ts     |  46 +++++
 airflow/ui/openapi-gen/requests/services.gen.ts    |  61 ++++++
 airflow/ui/openapi-gen/requests/types.gen.ts       |  82 ++++++++
 .../core_api/routes/public/test_import_error.py    | 219 +++++++++++++++++++++
 14 files changed, 930 insertions(+)

diff --git a/airflow/api_connexion/endpoints/import_error_endpoint.py 
b/airflow/api_connexion/endpoints/import_error_endpoint.py
index 76b706eac1..633dd0bebd 100644
--- a/airflow/api_connexion/endpoints/import_error_endpoint.py
+++ b/airflow/api_connexion/endpoints/import_error_endpoint.py
@@ -31,6 +31,7 @@ from airflow.api_connexion.schemas.error_schema import (
 from airflow.auth.managers.models.resource_details import AccessView, 
DagDetails
 from airflow.models.dag import DagModel
 from airflow.models.errors import ParseImportError
+from airflow.utils.api_migration import mark_fastapi_migration_done
 from airflow.utils.session import NEW_SESSION, provide_session
 from airflow.www.extensions.init_auth_manager import get_auth_manager
 
@@ -41,6 +42,7 @@ if TYPE_CHECKING:
     from airflow.auth.managers.models.batch_apis import IsAuthorizedDagRequest
 
 
+@mark_fastapi_migration_done
 @security.requires_access_view(AccessView.IMPORT_ERRORS)
 @provide_session
 def get_import_error(*, import_error_id: int, session: Session = NEW_SESSION) 
-> APIResponse:
@@ -72,6 +74,7 @@ def get_import_error(*, import_error_id: int, session: 
Session = NEW_SESSION) ->
     return import_error_schema.dump(error)
 
 
+@mark_fastapi_migration_done
 @security.requires_access_view(AccessView.IMPORT_ERRORS)
 @format_parameters({"limit": check_limit})
 @provide_session
diff --git a/airflow/api_fastapi/common/parameters.py 
b/airflow/api_fastapi/common/parameters.py
index bd65017637..218077ca59 100644
--- a/airflow/api_fastapi/common/parameters.py
+++ b/airflow/api_fastapi/common/parameters.py
@@ -32,6 +32,7 @@ from airflow.models import Base, Connection
 from airflow.models.dag import DagModel, DagTag
 from airflow.models.dagrun import DagRun
 from airflow.models.dagwarning import DagWarning, DagWarningType
+from airflow.models.errors import ParseImportError
 from airflow.utils import timezone
 from airflow.utils.state import DagRunState
 
@@ -158,6 +159,7 @@ class SortParam(BaseParam[str]):
         "last_run_state": DagRun.state,
         "last_run_start_date": DagRun.start_date,
         "connection_id": Connection.conn_id,
+        "import_error_id": ParseImportError.id,
     }
 
     def __init__(
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml 
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index e844cbceeb..28e3888480 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -1538,6 +1538,105 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /public/importErrors/{import_error_id}:
+    get:
+      tags:
+      - Import Error
+      summary: Get Import Error
+      description: Get an import error.
+      operationId: get_import_error
+      parameters:
+      - name: import_error_id
+        in: path
+        required: true
+        schema:
+          type: integer
+          title: Import Error Id
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ImportErrorResponse'
+        '401':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Unauthorized
+        '403':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Forbidden
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Not Found
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+  /public/importErrors/:
+    get:
+      tags:
+      - Import Error
+      summary: Get Import Errors
+      description: Get all import errors.
+      operationId: get_import_errors
+      parameters:
+      - name: limit
+        in: query
+        required: false
+        schema:
+          type: integer
+          default: 100
+          title: Limit
+      - name: offset
+        in: query
+        required: false
+        schema:
+          type: integer
+          default: 0
+          title: Offset
+      - name: order_by
+        in: query
+        required: false
+        schema:
+          type: string
+          default: id
+          title: Order By
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ImportErrorCollectionResponse'
+        '401':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Unauthorized
+        '403':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Forbidden
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
   /public/monitor/health:
     get:
       tags:
@@ -3454,6 +3553,45 @@ components:
       - task_instance_states
       title: HistoricalMetricDataResponse
       description: Historical Metric Data serializer for responses.
+    ImportErrorCollectionResponse:
+      properties:
+        import_errors:
+          items:
+            $ref: '#/components/schemas/ImportErrorResponse'
+          type: array
+          title: Import Errors
+        total_entries:
+          type: integer
+          title: Total Entries
+      type: object
+      required:
+      - import_errors
+      - total_entries
+      title: ImportErrorCollectionResponse
+      description: Import Error Collection Response.
+    ImportErrorResponse:
+      properties:
+        import_error_id:
+          type: integer
+          title: Import Error Id
+        timestamp:
+          type: string
+          format: date-time
+          title: Timestamp
+        filename:
+          type: string
+          title: Filename
+        stack_trace:
+          type: string
+          title: Stack Trace
+      type: object
+      required:
+      - import_error_id
+      - timestamp
+      - filename
+      - stack_trace
+      title: ImportErrorResponse
+      description: Import Error Response.
     JobResponse:
       properties:
         id:
diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py 
b/airflow/api_fastapi/core_api/routes/public/__init__.py
index a443f5a28a..68caa2d775 100644
--- a/airflow/api_fastapi/core_api/routes/public/__init__.py
+++ b/airflow/api_fastapi/core_api/routes/public/__init__.py
@@ -25,6 +25,7 @@ from airflow.api_fastapi.core_api.routes.public.dag_sources 
import dag_sources_r
 from airflow.api_fastapi.core_api.routes.public.dag_warning import 
dag_warning_router
 from airflow.api_fastapi.core_api.routes.public.dags import dags_router
 from airflow.api_fastapi.core_api.routes.public.event_logs import 
event_logs_router
+from airflow.api_fastapi.core_api.routes.public.import_error import 
import_error_router
 from airflow.api_fastapi.core_api.routes.public.monitor import monitor_router
 from airflow.api_fastapi.core_api.routes.public.plugins import plugins_router
 from airflow.api_fastapi.core_api.routes.public.pools import pools_router
@@ -43,6 +44,7 @@ public_router.include_router(dag_run_router)
 public_router.include_router(dag_sources_router)
 public_router.include_router(dags_router)
 public_router.include_router(event_logs_router)
+public_router.include_router(import_error_router)
 public_router.include_router(monitor_router)
 public_router.include_router(dag_warning_router)
 public_router.include_router(plugins_router)
diff --git a/airflow/api_fastapi/core_api/routes/public/import_error.py 
b/airflow/api_fastapi/core_api/routes/public/import_error.py
new file mode 100644
index 0000000000..9007d6ff89
--- /dev/null
+++ b/airflow/api_fastapi/core_api/routes/public/import_error.py
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from fastapi import Depends, HTTPException, status
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+from typing_extensions import Annotated
+
+from airflow.api_fastapi.common.db.common import (
+    get_session,
+    paginated_select,
+)
+from airflow.api_fastapi.common.parameters import (
+    QueryLimit,
+    QueryOffset,
+    SortParam,
+)
+from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
+from airflow.api_fastapi.core_api.serializers.import_error import (
+    ImportErrorCollectionResponse,
+    ImportErrorResponse,
+)
+from airflow.models.errors import ParseImportError
+
+import_error_router = AirflowRouter(tags=["Import Error"], 
prefix="/importErrors")
+
+
+@import_error_router.get(
+    "/{import_error_id}",
+    responses=create_openapi_http_exception_doc(
+        [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN, 
status.HTTP_404_NOT_FOUND]
+    ),
+)
+async def get_import_error(
+    import_error_id: int,
+    session: Annotated[Session, Depends(get_session)],
+) -> ImportErrorResponse:
+    """Get an import error."""
+    error = session.scalar(select(ParseImportError).where(ParseImportError.id 
== import_error_id))
+    if error is None:
+        raise HTTPException(404, f"The ImportError with import_error_id: 
`{import_error_id}` was not found")
+
+    return ImportErrorResponse.model_validate(
+        error,
+        from_attributes=True,
+    )
+
+
+@import_error_router.get(
+    "/",
+    responses=create_openapi_http_exception_doc([status.HTTP_401_UNAUTHORIZED, 
status.HTTP_403_FORBIDDEN]),
+)
+async def get_import_errors(
+    limit: QueryLimit,
+    offset: QueryOffset,
+    order_by: Annotated[
+        SortParam,
+        Depends(
+            SortParam(
+                [
+                    "id",
+                    "import_error_id",
+                    "timestamp",
+                    "filename",
+                    "stacktrace",
+                ],
+                ParseImportError,
+            ).dynamic_depends()
+        ),
+    ],
+    session: Annotated[Session, Depends(get_session)],
+) -> ImportErrorCollectionResponse:
+    """Get all import errors."""
+    import_errors_select, total_entries = paginated_select(
+        select(ParseImportError),
+        [],
+        order_by,
+        offset,
+        limit,
+        session,
+    )
+    import_errors = session.scalars(import_errors_select).all()
+
+    return ImportErrorCollectionResponse(
+        import_errors=[
+            ImportErrorResponse.model_validate(error, from_attributes=True) 
for error in import_errors
+        ],
+        total_entries=total_entries,
+    )
diff --git a/airflow/api_fastapi/core_api/serializers/import_error.py 
b/airflow/api_fastapi/core_api/serializers/import_error.py
new file mode 100644
index 0000000000..ebc65e23ec
--- /dev/null
+++ b/airflow/api_fastapi/core_api/serializers/import_error.py
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import datetime
+
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class ImportErrorResponse(BaseModel):
+    """Import Error Response."""
+
+    id: int = Field(alias="import_error_id")
+    timestamp: datetime
+    filename: str
+    stacktrace: str = Field(alias="stack_trace")
+
+    model_config = ConfigDict(populate_by_name=True)
+
+
+class ImportErrorCollectionResponse(BaseModel):
+    """Import Error Collection Response."""
+
+    import_errors: list[ImportErrorResponse]
+    total_entries: int
diff --git a/airflow/ui/openapi-gen/queries/common.ts 
b/airflow/ui/openapi-gen/queries/common.ts
index 1248a77ce1..36ea524e01 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -12,6 +12,7 @@ import {
   DagsService,
   DashboardService,
   EventLogService,
+  ImportErrorService,
   MonitorService,
   PluginService,
   PoolService,
@@ -421,6 +422,50 @@ export const UseEventLogServiceGetEventLogsKeyFn = (
     },
   ]),
 ];
+export type ImportErrorServiceGetImportErrorDefaultResponse = Awaited<
+  ReturnType<typeof ImportErrorService.getImportError>
+>;
+export type ImportErrorServiceGetImportErrorQueryResult<
+  TData = ImportErrorServiceGetImportErrorDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useImportErrorServiceGetImportErrorKey =
+  "ImportErrorServiceGetImportError";
+export const UseImportErrorServiceGetImportErrorKeyFn = (
+  {
+    importErrorId,
+  }: {
+    importErrorId: number;
+  },
+  queryKey?: Array<unknown>,
+) => [
+  useImportErrorServiceGetImportErrorKey,
+  ...(queryKey ?? [{ importErrorId }]),
+];
+export type ImportErrorServiceGetImportErrorsDefaultResponse = Awaited<
+  ReturnType<typeof ImportErrorService.getImportErrors>
+>;
+export type ImportErrorServiceGetImportErrorsQueryResult<
+  TData = ImportErrorServiceGetImportErrorsDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useImportErrorServiceGetImportErrorsKey =
+  "ImportErrorServiceGetImportErrors";
+export const UseImportErrorServiceGetImportErrorsKeyFn = (
+  {
+    limit,
+    offset,
+    orderBy,
+  }: {
+    limit?: number;
+    offset?: number;
+    orderBy?: string;
+  } = {},
+  queryKey?: Array<unknown>,
+) => [
+  useImportErrorServiceGetImportErrorsKey,
+  ...(queryKey ?? [{ limit, offset, orderBy }]),
+];
 export type MonitorServiceGetHealthDefaultResponse = Awaited<
   ReturnType<typeof MonitorService.getHealth>
 >;
diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow/ui/openapi-gen/queries/prefetch.ts
index bf6ad800be..6c41b7a5a1 100644
--- a/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -12,6 +12,7 @@ import {
   DagsService,
   DashboardService,
   EventLogService,
+  ImportErrorService,
   MonitorService,
   PluginService,
   PoolService,
@@ -543,6 +544,59 @@ export const prefetchUseEventLogServiceGetEventLogs = (
         tryNumber,
       }),
   });
+/**
+ * Get Import Error
+ * Get an import error.
+ * @param data The data for the request.
+ * @param data.importErrorId
+ * @returns ImportErrorResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseImportErrorServiceGetImportError = (
+  queryClient: QueryClient,
+  {
+    importErrorId,
+  }: {
+    importErrorId: number;
+  },
+) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UseImportErrorServiceGetImportErrorKeyFn({
+      importErrorId,
+    }),
+    queryFn: () => ImportErrorService.getImportError({ importErrorId }),
+  });
+/**
+ * Get Import Errors
+ * Get all import errors.
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @param data.orderBy
+ * @returns ImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseImportErrorServiceGetImportErrors = (
+  queryClient: QueryClient,
+  {
+    limit,
+    offset,
+    orderBy,
+  }: {
+    limit?: number;
+    offset?: number;
+    orderBy?: string;
+  } = {},
+) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({
+      limit,
+      offset,
+      orderBy,
+    }),
+    queryFn: () =>
+      ImportErrorService.getImportErrors({ limit, offset, orderBy }),
+  });
 /**
  * Get Health
  * @returns HealthInfoSchema Successful Response
diff --git a/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow/ui/openapi-gen/queries/queries.ts
index 70796be401..f4b7c41195 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -17,6 +17,7 @@ import {
   DagsService,
   DashboardService,
   EventLogService,
+  ImportErrorService,
   MonitorService,
   PluginService,
   PoolService,
@@ -671,6 +672,72 @@ export const useEventLogServiceGetEventLogs = <
       }) as TData,
     ...options,
   });
+/**
+ * Get Import Error
+ * Get an import error.
+ * @param data The data for the request.
+ * @param data.importErrorId
+ * @returns ImportErrorResponse Successful Response
+ * @throws ApiError
+ */
+export const useImportErrorServiceGetImportError = <
+  TData = Common.ImportErrorServiceGetImportErrorDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    importErrorId,
+  }: {
+    importErrorId: number;
+  },
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UseImportErrorServiceGetImportErrorKeyFn(
+      { importErrorId },
+      queryKey,
+    ),
+    queryFn: () =>
+      ImportErrorService.getImportError({ importErrorId }) as TData,
+    ...options,
+  });
+/**
+ * Get Import Errors
+ * Get all import errors.
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @param data.orderBy
+ * @returns ImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const useImportErrorServiceGetImportErrors = <
+  TData = Common.ImportErrorServiceGetImportErrorsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    limit,
+    offset,
+    orderBy,
+  }: {
+    limit?: number;
+    offset?: number;
+    orderBy?: string;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn(
+      { limit, offset, orderBy },
+      queryKey,
+    ),
+    queryFn: () =>
+      ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData,
+    ...options,
+  });
 /**
  * Get Health
  * @returns HealthInfoSchema Successful Response
diff --git a/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow/ui/openapi-gen/queries/suspense.ts
index 4f75c2ba0c..2870605672 100644
--- a/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow/ui/openapi-gen/queries/suspense.ts
@@ -12,6 +12,7 @@ import {
   DagsService,
   DashboardService,
   EventLogService,
+  ImportErrorService,
   MonitorService,
   PluginService,
   PoolService,
@@ -657,6 +658,72 @@ export const useEventLogServiceGetEventLogsSuspense = <
       }) as TData,
     ...options,
   });
+/**
+ * Get Import Error
+ * Get an import error.
+ * @param data The data for the request.
+ * @param data.importErrorId
+ * @returns ImportErrorResponse Successful Response
+ * @throws ApiError
+ */
+export const useImportErrorServiceGetImportErrorSuspense = <
+  TData = Common.ImportErrorServiceGetImportErrorDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    importErrorId,
+  }: {
+    importErrorId: number;
+  },
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UseImportErrorServiceGetImportErrorKeyFn(
+      { importErrorId },
+      queryKey,
+    ),
+    queryFn: () =>
+      ImportErrorService.getImportError({ importErrorId }) as TData,
+    ...options,
+  });
+/**
+ * Get Import Errors
+ * Get all import errors.
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @param data.orderBy
+ * @returns ImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const useImportErrorServiceGetImportErrorsSuspense = <
+  TData = Common.ImportErrorServiceGetImportErrorsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    limit,
+    offset,
+    orderBy,
+  }: {
+    limit?: number;
+    offset?: number;
+    orderBy?: string;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn(
+      { limit, offset, orderBy },
+      queryKey,
+    ),
+    queryFn: () =>
+      ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData,
+    ...options,
+  });
 /**
  * Get Health
  * @returns HealthInfoSchema Successful Response
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 53272eae2e..517743af17 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -1740,6 +1740,52 @@ export const $HistoricalMetricDataResponse = {
   description: "Historical Metric Data serializer for responses.",
 } as const;
 
+export const $ImportErrorCollectionResponse = {
+  properties: {
+    import_errors: {
+      items: {
+        $ref: "#/components/schemas/ImportErrorResponse",
+      },
+      type: "array",
+      title: "Import Errors",
+    },
+    total_entries: {
+      type: "integer",
+      title: "Total Entries",
+    },
+  },
+  type: "object",
+  required: ["import_errors", "total_entries"],
+  title: "ImportErrorCollectionResponse",
+  description: "Import Error Collection Response.",
+} as const;
+
+export const $ImportErrorResponse = {
+  properties: {
+    import_error_id: {
+      type: "integer",
+      title: "Import Error Id",
+    },
+    timestamp: {
+      type: "string",
+      format: "date-time",
+      title: "Timestamp",
+    },
+    filename: {
+      type: "string",
+      title: "Filename",
+    },
+    stack_trace: {
+      type: "string",
+      title: "Stack Trace",
+    },
+  },
+  type: "object",
+  required: ["import_error_id", "timestamp", "filename", "stack_trace"],
+  title: "ImportErrorResponse",
+  description: "Import Error Response.",
+} as const;
+
 export const $JobResponse = {
   properties: {
     id: {
diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow/ui/openapi-gen/requests/services.gen.ts
index fa5c7739c9..5597b0a6a9 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -53,6 +53,10 @@ import type {
   GetEventLogResponse,
   GetEventLogsData,
   GetEventLogsResponse,
+  GetImportErrorData,
+  GetImportErrorResponse,
+  GetImportErrorsData,
+  GetImportErrorsResponse,
   GetHealthResponse,
   ListDagWarningsData,
   ListDagWarningsResponse,
@@ -865,6 +869,63 @@ export class EventLogService {
   }
 }
 
+export class ImportErrorService {
+  /**
+   * Get Import Error
+   * Get an import error.
+   * @param data The data for the request.
+   * @param data.importErrorId
+   * @returns ImportErrorResponse Successful Response
+   * @throws ApiError
+   */
+  public static getImportError(
+    data: GetImportErrorData,
+  ): CancelablePromise<GetImportErrorResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/public/importErrors/{import_error_id}",
+      path: {
+        import_error_id: data.importErrorId,
+      },
+      errors: {
+        401: "Unauthorized",
+        403: "Forbidden",
+        404: "Not Found",
+        422: "Validation Error",
+      },
+    });
+  }
+
+  /**
+   * Get Import Errors
+   * Get all import errors.
+   * @param data The data for the request.
+   * @param data.limit
+   * @param data.offset
+   * @param data.orderBy
+   * @returns ImportErrorCollectionResponse Successful Response
+   * @throws ApiError
+   */
+  public static getImportErrors(
+    data: GetImportErrorsData = {},
+  ): CancelablePromise<GetImportErrorsResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/public/importErrors/",
+      query: {
+        limit: data.limit,
+        offset: data.offset,
+        order_by: data.orderBy,
+      },
+      errors: {
+        401: "Unauthorized",
+        403: "Forbidden",
+        422: "Validation Error",
+      },
+    });
+  }
+}
+
 export class MonitorService {
   /**
    * Get Health
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow/ui/openapi-gen/requests/types.gen.ts
index 909b78dd62..e3071b6493 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -416,6 +416,24 @@ export type HistoricalMetricDataResponse = {
   task_instance_states: 
airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState;
 };
 
+/**
+ * Import Error Collection Response.
+ */
+export type ImportErrorCollectionResponse = {
+  import_errors: Array<ImportErrorResponse>;
+  total_entries: number;
+};
+
+/**
+ * Import Error Response.
+ */
+export type ImportErrorResponse = {
+  import_error_id: number;
+  timestamp: string;
+  filename: string;
+  stack_trace: string;
+};
+
 /**
  * Job serializer for responses.
  */
@@ -878,6 +896,20 @@ export type GetEventLogsData = {
 
 export type GetEventLogsResponse = EventLogCollectionResponse;
 
+export type GetImportErrorData = {
+  importErrorId: number;
+};
+
+export type GetImportErrorResponse = ImportErrorResponse;
+
+export type GetImportErrorsData = {
+  limit?: number;
+  offset?: number;
+  orderBy?: string;
+};
+
+export type GetImportErrorsResponse = ImportErrorCollectionResponse;
+
 export type GetHealthResponse = HealthInfoSchema;
 
 export type ListDagWarningsData = {
@@ -1651,6 +1683,56 @@ export type $OpenApiTs = {
       };
     };
   };
+  "/public/importErrors/{import_error_id}": {
+    get: {
+      req: GetImportErrorData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: ImportErrorResponse;
+        /**
+         * Unauthorized
+         */
+        401: HTTPExceptionResponse;
+        /**
+         * Forbidden
+         */
+        403: HTTPExceptionResponse;
+        /**
+         * Not Found
+         */
+        404: HTTPExceptionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
+  "/public/importErrors/": {
+    get: {
+      req: GetImportErrorsData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: ImportErrorCollectionResponse;
+        /**
+         * Unauthorized
+         */
+        401: HTTPExceptionResponse;
+        /**
+         * Forbidden
+         */
+        403: HTTPExceptionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
   "/public/monitor/health": {
     get: {
       res: {
diff --git a/tests/api_fastapi/core_api/routes/public/test_import_error.py 
b/tests/api_fastapi/core_api/routes/public/test_import_error.py
new file mode 100644
index 0000000000..4271c05b6b
--- /dev/null
+++ b/tests/api_fastapi/core_api/routes/public/test_import_error.py
@@ -0,0 +1,219 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import datetime, timezone
+
+import pytest
+
+from airflow.models.errors import ParseImportError
+from airflow.utils.session import provide_session
+
+from tests_common.test_utils.db import clear_db_import_errors
+
+pytestmark = pytest.mark.db_test
+
+FILENAME1 = "test_filename1.py"
+FILENAME2 = "test_filename2.py"
+FILENAME3 = "Lorem_ipsum.py"
+STACKTRACE1 = "test_stacktrace1"
+STACKTRACE2 = "test_stacktrace2"
+STACKTRACE3 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+TIMESTAMP1 = datetime(2024, 6, 15, 1, 0, tzinfo=timezone.utc)
+TIMESTAMP2 = datetime(2024, 6, 15, 5, 0, tzinfo=timezone.utc)
+TIMESTAMP3 = datetime(2024, 6, 15, 3, 0, tzinfo=timezone.utc)
+IMPORT_ERROR_NON_EXISTED_ID = 9999
+IMPORT_ERROR_NON_EXISTED_KEY = "non_existed_key"
+
+
+class TestImportErrorEndpoint:
+    """Common class for /public/importErrors related unit tests."""
+
+    @staticmethod
+    def _clear_db():
+        clear_db_import_errors()
+
+    @pytest.fixture(autouse=True)
+    @provide_session
+    def setup(self, session=None) -> dict[str, ParseImportError]:
+        """
+        Setup method which is run before every test.
+        """
+        self._clear_db()
+        import_error1 = ParseImportError(
+            filename=FILENAME1,
+            stacktrace=STACKTRACE1,
+            timestamp=TIMESTAMP1,
+        )
+        import_error2 = ParseImportError(
+            filename=FILENAME2,
+            stacktrace=STACKTRACE2,
+            timestamp=TIMESTAMP2,
+        )
+        import_error3 = ParseImportError(
+            filename=FILENAME3,
+            stacktrace=STACKTRACE3,
+            timestamp=TIMESTAMP3,
+        )
+        session.add_all([import_error1, import_error2, import_error3])
+        session.commit()
+        return {FILENAME1: import_error1, FILENAME2: import_error2, FILENAME3: 
import_error3}
+
+    def teardown_method(self) -> None:
+        self._clear_db()
+
+
+class TestGetImportError(TestImportErrorEndpoint):
+    @pytest.mark.parametrize(
+        "import_error_key, expected_status_code, expected_body",
+        [
+            (
+                FILENAME1,
+                200,
+                {
+                    "import_error_id": 1,
+                    "timestamp": TIMESTAMP1,
+                    "filename": FILENAME1,
+                    "stack_trace": STACKTRACE1,
+                },
+            ),
+            (
+                FILENAME2,
+                200,
+                {
+                    "import_error_id": 2,
+                    "timestamp": TIMESTAMP2,
+                    "filename": FILENAME2,
+                    "stack_trace": STACKTRACE2,
+                },
+            ),
+            (IMPORT_ERROR_NON_EXISTED_KEY, 404, {}),
+        ],
+    )
+    def test_get_import_error(
+        self, test_client, setup, import_error_key, expected_status_code, 
expected_body
+    ):
+        import_error: ParseImportError | None = setup.get(import_error_key)
+        import_error_id = import_error.id if import_error else 
IMPORT_ERROR_NON_EXISTED_ID
+        response = test_client.get(f"/public/importErrors/{import_error_id}")
+        assert response.status_code == expected_status_code
+        if expected_status_code != 200:
+            return
+        expected_json = {
+            "import_error_id": import_error_id,
+            "timestamp": 
expected_body["timestamp"].isoformat().replace("+00:00", "Z"),
+            "filename": expected_body["filename"],
+            "stack_trace": expected_body["stack_trace"],
+        }
+        assert response.json() == expected_json
+
+
+class TestGetImportErrors(TestImportErrorEndpoint):
+    @pytest.mark.parametrize(
+        "query_params, expected_status_code, expected_total_entries, 
expected_filenames",
+        [
+            (
+                {},
+                200,
+                3,
+                [FILENAME1, FILENAME2, FILENAME3],
+            ),
+            # offset, limit
+            (
+                {"limit": 1, "offset": 1},
+                200,
+                3,
+                [FILENAME2],
+            ),
+            (
+                {"limit": 1, "offset": 2},
+                200,
+                3,
+                [FILENAME3],
+            ),
+            # order_by
+            (
+                {"order_by": "-filename"},
+                200,
+                3,
+                [FILENAME2, FILENAME1, FILENAME3],
+            ),
+            (
+                {"order_by": "timestamp"},
+                200,
+                3,
+                [FILENAME1, FILENAME3, FILENAME2],
+            ),
+            (
+                {"order_by": "import_error_id"},
+                200,
+                3,
+                [FILENAME1, FILENAME2, FILENAME3],
+            ),
+            (
+                {"order_by": "-import_error_id"},
+                200,
+                3,
+                [FILENAME3, FILENAME2, FILENAME1],
+            ),
+            # invalid order_by
+            (
+                {"order_by": "invalid_order_by"},
+                400,
+                0,
+                [],
+            ),
+            # combination of query parameters
+            (
+                {"limit": 2, "offset": 1, "order_by": "-filename"},
+                200,
+                3,
+                [FILENAME1, FILENAME3],
+            ),
+            (
+                {"limit": 1, "offset": 2, "order_by": "-filename"},
+                200,
+                3,
+                [FILENAME3],
+            ),
+            (
+                {"limit": 5, "offset": 1, "order_by": "timestamp"},
+                200,
+                3,
+                [FILENAME3, FILENAME2],
+            ),
+        ],
+    )
+    def test_get_import_errors(
+        self,
+        test_client,
+        query_params,
+        expected_status_code,
+        expected_total_entries,
+        expected_filenames,
+    ):
+        response = test_client.get("/public/importErrors", params=query_params)
+
+        assert response.status_code == expected_status_code
+        if expected_status_code != 200:
+            return
+
+        response_json = response.json()
+        assert response_json["total_entries"] == expected_total_entries
+        assert [
+            import_error["filename"] for import_error in 
response_json["import_errors"]
+        ] == expected_filenames


Reply via email to