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