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 8b515fafd2da7..ab4381c231b55 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 @@ -26,6 +26,7 @@ class ProviderResponse(BaseModel): package_name: str description: str version: str + documentation_url: str | None class ProviderCollectionResponse(BaseModel): 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 276fef3de3b25..63b0bda97f554 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 @@ -12014,11 +12014,17 @@ components: version: type: string title: Version + documentation_url: + anyOf: + - type: string + - type: 'null' + title: Documentation Url type: object required: - package_name - description - version + - documentation_url title: ProviderResponse description: Provider serializer for responses. QueuedEventCollectionResponse: 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 2f9fa3a1b86d4..8d659792b9771 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 @@ -31,4 +31,5 @@ def _provider_mapper(provider: ProviderInfo) -> ProviderResponse: package_name=provider.data["package-name"], description=_remove_rst_syntax(provider.data["description"]), version=provider.version, + documentation_url=provider.data["documentation-url"], ) diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index b13ab7ccd6cf7..0ddf7188021c9 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -601,6 +601,21 @@ def _discover_all_providers_from_packages(self) -> None: f"The package '{package_name}' from packaging information " f"{provider_info_package_name} do not match. Please make sure they are aligned" ) + + # issue-59576: Retrieve the project.urls.documentation from dist.metadata + project_urls = dist.metadata.get_all("Project-URL") + documentation_url: str | None = None + + if project_urls: + for entry in project_urls: + if "," in entry: + name, url = entry.split(",") + if name.strip().lower() == "documentation": + documentation_url = url + break + + provider_info["documentation-url"] = documentation_url + if package_name not in self._provider_dict: self._provider_dict[package_name] = ProviderInfo(version, provider_info) else: 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 d4a506eab4255..9ca36e3c90b0c 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 @@ -4782,10 +4782,21 @@ export const $ProviderResponse = { version: { type: 'string', title: 'Version' + }, + documentation_url: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Documentation Url' } }, type: 'object', - required: ['package_name', 'description', 'version'], + required: ['package_name', 'description', 'version', 'documentation_url'], title: 'ProviderResponse', description: 'Provider serializer for responses.' } as const; 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 bada4005bce18..3ef363b800072 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 @@ -1246,6 +1246,7 @@ export type ProviderResponse = { package_name: string; description: string; version: string; + documentation_url: string | null; }; /** diff --git a/airflow-core/src/airflow/ui/src/pages/Providers.tsx b/airflow-core/src/airflow/ui/src/pages/Providers.tsx index 8602f3534e993..9279fb648d978 100644 --- a/airflow-core/src/airflow/ui/src/pages/Providers.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Providers.tsx @@ -36,7 +36,10 @@ const createColumns = (translate: TFunction): Array> diff --git a/airflow-core/tests/unit/always/test_providers_manager.py b/airflow-core/tests/unit/always/test_providers_manager.py index 5fba4569c80c4..01c1ded00cc59 100644 --- a/airflow-core/tests/unit/always/test_providers_manager.py +++ b/airflow-core/tests/unit/always/test_providers_manager.py @@ -67,8 +67,10 @@ def test_providers_are_loaded(self): for provider in provider_list: package_name = provider_manager.providers[provider].data["package-name"] version = provider_manager.providers[provider].version + documentation_url = provider_manager.providers[provider].data["documentation-url"] assert re.search(r"[0-9]*\.[0-9]*\.[0-9]*.*", version) assert package_name == provider + assert isinstance(documentation_url, str) # just a coherence check - no exact number as otherwise we would have to update # several tests if we add new connections/provider which is not ideal assert len(provider_list) > 65 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 9b31da0dc9fc3..8571f7597b714 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 @@ -34,6 +34,7 @@ "name": "Amazon", "description": "`Amazon Web Services (AWS) `__.\n", "versions": ["1.0.0"], + "documentation-url": "https://airflow.apache.org/docs/apache-airflow-providers-amazon/1.0.0/", }, ), "apache-airflow-providers-apache-cassandra": ProviderInfo( @@ -43,6 +44,7 @@ "name": "Apache Cassandra", "description": "`Apache Cassandra `__.\n", "versions": ["1.0.0"], + "documentation-url": "https://airflow.apache.org/docs/apache-airflow-providers-apache-cassandra/1.0.0/", }, ), } diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 28ae65fb01491..71bff5eea1534 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -676,6 +676,7 @@ class ProviderResponse(BaseModel): 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")] = None class QueuedEventResponse(BaseModel):