This is an automated email from the ASF dual-hosted git repository.
henry3260 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 3293ced6410 [AIP-94] Add airflowctl providers get (#68854)
3293ced6410 is described below
commit 3293ced6410e6ac0a9ac042dfe2121b43979ed31
Author: Henry Chen <[email protected]>
AuthorDate: Thu Oct 29 01:46:13 2026 +0800
[AIP-94] Add airflowctl providers get (#68854)
* Add provider details endpoint to the public API
airflowctl needs a stable server-side source for detailed
installed-provider metadata without loading provider entry points on the client.
* Add airflowctl providers get command
Provider metadata lives on the API server, and users need summary and full
output compatible with the core CLI while the AIP-94 migration progresses.
* Accept -f as a short form of --full in airflowctl providers get
The core command this replaces, airflow providers get, declares its
flag as ("-f", "--full") and is already marked
@deprecated_for_airflowctl("airflowctl providers get"). Users moving
across should not have to unlearn the short form.
* Match the generated airflowctl provider datamodel to the API spec
The OpenAPI schema marks documentation_url as required on
ProviderDetailsResponse, so the client model must not give it a
default. With the default in place the datamodel codegen hook rewrites
the file on every run, which fails the pre-push and CI checks.
* Return typed provider metadata from the provider details endpoint
Serializing the raw provider.yaml dict meant every key added to provider
metadata in the future would be exposed through the public API by
default, and the OpenAPI spec described provider_info as an untyped
object. Modelling the response after provider_info.schema.json (the
schema every provider is validated against at discovery time) turns
serialization into an explicit allowlist and gives the generated UI and
airflowctl clients real types. Also drops a no-op catch-reraise in the
airflowctl providers operation, per review feedback on the PR.
* Keep all provider metadata in the provider details response
The typed provider_info model drops every key it does not declare, so a
provider whose metadata is made up mostly of the keys shared with the
summary response came back nearly empty: `airflowctl providers get --full`
returned less than the `airflow providers get --full` command it replaces.
* Return provider metadata as snake_case and keep all connection keys
The provider detail response reused the kebab-case key names from
provider.yaml, which would have made these the only non-snake_case
fields in the public API and unusable as attributes in the generated
clients. Reading the kebab-case keys is a parsing concern, so it moves
to validation_alias and leaves serialization on the field names.
Comparing the response against the deprecated `airflow providers get
--full` also showed metadata silently dropped because the models did
not declare it: ui-field-behaviour, conn-fields and external-services
on connection types, and is_template and see_also on config options.
* Guard the providers API against provider metadata key drift
Keys ``ProviderInfoResponse`` does not declare are dropped from the response
without raising, so a key added to ``provider_info.schema.json`` would go
missing from ``GET /providers/{package_name}`` with nothing to signal it.
This
PR lost ``ui-field-behaviour``, ``conn-fields``, ``external-services``,
``is_template`` and ``see_also`` that way until review spotted them by hand.
---
airflow-core/docs/security/api_permissions_ref.rst | 4 +
.../api_fastapi/core_api/datamodels/providers.py | 168 ++++
.../core_api/openapi/v2-rest-api-generated.yaml | 590 ++++++++++++
.../core_api/routes/public/providers.py | 23 +-
.../core_api/services/public/providers.py | 17 +-
.../src/airflow/ui/openapi-gen/queries/common.ts | 6 +
.../ui/openapi-gen/queries/ensureQueryData.ts | 11 +
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 11 +
.../src/airflow/ui/openapi-gen/queries/queries.ts | 11 +
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 11 +
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 1019 +++++++++++++++++++-
.../ui/openapi-gen/requests/services.gen.ts | 26 +-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 215 +++++
.../core_api/datamodels/test_providers.py | 50 +
.../core_api/routes/public/test_providers.py | 149 +++
.../airflowctl_tests/test_airflowctl_commands.py | 1 +
airflow-ctl/docs/images/command_hashes.txt | 2 +-
airflow-ctl/docs/images/output_providers.svg | 70 +-
.../src/airflowctl/api/datamodels/generated.py | 187 ++++
airflow-ctl/src/airflowctl/api/operations.py | 6 +
airflow-ctl/src/airflowctl/ctl/cli_config.py | 43 +-
.../airflowctl/ctl/commands/provider_command.py | 27 +-
airflow-ctl/src/airflowctl/ctl/help_texts.yaml | 1 +
.../tests/airflow_ctl/api/test_operations.py | 22 +
.../ctl/commands/test_provider_command.py | 107 ++
.../tests/airflow_ctl/ctl/test_cli_config.py | 21 +
providers/fab/docs/auth-manager/access-control.rst | 4 +
27 files changed, 2740 insertions(+), 62 deletions(-)
diff --git a/airflow-core/docs/security/api_permissions_ref.rst
b/airflow-core/docs/security/api_permissions_ref.rst
index 9d47f6bba3b..c4997864f34 100644
--- a/airflow-core/docs/security/api_permissions_ref.rst
+++ b/airflow-core/docs/security/api_permissions_ref.rst
@@ -582,6 +582,10 @@ source code so it stays up to date as endpoints are added
or changed.
- ``/api/v2/providers``
- ``View.PROVIDERS``
- ``PROVIDERS``
+ * - ``GET``
+ - ``/api/v2/providers/{provider_name}``
+ - ``View.PROVIDERS``
+ - ``PROVIDERS``
* - ``GET``
- ``/api/v2/variables``
- ``Variable``
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
index ab4381c231b..1df1091faf2 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
@@ -17,6 +17,10 @@
from __future__ import annotations
+from typing import Any
+
+from pydantic import Field
+
from airflow.api_fastapi.core_api.base import BaseModel
@@ -34,3 +38,167 @@ class ProviderCollectionResponse(BaseModel):
providers: list[ProviderResponse]
total_entries: int
+
+
+# The models below mirror the provider metadata structure defined in
+# airflow-core/src/airflow/provider_info.schema.json (deprecated keys
excluded). Keys the
+# models do not declare are dropped from the response, so new metadata keys
need a field here.
+
+
+class ProviderIntegrationInfo(BaseModel):
+ """Integration provided by a provider."""
+
+ integration_name: str = Field(validation_alias="integration-name")
+ external_doc_url: str | None = Field(None,
validation_alias="external-doc-url")
+ how_to_guide: list[str] | None = Field(None,
validation_alias="how-to-guide")
+ logo: str | None = None
+ tags: list[str] | None = None
+
+
+class ProviderIntegrationModulesInfo(BaseModel):
+ """Python modules an integration contributes for one kind of
functionality."""
+
+ integration_name: str = Field(validation_alias="integration-name")
+ python_modules: list[str] = Field(validation_alias="python-modules")
+
+
+class ProviderAssetUriInfo(BaseModel):
+ """Asset URI scheme handling provided by a provider."""
+
+ schemes: list[str]
+ handler: str | None = None
+ factory: str | None = None
+ to_openlineage_converter: str | None = None
+
+
+class ProviderDialectInfo(BaseModel):
+ """SQL dialect provided by a provider."""
+
+ dialect_type: str | None = Field(None, validation_alias="dialect-type")
+ dialect_class_name: str | None = Field(None,
validation_alias="dialect-class-name")
+
+
+class ProviderTransferInfo(BaseModel):
+ """Transfer operator provided by a provider."""
+
+ source_integration_name: str =
Field(validation_alias="source-integration-name")
+ target_integration_name: str =
Field(validation_alias="target-integration-name")
+ python_module: str = Field(validation_alias="python-module")
+ how_to_guide: str | None = Field(None, validation_alias="how-to-guide")
+
+
+class ProviderConnectionUiFieldBehaviourInfo(BaseModel):
+ """Customizations a connection type applies to the standard connection
form fields."""
+
+ hidden_fields: list[str] | None = Field(None,
validation_alias="hidden-fields")
+ relabeling: dict[str, str] | None = None
+ placeholders: dict[str, str] | None = None
+
+
+class ProviderConnectionFieldInfo(BaseModel):
+ """Custom connection field a connection type stores in
``Connection.extra``."""
+
+ label: str
+ description: str | None = None
+ # A JSON Schema fragment that allows additional keywords, so it is carried
through
+ # untyped. The field is not named ``schema`` because that shadows a
BaseModel attribute.
+ field_schema: dict[str, Any] = Field(validation_alias="schema",
serialization_alias="schema")
+
+
+class ProviderConnectionTypeInfo(BaseModel):
+ """Connection type provided by a provider."""
+
+ connection_type: str = Field(validation_alias="connection-type")
+ hook_class_name: str = Field(validation_alias="hook-class-name")
+ hook_name: str | None = Field(None, validation_alias="hook-name")
+ ui_field_behaviour: ProviderConnectionUiFieldBehaviourInfo | None = Field(
+ None, validation_alias="ui-field-behaviour"
+ )
+ conn_fields: dict[str, ProviderConnectionFieldInfo] | None = Field(None,
validation_alias="conn-fields")
+ external_services: list[str] | None = Field(None,
validation_alias="external-services")
+
+
+class ProviderRemoteLoggingInfo(BaseModel):
+ """Remote logging IO handler provided by a provider."""
+
+ classpath: str
+ scheme: str
+
+
+class ProviderConfigOptionInfo(BaseModel):
+ """Configuration option contributed by a provider."""
+
+ description: str | None = None
+ version_added: str | None = None
+ type: str | None = None
+ example: str | int | float | None = None
+ default: str | int | float | None = None
+ sensitive: bool | None = None
+ is_template: bool | None = None
+ see_also: str | None = None
+
+
+class ProviderConfigSectionInfo(BaseModel):
+ """Configuration section contributed by a provider."""
+
+ description: str | None = None
+ options: dict[str, ProviderConfigOptionInfo] = Field(default_factory=dict)
+
+
+class ProviderTaskDecoratorInfo(BaseModel):
+ """TaskFlow decorator provided by a provider."""
+
+ name: str | None = None
+ class_name: str | None = Field(None, validation_alias="class-name")
+
+
+class ProviderPluginInfo(BaseModel):
+ """Plugin provided by a provider."""
+
+ name: str | None = None
+ plugin_class: str | None = Field(None, validation_alias="plugin-class")
+
+
+class ProviderInfoResponse(BaseModel):
+ """Typed provider metadata (from ``provider.yaml``) exposed by the API."""
+
+ package_name: str | None = Field(None, validation_alias="package-name")
+ name: str | None = None
+ description: str | None = None
+ # Not part of provider_info.schema.json: added to the provider metadata
when the
+ # distribution is built, but present in the installed provider data at
runtime.
+ documentation_url: str | None = Field(None,
validation_alias="documentation-url")
+ filesystems: list[str] | None = None
+ integrations: list[ProviderIntegrationInfo] | None = None
+ operators: list[ProviderIntegrationModulesInfo] | None = None
+ sensors: list[ProviderIntegrationModulesInfo] | None = None
+ hooks: list[ProviderIntegrationModulesInfo] | None = None
+ triggers: list[ProviderIntegrationModulesInfo] | None = None
+ bundles: list[ProviderIntegrationModulesInfo] | None = None
+ asset_uris: list[ProviderAssetUriInfo] | None = Field(None,
validation_alias="asset-uris")
+ dialects: list[ProviderDialectInfo] | None = None
+ transfers: list[ProviderTransferInfo] | None = None
+ connection_types: list[ProviderConnectionTypeInfo] | None = Field(
+ None, validation_alias="connection-types"
+ )
+ extra_links: list[str] | None = Field(None, validation_alias="extra-links")
+ secrets_backends: list[str] | None = Field(None,
validation_alias="secrets-backends")
+ email_backends: list[str] | None = Field(None,
validation_alias="email-backends")
+ logging: list[str] | None = None
+ remote_logging: list[ProviderRemoteLoggingInfo] | None = Field(None,
validation_alias="remote-logging")
+ auth_backends: list[str] | None = Field(None,
validation_alias="auth-backends")
+ auth_managers: list[str] | None = Field(None,
validation_alias="auth-managers")
+ notifications: list[str] | None = None
+ executors: list[str] | None = None
+ db_managers: list[str] | None = Field(None, validation_alias="db-managers")
+ cli: list[str] | None = None
+ config: dict[str, ProviderConfigSectionInfo] | None = None
+ task_decorators: list[ProviderTaskDecoratorInfo] | None = Field(None,
validation_alias="task-decorators")
+ plugins: list[ProviderPluginInfo] | None = None
+ queues: list[str] | None = None
+
+
+class ProviderDetailsResponse(ProviderResponse):
+ """Detailed provider serializer for responses."""
+
+ provider_info: ProviderInfoResponse
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 0ee25e530c0..7e99a405b68 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -6124,6 +6124,54 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /api/v2/providers/{provider_name}:
+ get:
+ tags:
+ - Provider
+ summary: Get Provider
+ description: Get detailed information about an installed provider.
+ operationId: get_provider
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: provider_name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Provider Name
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProviderDetailsResponse'
+ '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'
/api/v2/assets/{asset_id}/state-store:
get:
tags:
@@ -17524,6 +17572,33 @@ components:
- team_name
title: PoolResponse
description: Pool serializer for responses.
+ ProviderAssetUriInfo:
+ properties:
+ schemes:
+ items:
+ type: string
+ type: array
+ title: Schemes
+ handler:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Handler
+ factory:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Factory
+ to_openlineage_converter:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: To Openlineage Converter
+ type: object
+ required:
+ - schemes
+ title: ProviderAssetUriInfo
+ description: Asset URI scheme handling provided by a provider.
ProviderCollectionResponse:
properties:
providers:
@@ -17540,6 +17615,483 @@ components:
- total_entries
title: ProviderCollectionResponse
description: Provider Collection serializer for responses.
+ ProviderConfigOptionInfo:
+ properties:
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
+ version_added:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Version Added
+ type:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Type
+ example:
+ anyOf:
+ - type: string
+ - type: integer
+ - type: number
+ - type: 'null'
+ title: Example
+ default:
+ anyOf:
+ - type: string
+ - type: integer
+ - type: number
+ - type: 'null'
+ title: Default
+ sensitive:
+ anyOf:
+ - type: boolean
+ - type: 'null'
+ title: Sensitive
+ is_template:
+ anyOf:
+ - type: boolean
+ - type: 'null'
+ title: Is Template
+ see_also:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: See Also
+ type: object
+ title: ProviderConfigOptionInfo
+ description: Configuration option contributed by a provider.
+ ProviderConfigSectionInfo:
+ properties:
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
+ options:
+ additionalProperties:
+ $ref: '#/components/schemas/ProviderConfigOptionInfo'
+ type: object
+ title: Options
+ type: object
+ title: ProviderConfigSectionInfo
+ description: Configuration section contributed by a provider.
+ ProviderConnectionFieldInfo:
+ properties:
+ label:
+ type: string
+ title: Label
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
+ schema:
+ additionalProperties: true
+ type: object
+ title: Schema
+ type: object
+ required:
+ - label
+ - schema
+ title: ProviderConnectionFieldInfo
+ description: Custom connection field a connection type stores in
``Connection.extra``.
+ ProviderConnectionTypeInfo:
+ properties:
+ connection_type:
+ type: string
+ title: Connection Type
+ hook_class_name:
+ type: string
+ title: Hook Class Name
+ hook_name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Hook Name
+ ui_field_behaviour:
+ anyOf:
+ - $ref: '#/components/schemas/ProviderConnectionUiFieldBehaviourInfo'
+ - type: 'null'
+ conn_fields:
+ anyOf:
+ - additionalProperties:
+ $ref: '#/components/schemas/ProviderConnectionFieldInfo'
+ type: object
+ - type: 'null'
+ title: Conn Fields
+ external_services:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: External Services
+ type: object
+ required:
+ - connection_type
+ - hook_class_name
+ title: ProviderConnectionTypeInfo
+ description: Connection type provided by a provider.
+ ProviderConnectionUiFieldBehaviourInfo:
+ properties:
+ hidden_fields:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Hidden Fields
+ relabeling:
+ anyOf:
+ - additionalProperties:
+ type: string
+ type: object
+ - type: 'null'
+ title: Relabeling
+ placeholders:
+ anyOf:
+ - additionalProperties:
+ type: string
+ type: object
+ - type: 'null'
+ title: Placeholders
+ type: object
+ title: ProviderConnectionUiFieldBehaviourInfo
+ description: Customizations a connection type applies to the standard
connection
+ form fields.
+ ProviderDetailsResponse:
+ properties:
+ package_name:
+ type: string
+ title: Package Name
+ description:
+ type: string
+ title: Description
+ version:
+ type: string
+ title: Version
+ documentation_url:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Documentation Url
+ provider_info:
+ $ref: '#/components/schemas/ProviderInfoResponse'
+ type: object
+ required:
+ - package_name
+ - description
+ - version
+ - documentation_url
+ - provider_info
+ title: ProviderDetailsResponse
+ description: Detailed provider serializer for responses.
+ ProviderDialectInfo:
+ properties:
+ dialect_type:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Dialect Type
+ dialect_class_name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Dialect Class Name
+ type: object
+ title: ProviderDialectInfo
+ description: SQL dialect provided by a provider.
+ ProviderInfoResponse:
+ properties:
+ package_name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Package Name
+ name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Name
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
+ documentation_url:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Documentation Url
+ filesystems:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Filesystems
+ integrations:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationInfo'
+ type: array
+ - type: 'null'
+ title: Integrations
+ operators:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationModulesInfo'
+ type: array
+ - type: 'null'
+ title: Operators
+ sensors:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationModulesInfo'
+ type: array
+ - type: 'null'
+ title: Sensors
+ hooks:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationModulesInfo'
+ type: array
+ - type: 'null'
+ title: Hooks
+ triggers:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationModulesInfo'
+ type: array
+ - type: 'null'
+ title: Triggers
+ bundles:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderIntegrationModulesInfo'
+ type: array
+ - type: 'null'
+ title: Bundles
+ asset_uris:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderAssetUriInfo'
+ type: array
+ - type: 'null'
+ title: Asset Uris
+ dialects:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderDialectInfo'
+ type: array
+ - type: 'null'
+ title: Dialects
+ transfers:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderTransferInfo'
+ type: array
+ - type: 'null'
+ title: Transfers
+ connection_types:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderConnectionTypeInfo'
+ type: array
+ - type: 'null'
+ title: Connection Types
+ extra_links:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Extra Links
+ secrets_backends:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Secrets Backends
+ email_backends:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Email Backends
+ logging:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Logging
+ remote_logging:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderRemoteLoggingInfo'
+ type: array
+ - type: 'null'
+ title: Remote Logging
+ auth_backends:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Auth Backends
+ auth_managers:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Auth Managers
+ notifications:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Notifications
+ executors:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Executors
+ db_managers:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Db Managers
+ cli:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Cli
+ config:
+ anyOf:
+ - additionalProperties:
+ $ref: '#/components/schemas/ProviderConfigSectionInfo'
+ type: object
+ - type: 'null'
+ title: Config
+ task_decorators:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderTaskDecoratorInfo'
+ type: array
+ - type: 'null'
+ title: Task Decorators
+ plugins:
+ anyOf:
+ - items:
+ $ref: '#/components/schemas/ProviderPluginInfo'
+ type: array
+ - type: 'null'
+ title: Plugins
+ queues:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Queues
+ type: object
+ title: ProviderInfoResponse
+ description: Typed provider metadata (from ``provider.yaml``) exposed by
the
+ API.
+ ProviderIntegrationInfo:
+ properties:
+ integration_name:
+ type: string
+ title: Integration Name
+ external_doc_url:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: External Doc Url
+ how_to_guide:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: How To Guide
+ logo:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Logo
+ tags:
+ anyOf:
+ - items:
+ type: string
+ type: array
+ - type: 'null'
+ title: Tags
+ type: object
+ required:
+ - integration_name
+ title: ProviderIntegrationInfo
+ description: Integration provided by a provider.
+ ProviderIntegrationModulesInfo:
+ properties:
+ integration_name:
+ type: string
+ title: Integration Name
+ python_modules:
+ items:
+ type: string
+ type: array
+ title: Python Modules
+ type: object
+ required:
+ - integration_name
+ - python_modules
+ title: ProviderIntegrationModulesInfo
+ description: Python modules an integration contributes for one kind of
functionality.
+ ProviderPluginInfo:
+ properties:
+ name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Name
+ plugin_class:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Plugin Class
+ type: object
+ title: ProviderPluginInfo
+ description: Plugin provided by a provider.
+ ProviderRemoteLoggingInfo:
+ properties:
+ classpath:
+ type: string
+ title: Classpath
+ scheme:
+ type: string
+ title: Scheme
+ type: object
+ required:
+ - classpath
+ - scheme
+ title: ProviderRemoteLoggingInfo
+ description: Remote logging IO handler provided by a provider.
ProviderResponse:
properties:
package_name:
@@ -17564,6 +18116,44 @@ components:
- documentation_url
title: ProviderResponse
description: Provider serializer for responses.
+ ProviderTaskDecoratorInfo:
+ properties:
+ name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Name
+ class_name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Class Name
+ type: object
+ title: ProviderTaskDecoratorInfo
+ description: TaskFlow decorator provided by a provider.
+ ProviderTransferInfo:
+ properties:
+ source_integration_name:
+ type: string
+ title: Source Integration Name
+ target_integration_name:
+ type: string
+ title: Target Integration Name
+ python_module:
+ type: string
+ title: Python Module
+ how_to_guide:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: How To Guide
+ type: object
+ required:
+ - source_integration_name
+ - target_integration_name
+ - python_module
+ title: ProviderTransferInfo
+ description: Transfer operator provided by a provider.
QueuedEventCollectionResponse:
properties:
queued_events:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/providers.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/providers.py
index 813c890db5c..367b117c0d0 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/providers.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/providers.py
@@ -17,14 +17,18 @@
from __future__ import annotations
-from fastapi import Depends
+from fastapi import Depends, HTTPException, status
from airflow.api_fastapi.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.datamodels.providers import
ProviderCollectionResponse
+from airflow.api_fastapi.core_api.datamodels.providers import (
+ ProviderCollectionResponse,
+ ProviderDetailsResponse,
+)
+from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.security import requires_access_view
-from airflow.api_fastapi.core_api.services.public.providers import
_provider_mapper
+from airflow.api_fastapi.core_api.services.public.providers import
_provider_mapper, map_provider_details
from airflow.providers_manager import ProvidersManager
providers_router = AirflowRouter(tags=["Provider"], prefix="/providers")
@@ -47,3 +51,16 @@ def get_providers(
if limit.value is not None and offset.value is not None:
providers = providers[offset.value : offset.value + limit.value]
return ProviderCollectionResponse(providers=providers,
total_entries=total_entries)
+
+
+@providers_router.get(
+ "/{provider_name}",
+ responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
+ dependencies=[Depends(requires_access_view(AccessView.PROVIDERS))],
+)
+def get_provider(provider_name: str) -> ProviderDetailsResponse:
+ """Get detailed information about an installed provider."""
+ provider = ProvidersManager().providers.get(provider_name)
+ if provider is None:
+ raise HTTPException(status.HTTP_404_NOT_FOUND, f"Provider
{provider_name!r} was not found")
+ return map_provider_details(provider)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py
index 8d659792b97..4623e75674d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py
@@ -18,7 +18,11 @@ from __future__ import annotations
import re
-from airflow.api_fastapi.core_api.datamodels.providers import ProviderResponse
+from airflow.api_fastapi.core_api.datamodels.providers import (
+ ProviderDetailsResponse,
+ ProviderInfoResponse,
+ ProviderResponse,
+)
from airflow.providers_manager import ProviderInfo
@@ -33,3 +37,14 @@ def _provider_mapper(provider: ProviderInfo) ->
ProviderResponse:
version=provider.version,
documentation_url=provider.data["documentation-url"],
)
+
+
+def map_provider_details(provider: ProviderInfo) -> ProviderDetailsResponse:
+ """Map provider information to a detailed API response."""
+ provider_info = ProviderInfoResponse.model_validate(provider.data)
+ if provider_info.description is not None:
+ provider_info.description =
_remove_rst_syntax(provider_info.description)
+ return ProviderDetailsResponse(
+ **_provider_mapper(provider).model_dump(),
+ provider_info=provider_info,
+ )
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index 8ade19b873f..5170bea5453 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -922,6 +922,12 @@ export const UseProviderServiceGetProvidersKeyFn = ({
limit, offset }: {
limit?: number;
offset?: number;
} = {}, queryKey?: Array<unknown>) => [useProviderServiceGetProvidersKey,
...(queryKey ?? [{ limit, offset }])];
+export type ProviderServiceGetProviderDefaultResponse =
Awaited<ReturnType<typeof ProviderService.getProvider>>;
+export type ProviderServiceGetProviderQueryResult<TData =
ProviderServiceGetProviderDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
+export const useProviderServiceGetProviderKey = "ProviderServiceGetProvider";
+export const UseProviderServiceGetProviderKeyFn = ({ providerName }: {
+ providerName: string;
+}, queryKey?: Array<unknown>) => [useProviderServiceGetProviderKey,
...(queryKey ?? [{ providerName }])];
export type AssetStateStoreServiceListAssetStateStoreDefaultResponse =
Awaited<ReturnType<typeof AssetStateStoreService.listAssetStateStore>>;
export type AssetStateStoreServiceListAssetStateStoreQueryResult<TData =
AssetStateStoreServiceListAssetStateStoreDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useAssetStateStoreServiceListAssetStateStoreKey =
"AssetStateStoreServiceListAssetStateStore";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 26726a5bc9f..1179186d336 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1837,6 +1837,17 @@ export const ensureUseProviderServiceGetProvidersData =
(queryClient: QueryClien
offset?: number;
} = {}) => queryClient.ensureQueryData({ queryKey:
Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }), queryFn: () =>
ProviderService.getProviders({ limit, offset }) });
/**
+* Get Provider
+* Get detailed information about an installed provider.
+* @param data The data for the request.
+* @param data.providerName
+* @returns ProviderDetailsResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseProviderServiceGetProviderData = (queryClient:
QueryClient, { providerName }: {
+ providerName: string;
+}) => queryClient.ensureQueryData({ queryKey:
Common.UseProviderServiceGetProviderKeyFn({ providerName }), queryFn: () =>
ProviderService.getProvider({ providerName }) });
+/**
* List Asset State Store
* List all state store entries for an asset.
* @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index d8feaacbb9b..2f7eac20576 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1837,6 +1837,17 @@ export const prefetchUseProviderServiceGetProviders =
(queryClient: QueryClient,
offset?: number;
} = {}) => queryClient.prefetchQuery({ queryKey:
Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }), queryFn: () =>
ProviderService.getProviders({ limit, offset }) });
/**
+* Get Provider
+* Get detailed information about an installed provider.
+* @param data The data for the request.
+* @param data.providerName
+* @returns ProviderDetailsResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseProviderServiceGetProvider = (queryClient:
QueryClient, { providerName }: {
+ providerName: string;
+}) => queryClient.prefetchQuery({ queryKey:
Common.UseProviderServiceGetProviderKeyFn({ providerName }), queryFn: () =>
ProviderService.getProvider({ providerName }) });
+/**
* List Asset State Store
* List all state store entries for an asset.
* @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index bbac95f6116..048d540f134 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1837,6 +1837,17 @@ export const useProviderServiceGetProviders = <TData =
Common.ProviderServiceGet
offset?: number;
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }, queryKey),
queryFn: () => ProviderService.getProviders({ limit, offset }) as TData,
...options });
/**
+* Get Provider
+* Get detailed information about an installed provider.
+* @param data The data for the request.
+* @param data.providerName
+* @returns ProviderDetailsResponse Successful Response
+* @throws ApiError
+*/
+export const useProviderServiceGetProvider = <TData =
Common.ProviderServiceGetProviderDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ providerName }: {
+ providerName: string;
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseProviderServiceGetProviderKeyFn({ providerName }, queryKey), queryFn:
() => ProviderService.getProvider({ providerName }) as TData, ...options });
+/**
* List Asset State Store
* List all state store entries for an asset.
* @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 162f495e6ba..71efee38492 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1837,6 +1837,17 @@ export const useProviderServiceGetProvidersSuspense =
<TData = Common.ProviderSe
offset?: number;
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseProviderServiceGetProvidersKeyFn({ limit, offset }, queryKey),
queryFn: () => ProviderService.getProviders({ limit, offset }) as TData,
...options });
/**
+* Get Provider
+* Get detailed information about an installed provider.
+* @param data The data for the request.
+* @param data.providerName
+* @returns ProviderDetailsResponse Successful Response
+* @throws ApiError
+*/
+export const useProviderServiceGetProviderSuspense = <TData =
Common.ProviderServiceGetProviderDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ providerName }: {
+ providerName: string;
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseProviderServiceGetProviderKeyFn({ providerName }, queryKey), queryFn:
() => ProviderService.getProvider({ providerName }) as TData, ...options });
+/**
* List Asset State Store
* List all state store entries for an asset.
* @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 8c151dcf36f..47cb8ea9bdf 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -7463,24 +7463,963 @@ export const $PoolResponse = {
description: 'Pool serializer for responses.'
} as const;
+export const $ProviderAssetUriInfo = {
+ properties: {
+ schemes: {
+ items: {
+ type: 'string'
+ },
+ type: 'array',
+ title: 'Schemes'
+ },
+ handler: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Handler'
+ },
+ factory: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Factory'
+ },
+ to_openlineage_converter: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'To Openlineage Converter'
+ }
+ },
+ type: 'object',
+ required: ['schemes'],
+ title: 'ProviderAssetUriInfo',
+ description: 'Asset URI scheme handling provided by a provider.'
+} as const;
+
export const $ProviderCollectionResponse = {
properties: {
- providers: {
+ providers: {
+ items: {
+ '$ref': '#/components/schemas/ProviderResponse'
+ },
+ type: 'array',
+ title: 'Providers'
+ },
+ total_entries: {
+ type: 'integer',
+ title: 'Total Entries'
+ }
+ },
+ type: 'object',
+ required: ['providers', 'total_entries'],
+ title: 'ProviderCollectionResponse',
+ description: 'Provider Collection serializer for responses.'
+} as const;
+
+export const $ProviderConfigOptionInfo = {
+ properties: {
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ version_added: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Version Added'
+ },
+ type: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Type'
+ },
+ example: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'integer'
+ },
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Example'
+ },
+ default: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'integer'
+ },
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Default'
+ },
+ sensitive: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Sensitive'
+ },
+ is_template: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Is Template'
+ },
+ see_also: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'See Also'
+ }
+ },
+ type: 'object',
+ title: 'ProviderConfigOptionInfo',
+ description: 'Configuration option contributed by a provider.'
+} as const;
+
+export const $ProviderConfigSectionInfo = {
+ properties: {
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ options: {
+ additionalProperties: {
+ '$ref': '#/components/schemas/ProviderConfigOptionInfo'
+ },
+ type: 'object',
+ title: 'Options'
+ }
+ },
+ type: 'object',
+ title: 'ProviderConfigSectionInfo',
+ description: 'Configuration section contributed by a provider.'
+} as const;
+
+export const $ProviderConnectionFieldInfo = {
+ properties: {
+ label: {
+ type: 'string',
+ title: 'Label'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ schema: {
+ additionalProperties: true,
+ type: 'object',
+ title: 'Schema'
+ }
+ },
+ type: 'object',
+ required: ['label', 'schema'],
+ title: 'ProviderConnectionFieldInfo',
+ description: 'Custom connection field a connection type stores in
``Connection.extra``.'
+} as const;
+
+export const $ProviderConnectionTypeInfo = {
+ properties: {
+ connection_type: {
+ type: 'string',
+ title: 'Connection Type'
+ },
+ hook_class_name: {
+ type: 'string',
+ title: 'Hook Class Name'
+ },
+ hook_name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Hook Name'
+ },
+ ui_field_behaviour: {
+ anyOf: [
+ {
+ '$ref':
'#/components/schemas/ProviderConnectionUiFieldBehaviourInfo'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ },
+ conn_fields: {
+ anyOf: [
+ {
+ additionalProperties: {
+ '$ref':
'#/components/schemas/ProviderConnectionFieldInfo'
+ },
+ type: 'object'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Conn Fields'
+ },
+ external_services: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'External Services'
+ }
+ },
+ type: 'object',
+ required: ['connection_type', 'hook_class_name'],
+ title: 'ProviderConnectionTypeInfo',
+ description: 'Connection type provided by a provider.'
+} as const;
+
+export const $ProviderConnectionUiFieldBehaviourInfo = {
+ properties: {
+ hidden_fields: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Hidden Fields'
+ },
+ relabeling: {
+ anyOf: [
+ {
+ additionalProperties: {
+ type: 'string'
+ },
+ type: 'object'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Relabeling'
+ },
+ placeholders: {
+ anyOf: [
+ {
+ additionalProperties: {
+ type: 'string'
+ },
+ type: 'object'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Placeholders'
+ }
+ },
+ type: 'object',
+ title: 'ProviderConnectionUiFieldBehaviourInfo',
+ description: 'Customizations a connection type applies to the standard
connection form fields.'
+} as const;
+
+export const $ProviderDetailsResponse = {
+ properties: {
+ package_name: {
+ type: 'string',
+ title: 'Package Name'
+ },
+ description: {
+ type: 'string',
+ title: 'Description'
+ },
+ version: {
+ type: 'string',
+ title: 'Version'
+ },
+ documentation_url: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Documentation Url'
+ },
+ provider_info: {
+ '$ref': '#/components/schemas/ProviderInfoResponse'
+ }
+ },
+ type: 'object',
+ required: ['package_name', 'description', 'version', 'documentation_url',
'provider_info'],
+ title: 'ProviderDetailsResponse',
+ description: 'Detailed provider serializer for responses.'
+} as const;
+
+export const $ProviderDialectInfo = {
+ properties: {
+ dialect_type: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Dialect Type'
+ },
+ dialect_class_name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Dialect Class Name'
+ }
+ },
+ type: 'object',
+ title: 'ProviderDialectInfo',
+ description: 'SQL dialect provided by a provider.'
+} as const;
+
+export const $ProviderInfoResponse = {
+ properties: {
+ package_name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Package Name'
+ },
+ name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Name'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ documentation_url: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Documentation Url'
+ },
+ filesystems: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Filesystems'
+ },
+ integrations: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ProviderIntegrationInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Integrations'
+ },
+ operators: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderIntegrationModulesInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Operators'
+ },
+ sensors: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderIntegrationModulesInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Sensors'
+ },
+ hooks: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderIntegrationModulesInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Hooks'
+ },
+ triggers: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderIntegrationModulesInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Triggers'
+ },
+ bundles: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderIntegrationModulesInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Bundles'
+ },
+ asset_uris: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ProviderAssetUriInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Asset Uris'
+ },
+ dialects: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ProviderDialectInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Dialects'
+ },
+ transfers: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ProviderTransferInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Transfers'
+ },
+ connection_types: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderConnectionTypeInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Connection Types'
+ },
+ extra_links: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Extra Links'
+ },
+ secrets_backends: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Secrets Backends'
+ },
+ email_backends: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Email Backends'
+ },
+ logging: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Logging'
+ },
+ remote_logging: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderRemoteLoggingInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Remote Logging'
+ },
+ auth_backends: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Auth Backends'
+ },
+ auth_managers: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Auth Managers'
+ },
+ notifications: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Notifications'
+ },
+ executors: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Executors'
+ },
+ db_managers: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Db Managers'
+ },
+ cli: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Cli'
+ },
+ config: {
+ anyOf: [
+ {
+ additionalProperties: {
+ '$ref':
'#/components/schemas/ProviderConfigSectionInfo'
+ },
+ type: 'object'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Config'
+ },
+ task_decorators: {
+ anyOf: [
+ {
+ items: {
+ '$ref':
'#/components/schemas/ProviderTaskDecoratorInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Task Decorators'
+ },
+ plugins: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ProviderPluginInfo'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Plugins'
+ },
+ queues: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Queues'
+ }
+ },
+ type: 'object',
+ title: 'ProviderInfoResponse',
+ description: 'Typed provider metadata (from ``provider.yaml``) exposed by
the API.'
+} as const;
+
+export const $ProviderIntegrationInfo = {
+ properties: {
+ integration_name: {
+ type: 'string',
+ title: 'Integration Name'
+ },
+ external_doc_url: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'External Doc Url'
+ },
+ how_to_guide: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'How To Guide'
+ },
+ logo: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Logo'
+ },
+ tags: {
+ anyOf: [
+ {
+ items: {
+ type: 'string'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Tags'
+ }
+ },
+ type: 'object',
+ required: ['integration_name'],
+ title: 'ProviderIntegrationInfo',
+ description: 'Integration provided by a provider.'
+} as const;
+
+export const $ProviderIntegrationModulesInfo = {
+ properties: {
+ integration_name: {
+ type: 'string',
+ title: 'Integration Name'
+ },
+ python_modules: {
items: {
- '$ref': '#/components/schemas/ProviderResponse'
+ type: 'string'
},
type: 'array',
- title: 'Providers'
+ title: 'Python Modules'
+ }
+ },
+ type: 'object',
+ required: ['integration_name', 'python_modules'],
+ title: 'ProviderIntegrationModulesInfo',
+ description: 'Python modules an integration contributes for one kind of
functionality.'
+} as const;
+
+export const $ProviderPluginInfo = {
+ properties: {
+ name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Name'
},
- total_entries: {
- type: 'integer',
- title: 'Total Entries'
+ plugin_class: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Plugin Class'
}
},
type: 'object',
- required: ['providers', 'total_entries'],
- title: 'ProviderCollectionResponse',
- description: 'Provider Collection serializer for responses.'
+ title: 'ProviderPluginInfo',
+ description: 'Plugin provided by a provider.'
+} as const;
+
+export const $ProviderRemoteLoggingInfo = {
+ properties: {
+ classpath: {
+ type: 'string',
+ title: 'Classpath'
+ },
+ scheme: {
+ type: 'string',
+ title: 'Scheme'
+ }
+ },
+ type: 'object',
+ required: ['classpath', 'scheme'],
+ title: 'ProviderRemoteLoggingInfo',
+ description: 'Remote logging IO handler provided by a provider.'
} as const;
export const $ProviderResponse = {
@@ -7515,6 +8454,68 @@ export const $ProviderResponse = {
description: 'Provider serializer for responses.'
} as const;
+export const $ProviderTaskDecoratorInfo = {
+ properties: {
+ name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Name'
+ },
+ class_name: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Class Name'
+ }
+ },
+ type: 'object',
+ title: 'ProviderTaskDecoratorInfo',
+ description: 'TaskFlow decorator provided by a provider.'
+} as const;
+
+export const $ProviderTransferInfo = {
+ properties: {
+ source_integration_name: {
+ type: 'string',
+ title: 'Source Integration Name'
+ },
+ target_integration_name: {
+ type: 'string',
+ title: 'Target Integration Name'
+ },
+ python_module: {
+ type: 'string',
+ title: 'Python Module'
+ },
+ how_to_guide: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'How To Guide'
+ }
+ },
+ type: 'object',
+ required: ['source_integration_name', 'target_integration_name',
'python_module'],
+ title: 'ProviderTransferInfo',
+ description: 'Transfer operator provided by a provider.'
+} as const;
+
export const $QueuedEventCollectionResponse = {
properties: {
queued_events: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index b82d12631cc..ee4130f8e15 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
export class AssetService {
/**
@@ -4234,6 +4234,30 @@ export class ProviderService {
});
}
+ /**
+ * Get Provider
+ * Get detailed information about an installed provider.
+ * @param data The data for the request.
+ * @param data.providerName
+ * @returns ProviderDetailsResponse Successful Response
+ * @throws ApiError
+ */
+ public static getProvider(data: GetProviderData):
CancelablePromise<GetProviderResponse> {
+ return __request(OpenAPI, {
+ method: 'GET',
+ url: '/api/v2/providers/{provider_name}',
+ path: {
+ provider_name: data.providerName
+ },
+ errors: {
+ 401: 'Unauthorized',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 422: 'Validation Error'
+ }
+ });
+ }
+
}
export class AssetStateStoreService {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index c34a9a1dfee..fe19ce55c37 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2034,6 +2034,16 @@ export type PoolResponse = {
team_name: string | null;
};
+/**
+ * Asset URI scheme handling provided by a provider.
+ */
+export type ProviderAssetUriInfo = {
+ schemes: Array<(string)>;
+ handler?: string | null;
+ factory?: string | null;
+ to_openlineage_converter?: string | null;
+};
+
/**
* Provider Collection serializer for responses.
*/
@@ -2042,6 +2052,160 @@ export type ProviderCollectionResponse = {
total_entries: number;
};
+/**
+ * Configuration option contributed by a provider.
+ */
+export type ProviderConfigOptionInfo = {
+ description?: string | null;
+ version_added?: string | null;
+ type?: string | null;
+ example?: string | number | null;
+ default?: string | number | null;
+ sensitive?: boolean | null;
+ is_template?: boolean | null;
+ see_also?: string | null;
+};
+
+/**
+ * Configuration section contributed by a provider.
+ */
+export type ProviderConfigSectionInfo = {
+ description?: string | null;
+ options?: {
+ [key: string]: ProviderConfigOptionInfo;
+ };
+};
+
+/**
+ * Custom connection field a connection type stores in ``Connection.extra``.
+ */
+export type ProviderConnectionFieldInfo = {
+ label: string;
+ description?: string | null;
+ schema: {
+ [key: string]: unknown;
+ };
+};
+
+/**
+ * Connection type provided by a provider.
+ */
+export type ProviderConnectionTypeInfo = {
+ connection_type: string;
+ hook_class_name: string;
+ hook_name?: string | null;
+ ui_field_behaviour?: ProviderConnectionUiFieldBehaviourInfo | null;
+ conn_fields?: {
+ [key: string]: ProviderConnectionFieldInfo;
+} | null;
+ external_services?: Array<(string)> | null;
+};
+
+/**
+ * Customizations a connection type applies to the standard connection form
fields.
+ */
+export type ProviderConnectionUiFieldBehaviourInfo = {
+ hidden_fields?: Array<(string)> | null;
+ relabeling?: {
+ [key: string]: (string);
+} | null;
+ placeholders?: {
+ [key: string]: (string);
+} | null;
+};
+
+/**
+ * Detailed provider serializer for responses.
+ */
+export type ProviderDetailsResponse = {
+ package_name: string;
+ description: string;
+ version: string;
+ documentation_url: string | null;
+ provider_info: ProviderInfoResponse;
+};
+
+/**
+ * SQL dialect provided by a provider.
+ */
+export type ProviderDialectInfo = {
+ dialect_type?: string | null;
+ dialect_class_name?: string | null;
+};
+
+/**
+ * Typed provider metadata (from ``provider.yaml``) exposed by the API.
+ */
+export type ProviderInfoResponse = {
+ package_name?: string | null;
+ name?: string | null;
+ description?: string | null;
+ documentation_url?: string | null;
+ filesystems?: Array<(string)> | null;
+ integrations?: Array<ProviderIntegrationInfo> | null;
+ operators?: Array<ProviderIntegrationModulesInfo> | null;
+ sensors?: Array<ProviderIntegrationModulesInfo> | null;
+ hooks?: Array<ProviderIntegrationModulesInfo> | null;
+ triggers?: Array<ProviderIntegrationModulesInfo> | null;
+ bundles?: Array<ProviderIntegrationModulesInfo> | null;
+ asset_uris?: Array<ProviderAssetUriInfo> | null;
+ dialects?: Array<ProviderDialectInfo> | null;
+ transfers?: Array<ProviderTransferInfo> | null;
+ connection_types?: Array<ProviderConnectionTypeInfo> | null;
+ extra_links?: Array<(string)> | null;
+ secrets_backends?: Array<(string)> | null;
+ email_backends?: Array<(string)> | null;
+ logging?: Array<(string)> | null;
+ remote_logging?: Array<ProviderRemoteLoggingInfo> | null;
+ auth_backends?: Array<(string)> | null;
+ auth_managers?: Array<(string)> | null;
+ notifications?: Array<(string)> | null;
+ executors?: Array<(string)> | null;
+ db_managers?: Array<(string)> | null;
+ cli?: Array<(string)> | null;
+ config?: {
+ [key: string]: ProviderConfigSectionInfo;
+} | null;
+ task_decorators?: Array<ProviderTaskDecoratorInfo> | null;
+ plugins?: Array<ProviderPluginInfo> | null;
+ queues?: Array<(string)> | null;
+};
+
+/**
+ * Integration provided by a provider.
+ */
+export type ProviderIntegrationInfo = {
+ integration_name: string;
+ external_doc_url?: string | null;
+ how_to_guide?: Array<(string)> | null;
+ logo?: string | null;
+ tags?: Array<(string)> | null;
+};
+
+/**
+ * Python modules an integration contributes for one kind of functionality.
+ */
+export type ProviderIntegrationModulesInfo = {
+ integration_name: string;
+ python_modules: Array<(string)>;
+};
+
+/**
+ * Plugin provided by a provider.
+ */
+export type ProviderPluginInfo = {
+ name?: string | null;
+ plugin_class?: string | null;
+};
+
+/**
+ * Remote logging IO handler provided by a provider.
+ */
+export type ProviderRemoteLoggingInfo = {
+ classpath: string;
+ scheme: string;
+};
+
/**
* Provider serializer for responses.
*/
@@ -2052,6 +2216,24 @@ export type ProviderResponse = {
documentation_url: string | null;
};
+/**
+ * TaskFlow decorator provided by a provider.
+ */
+export type ProviderTaskDecoratorInfo = {
+ name?: string | null;
+ class_name?: string | null;
+};
+
+/**
+ * Transfer operator provided by a provider.
+ */
+export type ProviderTransferInfo = {
+ source_integration_name: string;
+ target_integration_name: string;
+ python_module: string;
+ how_to_guide?: string | null;
+};
+
/**
* Queued Event Collection serializer for responses.
*/
@@ -5125,6 +5307,12 @@ export type GetProvidersData = {
export type GetProvidersResponse = ProviderCollectionResponse;
+export type GetProviderData = {
+ providerName: string;
+};
+
+export type GetProviderResponse = ProviderDetailsResponse;
+
export type ListAssetStateStoreData = {
assetId: number;
limit?: number;
@@ -8898,6 +9086,33 @@ export type $OpenApiTs = {
};
};
};
+ '/api/v2/providers/{provider_name}': {
+ get: {
+ req: GetProviderData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: ProviderDetailsResponse;
+ /**
+ * Unauthorized
+ */
+ 401: HTTPExceptionResponse;
+ /**
+ * Forbidden
+ */
+ 403: HTTPExceptionResponse;
+ /**
+ * Not Found
+ */
+ 404: HTTPExceptionResponse;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
+ };
'/api/v2/assets/{asset_id}/state-store': {
get: {
req: ListAssetStateStoreData;
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_providers.py
b/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_providers.py
new file mode 100644
index 00000000000..e647264955b
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_providers.py
@@ -0,0 +1,50 @@
+# 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 json
+from importlib.resources import files as resource_files
+
+from airflow.api_fastapi.core_api.datamodels.providers import
ProviderInfoResponse
+
+# Added to the provider metadata when the distribution is built, so
provider_info.schema.json
+# declares no property for it.
+BUILD_TIME_KEYS = {"documentation-url"}
+
+
+def get_declared_provider_info_keys() -> set[str]:
+ """Return the provider metadata keys ``ProviderInfoResponse`` reads."""
+ keys: set[str] = set()
+ for name, field in ProviderInfoResponse.model_fields.items():
+ alias = field.validation_alias
+ keys.add(alias if isinstance(alias, str) else name)
+ return keys
+
+
+def get_supported_schema_keys() -> set[str]:
+ """Return the non-deprecated top-level properties of
``provider_info.schema.json``."""
+ schema = json.loads((resource_files("airflow") /
"provider_info.schema.json").read_text())
+ return {key for key, spec in schema["properties"].items() if "deprecated"
not in spec}
+
+
+def test_provider_info_response_declares_every_schema_key():
+ """
+ Keys ``ProviderInfoResponse`` does not declare are dropped from the API
response without
+ raising, so a key added to ``provider_info.schema.json`` has to fail here
rather than go
+ missing from ``GET /providers/{package_name}`` unnoticed.
+ """
+ assert get_declared_provider_info_keys() - BUILD_TIME_KEYS ==
get_supported_schema_keys()
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_providers.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_providers.py
index 8571f7597b7..b87a80b7a39 100644
---
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_providers.py
+++
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_providers.py
@@ -35,6 +35,50 @@ MOCK_PROVIDERS = {
"description": "`Amazon Web Services (AWS)
<https://aws.amazon.com/>`__.\n",
"versions": ["1.0.0"],
"documentation-url":
"https://airflow.apache.org/docs/apache-airflow-providers-amazon/1.0.0/",
+ "connection-types": [
+ {
+ "connection-type": "aws",
+ "hook-class-name":
"airflow.providers.amazon.aws.hooks.base_aws.AwsGenericHook",
+ "hook-name": "Amazon Web Services",
+ "ui-field-behaviour": {
+ "hidden-fields": ["host", "schema", "port"],
+ "relabeling": {"login": "AWS Access Key ID"},
+ "placeholders": {"password": "AWS Secret Access Key"},
+ },
+ "conn-fields": {
+ "region_name": {
+ "label": "Region Name",
+ "description": "AWS region",
+ "schema": {"type": "string", "default":
"us-east-1"},
+ }
+ },
+ "external-services": ["AWS"],
+ }
+ ],
+ "extra-links":
["airflow.providers.amazon.aws.links.emr.EmrClusterLink"],
+ "config": {
+ "aws": {
+ "description": "Amazon options",
+ "options": {
+ "log_format": {
+ "description": "Log format",
+ "version_added": "1.0.0",
+ "type": "string",
+ "example": None,
+ "default": "{message}",
+ "is_template": True,
+ },
+ "pod_template_file": {
+ "description": "Pod template",
+ "version_added": "1.0.0",
+ "type": "string",
+ "example": None,
+ "default": None,
+ "see_also": ":ref:`concepts:pod_template_file`",
+ },
+ },
+ }
+ },
},
),
"apache-airflow-providers-apache-cassandra": ProviderInfo(
@@ -49,6 +93,21 @@ MOCK_PROVIDERS = {
),
}
+# A provider whose metadata is made up entirely of the keys shared with the
summary response
+# plus ``email-backends`` — nothing here may be dropped from ``provider_info``.
+MOCK_SENDGRID_PROVIDER = {
+ "apache-airflow-providers-sendgrid": ProviderInfo(
+ "4.2.5",
+ {
+ "package-name": "apache-airflow-providers-sendgrid",
+ "name": "Sendgrid",
+ "description": "`Sendgrid <https://sendgrid.com/>`__\n",
+ "email-backends":
["airflow.providers.sendgrid.utils.emailer.send_email"],
+ "documentation-url":
"https://airflow.apache.org/docs/apache-airflow-providers-sendgrid/4.2.5",
+ },
+ ),
+}
+
class TestGetProviders:
@pytest.mark.parametrize(
@@ -84,3 +143,93 @@ class TestGetProviders:
def test_should_response_403(self, unauthorized_test_client):
response = unauthorized_test_client.get("/providers")
assert response.status_code == 403
+
+
+class TestGetProvider:
+ @mock.patch(
+ "airflow.providers_manager.ProvidersManager.providers",
+ new_callable=mock.PropertyMock,
+ return_value=MOCK_PROVIDERS,
+ )
+ def test_should_respond_200(self, mock_provider, test_client):
+ with assert_queries_count(0):
+ response =
test_client.get("/providers/apache-airflow-providers-amazon")
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body["package_name"] == "apache-airflow-providers-amazon"
+ assert body["version"] == "1.0.0"
+ assert body["description"] == "Amazon Web Services (AWS)
https://aws.amazon.com/"
+ provider_info = body["provider_info"]
+ assert provider_info["name"] == "Amazon"
+ # ``provider.yaml`` keys are kebab-case, the response is snake_case
like the rest of the API
+ assert provider_info["connection_types"] == [
+ {
+ "connection_type": "aws",
+ "hook_class_name":
"airflow.providers.amazon.aws.hooks.base_aws.AwsGenericHook",
+ "hook_name": "Amazon Web Services",
+ "ui_field_behaviour": {
+ "hidden_fields": ["host", "schema", "port"],
+ "relabeling": {"login": "AWS Access Key ID"},
+ "placeholders": {"password": "AWS Secret Access Key"},
+ },
+ "conn_fields": {
+ "region_name": {
+ "label": "Region Name",
+ "description": "AWS region",
+ "schema": {"type": "string", "default": "us-east-1"},
+ }
+ },
+ "external_services": ["AWS"],
+ }
+ ]
+ aws_options = provider_info["config"]["aws"]["options"]
+ assert aws_options["log_format"]["is_template"] is True
+ assert aws_options["pod_template_file"]["see_also"] ==
":ref:`concepts:pod_template_file`"
+ assert provider_info["extra_links"] ==
["airflow.providers.amazon.aws.links.emr.EmrClusterLink"]
+ assert provider_info["package_name"] ==
"apache-airflow-providers-amazon"
+ assert provider_info["description"] == "Amazon Web Services (AWS)
https://aws.amazon.com/"
+ assert (
+ provider_info["documentation_url"]
+ ==
"https://airflow.apache.org/docs/apache-airflow-providers-amazon/1.0.0/"
+ )
+ # Only keys explicitly declared in ProviderInfoResponse are serialized
+ assert "versions" not in provider_info
+
+ @mock.patch(
+ "airflow.providers_manager.ProvidersManager.providers",
+ new_callable=mock.PropertyMock,
+ return_value=MOCK_SENDGRID_PROVIDER,
+ )
+ def test_should_respond_200_with_all_provider_metadata(self,
mock_provider, test_client):
+ response =
test_client.get("/providers/apache-airflow-providers-sendgrid")
+
+ assert response.status_code == 200
+ provider_info = response.json()["provider_info"]
+ # The CLI renders the metadata with ``exclude_none``, so only the
populated keys matter
+ assert {key: value for key, value in provider_info.items() if value is
not None} == {
+ "package_name": "apache-airflow-providers-sendgrid",
+ "name": "Sendgrid",
+ "description": "Sendgrid https://sendgrid.com/",
+ "email_backends":
["airflow.providers.sendgrid.utils.emailer.send_email"],
+ "documentation_url":
"https://airflow.apache.org/docs/apache-airflow-providers-sendgrid/4.2.5",
+ }
+
+ @mock.patch(
+ "airflow.providers_manager.ProvidersManager.providers",
+ new_callable=mock.PropertyMock,
+ return_value=MOCK_PROVIDERS,
+ )
+ def test_should_respond_404(self, mock_provider, test_client):
+ response = test_client.get("/providers/missing-provider")
+
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Provider 'missing-provider' was
not found"}
+
+ def test_should_response_401(self, unauthenticated_test_client):
+ response =
unauthenticated_test_client.get("/providers/apache-airflow-providers-amazon")
+ assert response.status_code == 401
+
+ def test_should_response_403(self, unauthorized_test_client):
+ response =
unauthorized_test_client.get("/providers/apache-airflow-providers-amazon")
+ assert response.status_code == 403
diff --git
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index d3f3932c09a..79ed6d72229 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -145,6 +145,7 @@ TEST_COMMANDS = [
"pools delete test_import_pool",
# Providers commands
"providers list",
+ "providers get apache-airflow-providers-standard",
# Variables commands
"variables create --key=test_key --value=test_value",
"variables list",
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index d9605381065..830500bf60b 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -8,7 +8,7 @@ dags:21ac7c0005cac17bb15fd0dbf2af49fc
dagrun:07035226eaaff0a557d3ad9aab4b41b5
jobs:d4af478f28dae48ee18d43b998edc345
pools:19efe105b9515ab1926ebcaf0e028d71
-providers:34502fe09dc0b8b0a13e7e46efdffda6
+providers:826e68c9de8254293ba810a5c67fa8c7
taskinstances:af95eda24a2bc4c6bd87c9f1324dbc3b
tasks:eb70701dfe1b9baeda39d5eea07fde5f
variables:f8fc76d3d398b2780f4e97f7cd816646
diff --git a/airflow-ctl/docs/images/output_providers.svg
b/airflow-ctl/docs/images/output_providers.svg
index f4ce50e83cd..64b695f5a25 100644
--- a/airflow-ctl/docs/images/output_providers.svg
+++ b/airflow-ctl/docs/images/output_providers.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 294.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 342.79999999999995"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,78 +19,86 @@
font-weight: 700;
}
- .terminal-2177033504-matrix {
+ .terminal-848378382-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2177033504-title {
+ .terminal-848378382-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2177033504-r1 { fill: #ff8700 }
-.terminal-2177033504-r2 { fill: #c5c8c6 }
-.terminal-2177033504-r3 { fill: #808080 }
-.terminal-2177033504-r4 { fill: #68a0b3 }
+ .terminal-848378382-r1 { fill: #ff8700 }
+.terminal-848378382-r2 { fill: #c5c8c6 }
+.terminal-848378382-r3 { fill: #808080 }
+.terminal-848378382-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-2177033504-clip-terminal">
- <rect x="0" y="0" width="914.0" height="243.0" />
+ <clipPath id="terminal-848378382-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="291.79999999999995" />
</clipPath>
- <clipPath id="terminal-2177033504-line-0">
+ <clipPath id="terminal-848378382-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-1">
+<clipPath id="terminal-848378382-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-2">
+<clipPath id="terminal-848378382-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-3">
+<clipPath id="terminal-848378382-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-4">
+<clipPath id="terminal-848378382-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-5">
+<clipPath id="terminal-848378382-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-6">
+<clipPath id="terminal-848378382-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-7">
+<clipPath id="terminal-848378382-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-2177033504-line-8">
+<clipPath id="terminal-848378382-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-848378382-line-9">
+ <rect x="0" y="221.1" width="915" height="24.65"/>
+ </clipPath>
+<clipPath id="terminal-848378382-line-10">
+ <rect x="0" y="245.5" width="915" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="292" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="340.8" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-2177033504-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-848378382-clip-terminal)">
- <g class="terminal-2177033504-matrix">
- <text class="terminal-2177033504-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-2177033504-line-0)">Usage:</text><text
class="terminal-2177033504-r3" x="85.4" y="20" textLength="244"
clip-path="url(#terminal-2177033504-line-0)">airflowctl providers</text><text
class="terminal-2177033504-r2" x="329.4" y="20" textLength="24.4"
clip-path="url(#terminal-2177033504-line-0)"> [</text><text
class="terminal-2177033504-r4" x="353.8" y="20" textLength="24.4" clip-path="u
[...]
-</text><text class="terminal-2177033504-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-2177033504-line-1)">
-</text><text class="terminal-2177033504-r2" x="0" y="68.8" textLength="341.6"
clip-path="url(#terminal-2177033504-line-2)">Perform Providers operations</text><text
class="terminal-2177033504-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-2177033504-line-2)">
-</text><text class="terminal-2177033504-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-2177033504-line-3)">
-</text><text class="terminal-2177033504-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-2177033504-line-4)">Positional Arguments:</text><text
class="terminal-2177033504-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-2177033504-line-4)">
-</text><text class="terminal-2177033504-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-2177033504-line-5)">COMMAND</text><text
class="terminal-2177033504-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-2177033504-line-5)">
-</text><text class="terminal-2177033504-r4" x="48.8" y="166.4"
textLength="48.8" clip-path="url(#terminal-2177033504-line-6)">list</text><text
class="terminal-2177033504-r2" x="170.8" y="166.4" textLength="439.2"
clip-path="url(#terminal-2177033504-line-6)">List all installed Airflow providers</text><text
class="terminal-2177033504-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-2177033504-line-6)">
-</text><text class="terminal-2177033504-r2" x="915" y="190.8"
textLength="12.2" clip-path="url(#terminal-2177033504-line-7)">
-</text><text class="terminal-2177033504-r1" x="0" y="215.2" textLength="97.6"
clip-path="url(#terminal-2177033504-line-8)">Options:</text><text
class="terminal-2177033504-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-2177033504-line-8)">
-</text><text class="terminal-2177033504-r4" x="24.4" y="239.6"
textLength="24.4" clip-path="url(#terminal-2177033504-line-9)">-h</text><text
class="terminal-2177033504-r2" x="48.8" y="239.6" textLength="24.4"
clip-path="url(#terminal-2177033504-line-9)">, </text><text
class="terminal-2177033504-r4" x="73.2" y="239.6" textLength="73.2"
clip-path="url(#terminal-2177033504-line-9)">--help</text><text
class="terminal-2177033504-r2" x="170.8" y="239.6" textLength="378.2"
clip-path="url(# [...]
+ <g class="terminal-848378382-matrix">
+ <text class="terminal-848378382-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-848378382-line-0)">Usage:</text><text
class="terminal-848378382-r3" x="85.4" y="20" textLength="244"
clip-path="url(#terminal-848378382-line-0)">airflowctl providers</text><text
class="terminal-848378382-r2" x="329.4" y="20" textLength="24.4"
clip-path="url(#terminal-848378382-line-0)"> [</text><text
class="terminal-848378382-r4" x="353.8" y="20" textLength="24.4"
clip-path="url(#ter [...]
+</text><text class="terminal-848378382-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-848378382-line-1)">
+</text><text class="terminal-848378382-r2" x="0" y="68.8" textLength="341.6"
clip-path="url(#terminal-848378382-line-2)">Perform Providers operations</text><text
class="terminal-848378382-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-848378382-line-2)">
+</text><text class="terminal-848378382-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-848378382-line-3)">
+</text><text class="terminal-848378382-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-848378382-line-4)">Positional Arguments:</text><text
class="terminal-848378382-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-848378382-line-4)">
+</text><text class="terminal-848378382-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-848378382-line-5)">COMMAND</text><text
class="terminal-848378382-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-848378382-line-5)">
+</text><text class="terminal-848378382-r4" x="48.8" y="166.4"
textLength="36.6" clip-path="url(#terminal-848378382-line-6)">get</text><text
class="terminal-848378382-r2" x="170.8" y="166.4" textLength="610"
clip-path="url(#terminal-848378382-line-6)">Retrieve an installed provider by its package name</text><text
class="terminal-848378382-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-848378382-line-6)">
+</text><text class="terminal-848378382-r4" x="48.8" y="190.8"
textLength="48.8" clip-path="url(#terminal-848378382-line-7)">list</text><text
class="terminal-848378382-r2" x="170.8" y="190.8" textLength="439.2"
clip-path="url(#terminal-848378382-line-7)">List all installed Airflow providers</text><text
class="terminal-848378382-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-848378382-line-7)">
+</text><text class="terminal-848378382-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-848378382-line-8)">
+</text><text class="terminal-848378382-r1" x="0" y="239.6" textLength="97.6"
clip-path="url(#terminal-848378382-line-9)">Options:</text><text
class="terminal-848378382-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-848378382-line-9)">
+</text><text class="terminal-848378382-r4" x="24.4" y="264" textLength="24.4"
clip-path="url(#terminal-848378382-line-10)">-h</text><text
class="terminal-848378382-r2" x="48.8" y="264" textLength="24.4"
clip-path="url(#terminal-848378382-line-10)">, </text><text
class="terminal-848378382-r4" x="73.2" y="264" textLength="73.2"
clip-path="url(#terminal-848378382-line-10)">--help</text><text
class="terminal-848378382-r2" x="170.8" y="264" textLength="378.2"
clip-path="url(#terminal-848 [...]
+</text><text class="terminal-848378382-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-848378382-line-11)">
</text>
</g>
</g>
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 6bf97f5dc2e..fb299db1a10 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1246,6 +1246,109 @@ class PoolResponse(BaseModel):
team_name: Annotated[str | None, Field(title="Team Name")]
+class ProviderAssetUriInfo(BaseModel):
+ """
+ Asset URI scheme handling provided by a provider.
+ """
+
+ schemes: Annotated[list[str], Field(title="Schemes")]
+ handler: Annotated[str | None, Field(title="Handler")] = None
+ factory: Annotated[str | None, Field(title="Factory")] = None
+ to_openlineage_converter: Annotated[str | None, Field(title="To
Openlineage Converter")] = None
+
+
+class ProviderConfigOptionInfo(BaseModel):
+ """
+ Configuration option contributed by a provider.
+ """
+
+ description: Annotated[str | None, Field(title="Description")] = None
+ version_added: Annotated[str | None, Field(title="Version Added")] = None
+ type: Annotated[str | None, Field(title="Type")] = None
+ example: Annotated[str | int | float | None, Field(title="Example")] = None
+ default: Annotated[str | int | float | None, Field(title="Default")] = None
+ sensitive: Annotated[bool | None, Field(title="Sensitive")] = None
+ is_template: Annotated[bool | None, Field(title="Is Template")] = None
+ see_also: Annotated[str | None, Field(title="See Also")] = None
+
+
+class ProviderConfigSectionInfo(BaseModel):
+ """
+ Configuration section contributed by a provider.
+ """
+
+ description: Annotated[str | None, Field(title="Description")] = None
+ options: Annotated[dict[str, ProviderConfigOptionInfo] | None,
Field(title="Options")] = None
+
+
+class ProviderConnectionFieldInfo(BaseModel):
+ """
+ Custom connection field a connection type stores in ``Connection.extra``.
+ """
+
+ label: Annotated[str, Field(title="Label")]
+ description: Annotated[str | None, Field(title="Description")] = None
+ schema_: Annotated[dict[str, Any], Field(alias="schema", title="Schema")]
+
+
+class ProviderConnectionUiFieldBehaviourInfo(BaseModel):
+ """
+ Customizations a connection type applies to the standard connection form
fields.
+ """
+
+ hidden_fields: Annotated[list[str] | None, Field(title="Hidden Fields")] =
None
+ relabeling: Annotated[dict[str, str] | None, Field(title="Relabeling")] =
None
+ placeholders: Annotated[dict[str, str] | None,
Field(title="Placeholders")] = None
+
+
+class ProviderDialectInfo(BaseModel):
+ """
+ SQL dialect provided by a provider.
+ """
+
+ dialect_type: Annotated[str | None, Field(title="Dialect Type")] = None
+ dialect_class_name: Annotated[str | None, Field(title="Dialect Class
Name")] = None
+
+
+class ProviderIntegrationInfo(BaseModel):
+ """
+ Integration provided by a provider.
+ """
+
+ integration_name: Annotated[str, Field(title="Integration Name")]
+ external_doc_url: Annotated[str | None, Field(title="External Doc Url")] =
None
+ how_to_guide: Annotated[list[str] | None, Field(title="How To Guide")] =
None
+ logo: Annotated[str | None, Field(title="Logo")] = None
+ tags: Annotated[list[str] | None, Field(title="Tags")] = None
+
+
+class ProviderIntegrationModulesInfo(BaseModel):
+ """
+ Python modules an integration contributes for one kind of functionality.
+ """
+
+ integration_name: Annotated[str, Field(title="Integration Name")]
+ python_modules: Annotated[list[str], Field(title="Python Modules")]
+
+
+class ProviderPluginInfo(BaseModel):
+ """
+ Plugin provided by a provider.
+ """
+
+ name: Annotated[str | None, Field(title="Name")] = None
+ plugin_class: Annotated[str | None, Field(title="Plugin Class")] = None
+
+
+class ProviderRemoteLoggingInfo(BaseModel):
+ """
+ Remote logging IO handler provided by a provider.
+ """
+
+ classpath: Annotated[str, Field(title="Classpath")]
+ scheme: Annotated[str, Field(title="Scheme")]
+
+
class ProviderResponse(BaseModel):
"""
Provider serializer for responses.
@@ -1257,6 +1360,26 @@ class ProviderResponse(BaseModel):
documentation_url: Annotated[str | None, Field(title="Documentation Url")]
+class ProviderTaskDecoratorInfo(BaseModel):
+ """
+ TaskFlow decorator provided by a provider.
+ """
+
+ name: Annotated[str | None, Field(title="Name")] = None
+ class_name: Annotated[str | None, Field(title="Class Name")] = None
+
+
+class ProviderTransferInfo(BaseModel):
+ """
+ Transfer operator provided by a provider.
+ """
+
+ source_integration_name: Annotated[str, Field(title="Source Integration
Name")]
+ target_integration_name: Annotated[str, Field(title="Target Integration
Name")]
+ python_module: Annotated[str, Field(title="Python Module")]
+ how_to_guide: Annotated[str | None, Field(title="How To Guide")] = None
+
+
class QueuedEventResponse(BaseModel):
"""
Queued Event serializer for responses..
@@ -2645,6 +2768,58 @@ class ProviderCollectionResponse(BaseModel):
total_entries: Annotated[int, Field(title="Total Entries")]
+class ProviderConnectionTypeInfo(BaseModel):
+ """
+ Connection type provided by a provider.
+ """
+
+ connection_type: Annotated[str, Field(title="Connection Type")]
+ hook_class_name: Annotated[str, Field(title="Hook Class Name")]
+ hook_name: Annotated[str | None, Field(title="Hook Name")] = None
+ ui_field_behaviour: ProviderConnectionUiFieldBehaviourInfo | None = None
+ conn_fields: Annotated[dict[str, ProviderConnectionFieldInfo] | None,
Field(title="Conn Fields")] = None
+ external_services: Annotated[list[str] | None, Field(title="External
Services")] = None
+
+
+class ProviderInfoResponse(BaseModel):
+ """
+ Typed provider metadata (from ``provider.yaml``) exposed by the API.
+ """
+
+ package_name: Annotated[str | None, Field(title="Package Name")] = None
+ name: Annotated[str | None, Field(title="Name")] = None
+ description: Annotated[str | None, Field(title="Description")] = None
+ documentation_url: Annotated[str | None, Field(title="Documentation Url")]
= None
+ filesystems: Annotated[list[str] | None, Field(title="Filesystems")] = None
+ integrations: Annotated[list[ProviderIntegrationInfo] | None,
Field(title="Integrations")] = None
+ operators: Annotated[list[ProviderIntegrationModulesInfo] | None,
Field(title="Operators")] = None
+ sensors: Annotated[list[ProviderIntegrationModulesInfo] | None,
Field(title="Sensors")] = None
+ hooks: Annotated[list[ProviderIntegrationModulesInfo] | None,
Field(title="Hooks")] = None
+ triggers: Annotated[list[ProviderIntegrationModulesInfo] | None,
Field(title="Triggers")] = None
+ bundles: Annotated[list[ProviderIntegrationModulesInfo] | None,
Field(title="Bundles")] = None
+ asset_uris: Annotated[list[ProviderAssetUriInfo] | None,
Field(title="Asset Uris")] = None
+ dialects: Annotated[list[ProviderDialectInfo] | None,
Field(title="Dialects")] = None
+ transfers: Annotated[list[ProviderTransferInfo] | None,
Field(title="Transfers")] = None
+ connection_types: Annotated[list[ProviderConnectionTypeInfo] | None,
Field(title="Connection Types")] = (
+ None
+ )
+ extra_links: Annotated[list[str] | None, Field(title="Extra Links")] = None
+ secrets_backends: Annotated[list[str] | None, Field(title="Secrets
Backends")] = None
+ email_backends: Annotated[list[str] | None, Field(title="Email Backends")]
= None
+ logging: Annotated[list[str] | None, Field(title="Logging")] = None
+ remote_logging: Annotated[list[ProviderRemoteLoggingInfo] | None,
Field(title="Remote Logging")] = None
+ auth_backends: Annotated[list[str] | None, Field(title="Auth Backends")] =
None
+ auth_managers: Annotated[list[str] | None, Field(title="Auth Managers")] =
None
+ notifications: Annotated[list[str] | None, Field(title="Notifications")] =
None
+ executors: Annotated[list[str] | None, Field(title="Executors")] = None
+ db_managers: Annotated[list[str] | None, Field(title="Db Managers")] = None
+ cli: Annotated[list[str] | None, Field(title="Cli")] = None
+ config: Annotated[dict[str, ProviderConfigSectionInfo] | None,
Field(title="Config")] = None
+ task_decorators: Annotated[list[ProviderTaskDecoratorInfo] | None,
Field(title="Task Decorators")] = None
+ plugins: Annotated[list[ProviderPluginInfo] | None,
Field(title="Plugins")] = None
+ queues: Annotated[list[str] | None, Field(title="Queues")] = None
+
+
class QueuedEventCollectionResponse(BaseModel):
"""
Queued Event Collection serializer for responses.
@@ -3122,6 +3297,18 @@ class PluginCollectionResponse(BaseModel):
total_entries: Annotated[int, Field(title="Total Entries")]
+class ProviderDetailsResponse(BaseModel):
+ """
+ Detailed provider serializer for responses.
+ """
+
+ package_name: Annotated[str, Field(title="Package Name")]
+ description: Annotated[str, Field(title="Description")]
+ version: Annotated[str, Field(title="Version")]
+ documentation_url: Annotated[str | None, Field(title="Documentation Url")]
+ provider_info: ProviderInfoResponse
+
+
class TaskCollectionResponse(BaseModel):
"""
Task collection serializer for responses.
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index bc92952ac2f..4e0c9f962ce 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -70,6 +70,7 @@ from airflowctl.api.datamodels.generated import (
PoolPatchBody,
PoolResponse,
ProviderCollectionResponse,
+ ProviderDetailsResponse,
QueuedEventCollectionResponse,
QueuedEventResponse,
TaskDependencyCollectionResponse,
@@ -720,6 +721,11 @@ class PoolsOperations(BaseOperations):
class ProvidersOperations(BaseOperations):
"""Provider operations."""
+ def get(self, provider_name: str) -> ProviderDetailsResponse |
ServerResponseError:
+ """Get detailed information for a provider."""
+ self.response = self.client.get(f"providers/{provider_name}")
+ return
ProviderDetailsResponse.model_validate_json(self.response.content)
+
def list(self) -> ProviderCollectionResponse | ServerResponseError:
"""List all providers."""
return super().execute_list(path="providers",
data_model=ProviderCollectionResponse)
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 630bc1d4af1..1f5dd5b2691 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -449,6 +449,19 @@ ARG_CONFIG_VERBOSE = Arg(
action="store_true",
)
+# Provider arguments
+ARG_PROVIDER_NAME = Arg(
+ flags=("provider_name",),
+ type=str,
+ help="The package name of the provider",
+)
+ARG_PROVIDER_FULL = Arg(
+ flags=("-f", "--full"),
+ help="Show the complete provider metadata",
+ default=False,
+ action="store_true",
+)
+
# Version Command Args
ARG_REMOTE = Arg(
flags=("--remote",),
@@ -1075,7 +1088,9 @@ def merge_commands(
base_commands: list[CLICommand], commands_will_be_merged: list[CLICommand]
) -> list[CLICommand]:
"""
- Merge group commands with existing commands which extends base_commands
with will_be_merged commands.
+ Merge custom commands into generated commands.
+
+ Custom subcommands replace generated subcommands with the same name.
Args:
base_commands: List of base commands to be extended.
@@ -1097,8 +1112,12 @@ def merge_commands(
if command.name in merge_command_map.keys():
merged_command = merge_command_map[command.name]
if isinstance(command, GroupCommand):
- # Merge common group command with existing group command
- current_subcommands = list(command.subcommands)
+ replacement_names = {subcommand.name for subcommand in
merged_command.subcommands}
+ current_subcommands = [
+ subcommand
+ for subcommand in command.subcommands
+ if subcommand.name not in replacement_names
+ ]
current_subcommands.extend(list(merged_command.subcommands))
new_commands.append(
GroupCommand(
@@ -1355,6 +1374,19 @@ JOB_COMMANDS = (
),
)
+PROVIDER_COMMANDS = (
+ ActionCommand(
+ name="get",
+ help="Retrieve an installed provider by its package name",
+
func=lazy_load_command("airflowctl.ctl.commands.provider_command.get_provider"),
+ args=(
+ ARG_PROVIDER_NAME,
+ ARG_PROVIDER_FULL,
+ ARG_OUTPUT,
+ ),
+ ),
+)
+
core_commands: list[CLICommand] = [
GroupCommand(
name="auth",
@@ -1387,6 +1419,11 @@ core_commands: list[CLICommand] = [
help="Manage Airflow pools",
subcommands=POOL_COMMANDS,
),
+ GroupCommand(
+ name="providers",
+ help="Manage installed Airflow providers",
+ subcommands=PROVIDER_COMMANDS,
+ ),
GroupCommand(
name="tasks",
help="Manage Airflow tasks",
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
b/airflow-ctl/src/airflowctl/ctl/commands/provider_command.py
similarity index 52%
copy from airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
copy to airflow-ctl/src/airflowctl/ctl/commands/provider_command.py
index ab4381c231b..539c3ff7e2a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/provider_command.py
@@ -14,23 +14,24 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+"""Command functions for installed Airflow providers."""
from __future__ import annotations
-from airflow.api_fastapi.core_api.base import BaseModel
+from typing import Any
+from airflowctl.api.client import NEW_API_CLIENT, Client, ClientKind,
provide_api_client
+from airflowctl.ctl.console_formatting import AirflowConsole
-class ProviderResponse(BaseModel):
- """Provider serializer for responses."""
- package_name: str
- description: str
- version: str
- documentation_url: str | None
+@provide_api_client(kind=ClientKind.CLI)
+def get_provider(args, api_client: Client = NEW_API_CLIENT) -> dict[str, Any]:
+ """Get information for an installed provider."""
+ response = api_client.providers.get(provider_name=args.provider_name)
+ if args.full:
+ result = response.provider_info.model_dump(by_alias=True,
exclude_none=True)
+ else:
+ result = {"Provider": response.package_name, "Version":
response.version}
-
-class ProviderCollectionResponse(BaseModel):
- """Provider Collection serializer for responses."""
-
- providers: list[ProviderResponse]
- total_entries: int
+ AirflowConsole().print_as(data=[result], output=args.output)
+ return result
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index e0141373ef2..665cef60b3b 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -89,6 +89,7 @@ pools:
update: "Update an existing pool"
providers:
+ get: "Retrieve an installed provider by its package name"
list: "List all installed Airflow providers"
taskinstances:
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index b01f5231544..6f93d4f20b3 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -92,6 +92,8 @@ from airflowctl.api.datamodels.generated import (
PoolPatchBody,
PoolResponse,
ProviderCollectionResponse,
+ ProviderDetailsResponse,
+ ProviderInfoResponse,
ProviderResponse,
QueuedEventCollectionResponse,
QueuedEventResponse,
@@ -1924,6 +1926,26 @@ class TestProvidersOperations:
providers=[provider_response],
total_entries=1,
)
+ provider_details_response = ProviderDetailsResponse(
+ package_name="package_name",
+ version="version",
+ description="description",
+ documentation_url=None,
+ provider_info=ProviderInfoResponse.model_validate(
+ {"name": "Provider", "extra_links":
["airflow.providers.package.links.Link"]}
+ ),
+ )
+
+ def test_get(self):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == "/api/v2/providers/package_name"
+ return httpx.Response(
+ 200,
json=json.loads(self.provider_details_response.model_dump_json(by_alias=True))
+ )
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.providers.get("package_name")
+ assert response == self.provider_details_response
def test_list(self):
def handle_request(request: httpx.Request) -> httpx.Response:
diff --git
a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_provider_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_provider_command.py
new file mode 100644
index 00000000000..8ec1bf49f31
--- /dev/null
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_provider_command.py
@@ -0,0 +1,107 @@
+# 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.
+"""Tests for provider commands."""
+
+from __future__ import annotations
+
+import pytest
+
+from airflowctl.api.client import ClientKind
+from airflowctl.api.datamodels.generated import ProviderDetailsResponse,
ProviderInfoResponse
+from airflowctl.ctl import cli_parser
+from airflowctl.ctl.commands import provider_command
+
+
+class TestProviderCommands:
+ parser = cli_parser.get_parser()
+ provider_name = "apache-airflow-providers-standard"
+ provider_info = {
+ "name": "Standard",
+ "connection_types": [
+ {
+ "connection_type": "fs",
+ "hook_class_name":
"airflow.providers.standard.hooks.filesystem.FSHook",
+ "hook_name": "File (path)",
+ "ui_field_behaviour": {"hidden_fields": ["host", "port"]},
+ "conn_fields": {
+ "path": {"label": "Path", "schema": {"type": "string"}},
+ },
+ "external_services": ["Local filesystem"],
+ }
+ ],
+ "extra_links":
["airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink"],
+ }
+ provider_response = ProviderDetailsResponse(
+ package_name=provider_name,
+ version="1.0.0",
+ description="Standard provider",
+ documentation_url=None,
+ provider_info=ProviderInfoResponse.model_validate(provider_info),
+ )
+
+ @pytest.mark.parametrize(
+ ("extra_args", "expected"),
+ [
+ ([], {"Provider": provider_name, "Version": "1.0.0"}),
+ (["--full"], provider_info),
+ (["-f"], provider_info),
+ ],
+ ids=["summary", "full", "full-short"],
+ )
+ def test_get_provider(self, api_client_maker, extra_args, expected):
+ api_client = api_client_maker(
+ path=f"/api/v2/providers/{self.provider_name}",
+ response_json=self.provider_response.model_dump(mode="json",
by_alias=True),
+ expected_http_status_code=200,
+ kind=ClientKind.CLI,
+ )
+ args = self.parser.parse_args(["providers", "get", self.provider_name,
*extra_args])
+
+ result = provider_command.get_provider(args, api_client=api_client)
+
+ assert result == expected
+
+ sendgrid_name = "apache-airflow-providers-sendgrid"
+ # Metadata made up entirely of keys that also exist on the summary
response, plus
+ # ``email-backends`` — ``--full`` must keep every one of them.
+ sendgrid_info = {
+ "package_name": sendgrid_name,
+ "name": "Sendgrid",
+ "description": "Sendgrid https://sendgrid.com/",
+ "email_backends":
["airflow.providers.sendgrid.utils.emailer.send_email"],
+ "documentation_url":
"https://airflow.apache.org/docs/apache-airflow-providers-sendgrid/4.2.5",
+ }
+
+ def test_get_provider_full_keeps_all_metadata(self, api_client_maker):
+ provider_response = ProviderDetailsResponse(
+ package_name=self.sendgrid_name,
+ version="4.2.5",
+ description=self.sendgrid_info["description"],
+ documentation_url=self.sendgrid_info["documentation_url"],
+
provider_info=ProviderInfoResponse.model_validate(self.sendgrid_info),
+ )
+ api_client = api_client_maker(
+ path=f"/api/v2/providers/{self.sendgrid_name}",
+ response_json=provider_response.model_dump(mode="json",
by_alias=True),
+ expected_http_status_code=200,
+ kind=ClientKind.CLI,
+ )
+ args = self.parser.parse_args(["providers", "get", self.sendgrid_name,
"--full"])
+
+ result = provider_command.get_provider(args, api_client=api_client)
+
+ assert result == self.sendgrid_info
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index ce8b76fa43e..b41790be1df 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -786,6 +786,27 @@ class TestCliConfigMethods:
assert "subcommand3" in sub_command_names
assert "subcommand4" in sub_command_names
+ def test_merge_commands_replaces_duplicate_subcommand(self, no_op_method):
+ generated = GroupCommand(
+ name="providers",
+ help="Generated providers commands",
+ subcommands=(
+ ActionCommand(name="get", help="Generated get",
func=no_op_method, args=()),
+ ActionCommand(name="list", help="Generated list",
func=no_op_method, args=()),
+ ),
+ )
+ custom = GroupCommand(
+ name="providers",
+ help="Custom providers commands",
+ subcommands=(ActionCommand(name="get", help="Custom get",
func=no_op_method, args=()),),
+ )
+
+ merged = merge_commands(base_commands=[generated],
commands_will_be_merged=[custom])
+
+ subcommands = list(merged[0].subcommands)
+ assert [command.name for command in subcommands] == ["list", "get"]
+ assert subcommands[-1].help == "Custom get"
+
def test_add_auth_token_to_all_commands(self, no_op_method):
"""Test the add_auth_token_to_all_commands method."""
ARG_1 = Arg(
diff --git a/providers/fab/docs/auth-manager/access-control.rst
b/providers/fab/docs/auth-manager/access-control.rst
index 551e1c3c1c2..58554ae78af 100644
--- a/providers/fab/docs/auth-manager/access-control.rst
+++ b/providers/fab/docs/auth-manager/access-control.rst
@@ -756,6 +756,10 @@ Stable API Permissions
- GET
- Providers.can_read
- Op
+ * - ``/api/v2/providers/{provider_name}``
+ - GET
+ - Providers.can_read
+ - Op
* - ``/api/v2/variables``
- GET
- Variables.can_read