Skip to content

Commit

Permalink
AIP-84 Get Plugins (apache#43125)
Browse files Browse the repository at this point in the history
* AIP-84 Get Plugins

* Explicit types

* Handle extra unknown properties

* Fix CI
  • Loading branch information
pierrejeambrun authored and PaulKobow7536 committed Oct 24, 2024
1 parent 57eff78 commit 6ea4f0f
Show file tree
Hide file tree
Showing 14 changed files with 867 additions and 2 deletions.
2 changes: 2 additions & 0 deletions airflow/api_connexion/endpoints/plugin_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
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:
Expand Down
4 changes: 2 additions & 2 deletions airflow/api_fastapi/common/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ def to_orm(self, select: Select) -> Select:
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."""
Expand Down
202 changes: 202 additions & 0 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions airflow/api_fastapi/core_api/routes/public/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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
Expand All @@ -36,3 +37,4 @@
public_router.include_router(monitor_router)
public_router.include_router(pools_router)
public_router.include_router(providers_router)
public_router.include_router(plugins_router)
40 changes: 40 additions & 0 deletions airflow/api_fastapi/core_api/routes/public/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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 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

plugins_router = AirflowRouter(tags=["Plugin"], prefix="/plugins")


@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),
)
93 changes: 93 additions & 0 deletions airflow/api_fastapi/core_api/serializers/plugins.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 6ea4f0f

Please sign in to comment.