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 35142a907f AIP-84 Get Plugins (#43125)
35142a907f is described below

commit 35142a907f5f37c9a4d0fb2f402831703f258ad3
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Tue Oct 22 23:34:27 2024 +0800

    AIP-84 Get Plugins (#43125)
    
    * AIP-84 Get Plugins
    
    * Explicit types
    
    * Handle extra unknown properties
    
    * Fix CI
---
 airflow/api_connexion/endpoints/plugin_endpoint.py |   2 +
 airflow/api_fastapi/common/parameters.py           |   4 +-
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 202 +++++++++++++++++
 .../api_fastapi/core_api/routes/public/__init__.py |   2 +
 .../core_api/routes/public/plugins.py}             |  33 +--
 .../api_fastapi/core_api/serializers/plugins.py    |  93 ++++++++
 airflow/ui/openapi-gen/queries/common.ts           |  19 ++
 airflow/ui/openapi-gen/queries/prefetch.ts         |  23 ++
 airflow/ui/openapi-gen/queries/queries.ts          |  32 +++
 airflow/ui/openapi-gen/queries/suspense.ts         |  32 +++
 airflow/ui/openapi-gen/requests/schemas.gen.ts     | 247 +++++++++++++++++++++
 airflow/ui/openapi-gen/requests/services.gen.ts    |  28 +++
 airflow/ui/openapi-gen/requests/types.gen.ts       |  81 +++++++
 .../core_api/routes/public/test_plugins.py         |  64 ++++++
 14 files changed, 845 insertions(+), 17 deletions(-)

diff --git a/airflow/api_connexion/endpoints/plugin_endpoint.py 
b/airflow/api_connexion/endpoints/plugin_endpoint.py
index 5a100fd6d5..97f9d8c6c4 100644
--- a/airflow/api_connexion/endpoints/plugin_endpoint.py
+++ b/airflow/api_connexion/endpoints/plugin_endpoint.py
@@ -23,11 +23,13 @@ from airflow.api_connexion.parameters import check_limit, 
format_parameters
 from airflow.api_connexion.schemas.plugin_schema import PluginCollection, 
plugin_collection_schema
 from airflow.auth.managers.models.resource_details import AccessView
 from airflow.plugins_manager import get_plugin_info
+from airflow.utils.api_migration import mark_fastapi_migration_done
 
 if TYPE_CHECKING:
     from airflow.api_connexion.types import APIResponse
 
 
+@mark_fastapi_migration_done
 @security.requires_access_view(AccessView.PLUGINS)
 @format_parameters({"limit": check_limit})
 def get_plugins(*, limit: int, offset: int = 0) -> APIResponse:
diff --git a/airflow/api_fastapi/common/parameters.py 
b/airflow/api_fastapi/common/parameters.py
index 9b265c7583..7137a0a124 100644
--- a/airflow/api_fastapi/common/parameters.py
+++ b/airflow/api_fastapi/common/parameters.py
@@ -197,9 +197,9 @@ class SortParam(BaseParam[str]):
         primary_key_column = self.get_primary_key_column()
 
         if self.value[0] == "-":
-            return select.order_by(nullscheck, column.desc(), 
primary_key_column)
+            return select.order_by(nullscheck, column.desc(), 
primary_key_column.desc())
         else:
-            return select.order_by(nullscheck, column.asc(), 
primary_key_column)
+            return select.order_by(nullscheck, column.asc(), 
primary_key_column.asc())
 
     def get_primary_key_column(self) -> Column:
         """Get the primary key column of the model of SortParam object."""
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml 
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 0f862e496e..00fea1651a 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -1258,8 +1258,89 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /public/plugins/:
+    get:
+      tags:
+      - Plugin
+      summary: Get Plugins
+      operationId: get_plugins
+      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
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/PluginCollectionResponse'
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
 components:
   schemas:
+    AppBuilderMenuItemResponse:
+      properties:
+        name:
+          type: string
+          title: Name
+        href:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Href
+        category:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Category
+      additionalProperties: true
+      type: object
+      required:
+      - name
+      title: AppBuilderMenuItemResponse
+      description: Serializer for AppBuilder Menu Item responses.
+    AppBuilderViewResponse:
+      properties:
+        name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Name
+        category:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Category
+        view:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: View
+        label:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Label
+      additionalProperties: true
+      type: object
+      title: AppBuilderViewResponse
+      description: Serializer for AppBuilder View responses.
     BaseInfoSchema:
       properties:
         status:
@@ -1966,6 +2047,25 @@ components:
       title: DagTagPydantic
       description: Serializable representation of the DagTag ORM 
SqlAlchemyModel used
         by internal API.
+    FastAPIAppResponse:
+      properties:
+        app:
+          type: string
+          title: App
+        url_prefix:
+          type: string
+          title: Url Prefix
+        name:
+          type: string
+          title: Name
+      additionalProperties: true
+      type: object
+      required:
+      - app
+      - url_prefix
+      - name
+      title: FastAPIAppResponse
+      description: Serializer for Plugin FastAPI App responses.
     HTTPExceptionResponse:
       properties:
         detail:
@@ -2020,6 +2120,108 @@ components:
       - task_instance_states
       title: HistoricalMetricDataResponse
       description: Historical Metric Data serializer for responses.
+    PluginCollectionResponse:
+      properties:
+        plugins:
+          items:
+            $ref: '#/components/schemas/PluginResponse'
+          type: array
+          title: Plugins
+        total_entries:
+          type: integer
+          title: Total Entries
+      type: object
+      required:
+      - plugins
+      - total_entries
+      title: PluginCollectionResponse
+      description: Plugin Collection serializer.
+    PluginResponse:
+      properties:
+        name:
+          type: string
+          title: Name
+        hooks:
+          items:
+            type: string
+          type: array
+          title: Hooks
+        executors:
+          items:
+            type: string
+          type: array
+          title: Executors
+        macros:
+          items:
+            type: string
+          type: array
+          title: Macros
+        flask_blueprints:
+          items:
+            type: string
+          type: array
+          title: Flask Blueprints
+        fastapi_apps:
+          items:
+            $ref: '#/components/schemas/FastAPIAppResponse'
+          type: array
+          title: Fastapi Apps
+        appbuilder_views:
+          items:
+            $ref: '#/components/schemas/AppBuilderViewResponse'
+          type: array
+          title: Appbuilder Views
+        appbuilder_menu_items:
+          items:
+            $ref: '#/components/schemas/AppBuilderMenuItemResponse'
+          type: array
+          title: Appbuilder Menu Items
+        global_operator_extra_links:
+          items:
+            type: string
+          type: array
+          title: Global Operator Extra Links
+        operator_extra_links:
+          items:
+            type: string
+          type: array
+          title: Operator Extra Links
+        source:
+          type: string
+          title: Source
+        ti_deps:
+          items:
+            type: string
+          type: array
+          title: Ti Deps
+        listeners:
+          items:
+            type: string
+          type: array
+          title: Listeners
+        timetables:
+          items:
+            type: string
+          type: array
+          title: Timetables
+      type: object
+      required:
+      - name
+      - hooks
+      - executors
+      - macros
+      - flask_blueprints
+      - fastapi_apps
+      - appbuilder_views
+      - appbuilder_menu_items
+      - global_operator_extra_links
+      - operator_extra_links
+      - source
+      - ti_deps
+      - listeners
+      - timetables
+      title: PluginResponse
+      description: Plugin serializer.
     PoolCollectionResponse:
       properties:
         pools:
diff --git a/airflow/api_fastapi/core_api/routes/public/__init__.py 
b/airflow/api_fastapi/core_api/routes/public/__init__.py
index 0778d66cc4..89d216e438 100644
--- a/airflow/api_fastapi/core_api/routes/public/__init__.py
+++ b/airflow/api_fastapi/core_api/routes/public/__init__.py
@@ -22,6 +22,7 @@ from airflow.api_fastapi.core_api.routes.public.connections 
import connections_r
 from airflow.api_fastapi.core_api.routes.public.dag_run import dag_run_router
 from airflow.api_fastapi.core_api.routes.public.dags import dags_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
 from airflow.api_fastapi.core_api.routes.public.providers import 
providers_router
 from airflow.api_fastapi.core_api.routes.public.variables import 
variables_router
@@ -36,3 +37,4 @@ public_router.include_router(dag_run_router)
 public_router.include_router(monitor_router)
 public_router.include_router(pools_router)
 public_router.include_router(providers_router)
+public_router.include_router(plugins_router)
diff --git a/airflow/api_connexion/endpoints/plugin_endpoint.py 
b/airflow/api_fastapi/core_api/routes/public/plugins.py
similarity index 52%
copy from airflow/api_connexion/endpoints/plugin_endpoint.py
copy to airflow/api_fastapi/core_api/routes/public/plugins.py
index 5a100fd6d5..c264e74837 100644
--- a/airflow/api_connexion/endpoints/plugin_endpoint.py
+++ b/airflow/api_fastapi/core_api/routes/public/plugins.py
@@ -14,24 +14,27 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
 
-from typing import TYPE_CHECKING
+from __future__ import annotations
 
-from airflow.api_connexion import security
-from airflow.api_connexion.parameters import check_limit, format_parameters
-from airflow.api_connexion.schemas.plugin_schema import PluginCollection, 
plugin_collection_schema
-from airflow.auth.managers.models.resource_details import AccessView
+from airflow.api_fastapi.common.parameters import QueryLimit, QueryOffset
+from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.serializers.plugins import 
PluginCollectionResponse, PluginResponse
 from airflow.plugins_manager import get_plugin_info
 
-if TYPE_CHECKING:
-    from airflow.api_connexion.types import APIResponse
+plugins_router = AirflowRouter(tags=["Plugin"], prefix="/plugins")
 
 
[email protected]_access_view(AccessView.PLUGINS)
-@format_parameters({"limit": check_limit})
-def get_plugins(*, limit: int, offset: int = 0) -> APIResponse:
-    """Get plugins endpoint."""
-    plugins_info = get_plugin_info()
-    collection = PluginCollection(plugins=plugins_info[offset:][:limit], 
total_entries=len(plugins_info))
-    return plugin_collection_schema.dump(collection)
+@plugins_router.get("/")
+async def get_plugins(
+    limit: QueryLimit,
+    offset: QueryOffset,
+) -> PluginCollectionResponse:
+    plugins_info = sorted(get_plugin_info(), key=lambda x: x["name"])
+    return PluginCollectionResponse(
+        plugins=[
+            PluginResponse.model_validate(plugin_info)
+            for plugin_info in plugins_info[offset.value :][: limit.value]
+        ],
+        total_entries=len(plugins_info),
+    )
diff --git a/airflow/api_fastapi/core_api/serializers/plugins.py 
b/airflow/api_fastapi/core_api/serializers/plugins.py
new file mode 100644
index 0000000000..ee6812bb95
--- /dev/null
+++ b/airflow/api_fastapi/core_api/serializers/plugins.py
@@ -0,0 +1,93 @@
+# 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 typing import Any
+
+from pydantic import BaseModel, BeforeValidator, ConfigDict, field_validator
+from typing_extensions import Annotated
+
+from airflow.plugins_manager import AirflowPluginSource
+
+
+def coerce_to_string(data: Any) -> Any:
+    return str(data)
+
+
+class FastAPIAppResponse(BaseModel):
+    """Serializer for Plugin FastAPI App responses."""
+
+    model_config = ConfigDict(extra="allow")
+
+    app: str
+    url_prefix: str
+    name: str
+
+
+class AppBuilderViewResponse(BaseModel):
+    """Serializer for AppBuilder View responses."""
+
+    model_config = ConfigDict(extra="allow")
+
+    name: str | None = None
+    category: str | None = None
+    view: str | None = None
+    label: str | None = None
+
+
+class AppBuilderMenuItemResponse(BaseModel):
+    """Serializer for AppBuilder Menu Item responses."""
+
+    model_config = ConfigDict(extra="allow")
+
+    name: str
+    href: str | None = None
+    category: str | None = None
+
+
+class PluginResponse(BaseModel):
+    """Plugin serializer."""
+
+    name: str
+    hooks: list[str]
+    executors: list[str]
+    macros: list[str]
+    flask_blueprints: list[str]
+    fastapi_apps: list[FastAPIAppResponse]
+    appbuilder_views: list[AppBuilderViewResponse]
+    appbuilder_menu_items: list[AppBuilderMenuItemResponse]
+    global_operator_extra_links: list[str]
+    operator_extra_links: list[str]
+    source: Annotated[str, BeforeValidator(coerce_to_string)]
+    ti_deps: list[Annotated[str, BeforeValidator(coerce_to_string)]]
+    listeners: list[str]
+    timetables: list[str]
+
+    @field_validator("source", mode="before")
+    @classmethod
+    def convert_source(cls, data: Any) -> Any:
+        if isinstance(data, AirflowPluginSource):
+            return str(data)
+        return data
+
+
+class PluginCollectionResponse(BaseModel):
+    """Plugin Collection serializer."""
+
+    plugins: list[PluginResponse]
+    total_entries: int
diff --git a/airflow/ui/openapi-gen/queries/common.ts 
b/airflow/ui/openapi-gen/queries/common.ts
index 41b1ff86f1..45ffa188ac 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -8,6 +8,7 @@ import {
   DagService,
   DashboardService,
   MonitorService,
+  PluginService,
   PoolService,
   ProviderService,
   VariableService,
@@ -326,6 +327,24 @@ export const UseProviderServiceGetProvidersKeyFn = (
   } = {},
   queryKey?: Array<unknown>,
 ) => [useProviderServiceGetProvidersKey, ...(queryKey ?? [{ limit, offset }])];
+export type PluginServiceGetPluginsDefaultResponse = Awaited<
+  ReturnType<typeof PluginService.getPlugins>
+>;
+export type PluginServiceGetPluginsQueryResult<
+  TData = PluginServiceGetPluginsDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const usePluginServiceGetPluginsKey = "PluginServiceGetPlugins";
+export const UsePluginServiceGetPluginsKeyFn = (
+  {
+    limit,
+    offset,
+  }: {
+    limit?: number;
+    offset?: number;
+  } = {},
+  queryKey?: Array<unknown>,
+) => [usePluginServiceGetPluginsKey, ...(queryKey ?? [{ limit, offset }])];
 export type VariableServicePostVariableMutationResult = Awaited<
   ReturnType<typeof VariableService.postVariable>
 >;
diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow/ui/openapi-gen/queries/prefetch.ts
index 2e2e9b0e3c..36a6c251cb 100644
--- a/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -8,6 +8,7 @@ import {
   DagService,
   DashboardService,
   MonitorService,
+  PluginService,
   PoolService,
   ProviderService,
   VariableService,
@@ -406,3 +407,25 @@ export const prefetchUseProviderServiceGetProviders = (
     queryKey: Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }),
     queryFn: () => ProviderService.getProviders({ limit, offset }),
   });
+/**
+ * Get Plugins
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @returns PluginCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUsePluginServiceGetPlugins = (
+  queryClient: QueryClient,
+  {
+    limit,
+    offset,
+  }: {
+    limit?: number;
+    offset?: number;
+  } = {},
+) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UsePluginServiceGetPluginsKeyFn({ limit, offset }),
+    queryFn: () => PluginService.getPlugins({ limit, offset }),
+  });
diff --git a/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow/ui/openapi-gen/queries/queries.ts
index 5e05eab973..288bef3733 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -13,6 +13,7 @@ import {
   DagService,
   DashboardService,
   MonitorService,
+  PluginService,
   PoolService,
   ProviderService,
   VariableService,
@@ -525,6 +526,37 @@ export const useProviderServiceGetProviders = <
     queryFn: () => ProviderService.getProviders({ limit, offset }) as TData,
     ...options,
   });
+/**
+ * Get Plugins
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @returns PluginCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const usePluginServiceGetPlugins = <
+  TData = Common.PluginServiceGetPluginsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    limit,
+    offset,
+  }: {
+    limit?: number;
+    offset?: number;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UsePluginServiceGetPluginsKeyFn(
+      { limit, offset },
+      queryKey,
+    ),
+    queryFn: () => PluginService.getPlugins({ limit, offset }) as TData,
+    ...options,
+  });
 /**
  * Post Variable
  * Create a variable.
diff --git a/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow/ui/openapi-gen/queries/suspense.ts
index 8f032bacd4..8fd858a985 100644
--- a/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow/ui/openapi-gen/queries/suspense.ts
@@ -8,6 +8,7 @@ import {
   DagService,
   DashboardService,
   MonitorService,
+  PluginService,
   PoolService,
   ProviderService,
   VariableService,
@@ -520,3 +521,34 @@ export const useProviderServiceGetProvidersSuspense = <
     queryFn: () => ProviderService.getProviders({ limit, offset }) as TData,
     ...options,
   });
+/**
+ * Get Plugins
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @returns PluginCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const usePluginServiceGetPluginsSuspense = <
+  TData = Common.PluginServiceGetPluginsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    limit,
+    offset,
+  }: {
+    limit?: number;
+    offset?: number;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UsePluginServiceGetPluginsKeyFn(
+      { limit, offset },
+      queryKey,
+    ),
+    queryFn: () => PluginService.getPlugins({ limit, offset }) as TData,
+    ...options,
+  });
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 1556ae2541..33863db404 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -1,5 +1,94 @@
 // This file is auto-generated by @hey-api/openapi-ts
 
+export const $AppBuilderMenuItemResponse = {
+  properties: {
+    name: {
+      type: "string",
+      title: "Name",
+    },
+    href: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Href",
+    },
+    category: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Category",
+    },
+  },
+  additionalProperties: true,
+  type: "object",
+  required: ["name"],
+  title: "AppBuilderMenuItemResponse",
+  description: "Serializer for AppBuilder Menu Item responses.",
+} as const;
+
+export const $AppBuilderViewResponse = {
+  properties: {
+    name: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Name",
+    },
+    category: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Category",
+    },
+    view: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "View",
+    },
+    label: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Label",
+    },
+  },
+  additionalProperties: true,
+  type: "object",
+  title: "AppBuilderViewResponse",
+  description: "Serializer for AppBuilder View responses.",
+} as const;
+
 export const $BaseInfoSchema = {
   properties: {
     status: {
@@ -1116,6 +1205,28 @@ export const $DagTagPydantic = {
     "Serializable representation of the DagTag ORM SqlAlchemyModel used by 
internal API.",
 } as const;
 
+export const $FastAPIAppResponse = {
+  properties: {
+    app: {
+      type: "string",
+      title: "App",
+    },
+    url_prefix: {
+      type: "string",
+      title: "Url Prefix",
+    },
+    name: {
+      type: "string",
+      title: "Name",
+    },
+  },
+  additionalProperties: true,
+  type: "object",
+  required: ["app", "url_prefix", "name"],
+  title: "FastAPIAppResponse",
+  description: "Serializer for Plugin FastAPI App responses.",
+} as const;
+
 export const $HTTPExceptionResponse = {
   properties: {
     detail: {
@@ -1189,6 +1300,142 @@ export const $HistoricalMetricDataResponse = {
   description: "Historical Metric Data serializer for responses.",
 } as const;
 
+export const $PluginCollectionResponse = {
+  properties: {
+    plugins: {
+      items: {
+        $ref: "#/components/schemas/PluginResponse",
+      },
+      type: "array",
+      title: "Plugins",
+    },
+    total_entries: {
+      type: "integer",
+      title: "Total Entries",
+    },
+  },
+  type: "object",
+  required: ["plugins", "total_entries"],
+  title: "PluginCollectionResponse",
+  description: "Plugin Collection serializer.",
+} as const;
+
+export const $PluginResponse = {
+  properties: {
+    name: {
+      type: "string",
+      title: "Name",
+    },
+    hooks: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Hooks",
+    },
+    executors: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Executors",
+    },
+    macros: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Macros",
+    },
+    flask_blueprints: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Flask Blueprints",
+    },
+    fastapi_apps: {
+      items: {
+        $ref: "#/components/schemas/FastAPIAppResponse",
+      },
+      type: "array",
+      title: "Fastapi Apps",
+    },
+    appbuilder_views: {
+      items: {
+        $ref: "#/components/schemas/AppBuilderViewResponse",
+      },
+      type: "array",
+      title: "Appbuilder Views",
+    },
+    appbuilder_menu_items: {
+      items: {
+        $ref: "#/components/schemas/AppBuilderMenuItemResponse",
+      },
+      type: "array",
+      title: "Appbuilder Menu Items",
+    },
+    global_operator_extra_links: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Global Operator Extra Links",
+    },
+    operator_extra_links: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Operator Extra Links",
+    },
+    source: {
+      type: "string",
+      title: "Source",
+    },
+    ti_deps: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Ti Deps",
+    },
+    listeners: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Listeners",
+    },
+    timetables: {
+      items: {
+        type: "string",
+      },
+      type: "array",
+      title: "Timetables",
+    },
+  },
+  type: "object",
+  required: [
+    "name",
+    "hooks",
+    "executors",
+    "macros",
+    "flask_blueprints",
+    "fastapi_apps",
+    "appbuilder_views",
+    "appbuilder_menu_items",
+    "global_operator_extra_links",
+    "operator_extra_links",
+    "source",
+    "ti_deps",
+    "listeners",
+    "timetables",
+  ],
+  title: "PluginResponse",
+  description: "Plugin serializer.",
+} as const;
+
 export const $PoolCollectionResponse = {
   properties: {
     pools: {
diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow/ui/openapi-gen/requests/services.gen.ts
index 45c5c98526..8aa2949f29 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -50,6 +50,8 @@ import type {
   GetPoolsResponse,
   GetProvidersData,
   GetProvidersResponse,
+  GetPluginsData,
+  GetPluginsResponse,
 } from "./types.gen";
 
 export class AssetService {
@@ -743,3 +745,29 @@ export class ProviderService {
     });
   }
 }
+
+export class PluginService {
+  /**
+   * Get Plugins
+   * @param data The data for the request.
+   * @param data.limit
+   * @param data.offset
+   * @returns PluginCollectionResponse Successful Response
+   * @throws ApiError
+   */
+  public static getPlugins(
+    data: GetPluginsData = {},
+  ): CancelablePromise<GetPluginsResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/public/plugins/",
+      query: {
+        limit: data.limit,
+        offset: data.offset,
+      },
+      errors: {
+        422: "Validation Error",
+      },
+    });
+  }
+}
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow/ui/openapi-gen/requests/types.gen.ts
index cf70c15ed6..210a115f2d 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1,5 +1,26 @@
 // This file is auto-generated by @hey-api/openapi-ts
 
+/**
+ * Serializer for AppBuilder Menu Item responses.
+ */
+export type AppBuilderMenuItemResponse = {
+  name: string;
+  href?: string | null;
+  category?: string | null;
+  [key: string]: unknown | string;
+};
+
+/**
+ * Serializer for AppBuilder View responses.
+ */
+export type AppBuilderViewResponse = {
+  name?: string | null;
+  category?: string | null;
+  view?: string | null;
+  label?: string | null;
+  [key: string]: unknown;
+};
+
 /**
  * Base status field for metadatabase and scheduler.
  */
@@ -232,6 +253,16 @@ export type DagTagPydantic = {
   dag_id: string;
 };
 
+/**
+ * Serializer for Plugin FastAPI App responses.
+ */
+export type FastAPIAppResponse = {
+  app: string;
+  url_prefix: string;
+  name: string;
+  [key: string]: unknown | string;
+};
+
 /**
  * HTTPException Model used for error response.
  */
@@ -266,6 +297,34 @@ export type HistoricalMetricDataResponse = {
   task_instance_states: TaskInstanceState;
 };
 
+/**
+ * Plugin Collection serializer.
+ */
+export type PluginCollectionResponse = {
+  plugins: Array<PluginResponse>;
+  total_entries: number;
+};
+
+/**
+ * Plugin serializer.
+ */
+export type PluginResponse = {
+  name: string;
+  hooks: Array<string>;
+  executors: Array<string>;
+  macros: Array<string>;
+  flask_blueprints: Array<string>;
+  fastapi_apps: Array<FastAPIAppResponse>;
+  appbuilder_views: Array<AppBuilderViewResponse>;
+  appbuilder_menu_items: Array<AppBuilderMenuItemResponse>;
+  global_operator_extra_links: Array<string>;
+  operator_extra_links: Array<string>;
+  source: string;
+  ti_deps: Array<string>;
+  listeners: Array<string>;
+  timetables: Array<string>;
+};
+
 /**
  * Pool Collection serializer for responses.
  */
@@ -551,6 +610,13 @@ export type GetProvidersData = {
 
 export type GetProvidersResponse = ProviderCollectionResponse;
 
+export type GetPluginsData = {
+  limit?: number;
+  offset?: number;
+};
+
+export type GetPluginsResponse = PluginCollectionResponse;
+
 export type $OpenApiTs = {
   "/ui/next_run_assets/{dag_id}": {
     get: {
@@ -1143,4 +1209,19 @@ export type $OpenApiTs = {
       };
     };
   };
+  "/public/plugins/": {
+    get: {
+      req: GetPluginsData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: PluginCollectionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
 };
diff --git a/tests/api_fastapi/core_api/routes/public/test_plugins.py 
b/tests/api_fastapi/core_api/routes/public/test_plugins.py
new file mode 100644
index 0000000000..5aeba44f15
--- /dev/null
+++ b/tests/api_fastapi/core_api/routes/public/test_plugins.py
@@ -0,0 +1,64 @@
+# 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
+
+import pytest
+
+pytestmark = pytest.mark.db_test
+
+
+class TestGetConnections:
+    @pytest.mark.parametrize(
+        "query_params, expected_total_entries, expected_names",
+        [
+            # Filters
+            (
+                {},
+                13,
+                [
+                    "MetadataCollectionPlugin",
+                    "OpenLineageProviderPlugin",
+                    "databricks_workflow",
+                    "decreasing_priority_weight_strategy_plugin",
+                    "edge_executor",
+                    "hive",
+                    "plugin-a",
+                    "plugin-b",
+                    "plugin-c",
+                    "postload",
+                    "priority_weight_strategy_plugin",
+                    "test_plugin",
+                    "workday_timetable_plugin",
+                ],
+            ),
+            (
+                {"limit": 3, "offset": 2},
+                13,
+                ["databricks_workflow", 
"decreasing_priority_weight_strategy_plugin", "edge_executor"],
+            ),
+            ({"limit": 1}, 13, ["MetadataCollectionPlugin"]),
+        ],
+    )
+    def test_should_respond_200(
+        self, test_client, session, query_params, expected_total_entries, 
expected_names
+    ):
+        response = test_client.get("/public/plugins/", params=query_params)
+        assert response.status_code == 200
+
+        body = response.json()
+        assert body["total_entries"] == expected_total_entries
+        assert [plugin["name"] for plugin in body["plugins"]] == expected_names

Reply via email to