Skip to content

Commit 5e77ef2

Browse files
github-actions[bot]yuseok89
authored andcommitted
[v3-2-test] Load hook metadata from YAML without importing Hook class (#63826) (#64723)
* Load hook metadata from YAML without importing Hook class * Add hook-name to all provider.yaml connection-types * Add hook-name to connection types and regenerate get_provider_info.py * Fix ruff import order in connections.py * fix: import ProvidersManager at top level per review * Fix provider connection hook display names * Add iter_connection_type_hook_ui_metadata for connection UI hook metadata (cherry picked from commit c4a209b) Co-authored-by: Yuseok Jo <yuseok89@gmail.com>
1 parent 7fb2883 commit 5e77ef2

177 files changed

Lines changed: 365 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
ConnectionHookMetaData,
2828
StandardHookFields,
2929
)
30+
from airflow.providers_manager import HookInfo, ProvidersManager
3031
from airflow.serialization.definitions.param import SerializedParam
3132

3233
if TYPE_CHECKING:
33-
from airflow.providers_manager import ConnectionFormWidgetInfo, HookInfo
34+
from airflow.providers_manager import ConnectionFormWidgetInfo
3435

3536
log = logging.getLogger(__name__)
3637

@@ -125,8 +126,6 @@ def _get_hooks_with_mocked_fab() -> tuple[
125126
"""Get hooks with all details w/o FAB needing to be installed."""
126127
from unittest import mock
127128

128-
from airflow.providers_manager import ProvidersManager
129-
130129
def mock_lazy_gettext(txt: str) -> str:
131130
"""Mock for flask_babel.lazy_gettext."""
132131
return txt
@@ -225,19 +224,16 @@ def _convert_extra_fields(form_widgets: dict[str, ConnectionFormWidgetInfo]) ->
225224
@staticmethod
226225
@cache
227226
def hook_meta_data() -> list[ConnectionHookMetaData]:
228-
hooks, connection_form_widgets, field_behaviours = HookMetaService._get_hooks_with_mocked_fab()
229-
result: list[ConnectionHookMetaData] = []
230-
widgets = HookMetaService._convert_extra_fields(connection_form_widgets)
231-
for hook_key, hook_info in hooks.items():
232-
if not hook_info:
233-
continue
234-
hook_meta = ConnectionHookMetaData(
235-
connection_type=hook_key,
236-
hook_class_name=hook_info.hook_class_name,
237-
default_conn_name=None, # TODO: later
238-
hook_name=hook_info.hook_name,
239-
standard_fields=HookMetaService._make_standard_fields(field_behaviours.get(hook_key)),
240-
extra_fields=widgets.get(hook_key),
227+
pm = ProvidersManager()
228+
widgets = HookMetaService._convert_extra_fields(pm._connection_form_widgets_from_metadata)
229+
return [
230+
ConnectionHookMetaData(
231+
connection_type=meta.connection_type,
232+
hook_class_name=meta.hook_class_name,
233+
default_conn_name=None,
234+
hook_name=meta.hook_name,
235+
standard_fields=HookMetaService._make_standard_fields(meta.field_behaviour),
236+
extra_fields=widgets.get(meta.connection_type),
241237
)
242-
result.append(hook_meta)
243-
return result
238+
for meta in pm.iter_connection_type_hook_ui_metadata()
239+
]

airflow-core/src/airflow/provider.yaml.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@
378378
"description": "Hook class name that implements the connection type",
379379
"type": "string"
380380
},
381+
"hook-name": {
382+
"description": "Display name for the connection type in the UI (e.g. 'File (path)', 'Slack')",
383+
"type": "string"
384+
},
381385
"ui-field-behaviour": {
382386
"description": "Customizations for standard connection form fields",
383387
"type": "object",

airflow-core/src/airflow/provider_info.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@
298298
"hook-class-name": {
299299
"description": "Hook class name that implements the connection type",
300300
"type": "string"
301+
},
302+
"hook-name": {
303+
"description": "Display name for the connection type in the UI",
304+
"type": "string"
301305
}
302306
},
303307
"required": [

airflow-core/src/airflow/providers_manager.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import logging
2727
import traceback
2828
import warnings
29-
from collections.abc import Callable, MutableMapping
29+
from collections.abc import Callable, Iterator, MutableMapping
3030
from dataclasses import dataclass
3131
from functools import wraps
3232
from importlib.resources import files as resource_files
@@ -243,6 +243,15 @@ class HookInfo(NamedTuple):
243243
dialects: list[str] = []
244244

245245

246+
class ConnectionTypeHookUIMetadata(NamedTuple):
247+
"""Hook metadata for one connection type (connection UI); ``field_behaviour`` is standard fields."""
248+
249+
connection_type: str
250+
hook_name: str
251+
hook_class_name: str | None
252+
field_behaviour: dict | None
253+
254+
246255
class ConnectionFormWidgetInfo(NamedTuple):
247256
"""Connection Form Widget information."""
248257

@@ -413,6 +422,8 @@ def __init__(self):
413422
self._dialect_provider_dict: dict[str, DialectInfo] = {}
414423
# Keeps dict of hooks keyed by connection type. They are lazy evaluated at access time
415424
self._hooks_lazy_dict: LazyDictWithCache[str, HookInfo | Callable] = LazyDictWithCache()
425+
# Keeps hook display names read from provider.yaml (hook-name field)
426+
self._hook_name_dict: dict[str, str] = {}
416427
# Keeps methods that should be used to add custom widgets tuple of keyed by name of the extra field
417428
self._connection_form_widgets: dict[str, ConnectionFormWidgetInfo] = {}
418429
# Customizations for javascript fields are kept here
@@ -979,6 +990,9 @@ def _load_ui_metadata(self) -> None:
979990
if not connection_type or not hook_class_name:
980991
continue
981992

993+
if hook_name := conn_config.get("hook-name"):
994+
self._hook_name_dict[connection_type] = hook_name
995+
982996
if conn_fields := conn_config.get("conn-fields"):
983997
self._add_widgets(package_name, hook_class_name, connection_type, conn_fields)
984998

@@ -1349,6 +1363,45 @@ def hooks(self) -> MutableMapping[str, HookInfo | None]:
13491363
# When we return hooks here it will only be used to retrieve hook information
13501364
return self._hooks_lazy_dict
13511365

1366+
def iter_connection_type_hook_ui_metadata(self) -> Iterator[ConnectionTypeHookUIMetadata]:
1367+
"""
1368+
Yield hook metadata per connection type for the connection UI.
1369+
1370+
Does not import hook classes.
1371+
"""
1372+
self.initialize_providers_hooks()
1373+
all_types = frozenset(self._hooks_lazy_dict) | frozenset(self._hook_provider_dict)
1374+
for conn_type in sorted(all_types):
1375+
raw_entry = self._hooks_lazy_dict._raw_dict.get(conn_type)
1376+
provider_entry = self._hook_provider_dict.get(conn_type)
1377+
if isinstance(raw_entry, HookInfo):
1378+
hook_name = raw_entry.hook_name
1379+
hook_class_name = raw_entry.hook_class_name
1380+
elif provider_entry:
1381+
hook_name = self._hook_name_dict.get(conn_type, conn_type)
1382+
hook_class_name = provider_entry.hook_class_name
1383+
else:
1384+
hook_name = self._hook_name_dict.get(conn_type, conn_type)
1385+
hook_class_name = None
1386+
yield ConnectionTypeHookUIMetadata(
1387+
connection_type=conn_type,
1388+
hook_name=hook_name,
1389+
hook_class_name=hook_class_name,
1390+
field_behaviour=self._field_behaviours.get(conn_type),
1391+
)
1392+
1393+
@property
1394+
def _connection_form_widgets_from_metadata(self) -> dict[str, ConnectionFormWidgetInfo]:
1395+
"""Return connection form widgets from metadata without importing every hook."""
1396+
self.initialize_providers_hooks()
1397+
return self._connection_form_widgets
1398+
1399+
@property
1400+
def _field_behaviours_from_metadata(self) -> dict[str, dict]:
1401+
"""Return field behaviour dicts from metadata without importing every hook."""
1402+
self.initialize_providers_hooks()
1403+
return self._field_behaviours
1404+
13521405
@property
13531406
def dialects(self) -> MutableMapping[str, DialectInfo]:
13541407
"""Return dictionary of connection_type-to-dialect mapping."""

airflow-core/tests/unit/always/test_providers_manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,14 @@ def test_load_ui_for_http_provider(self):
428428
assert "relabeling" in behaviour
429429
assert "placeholders" in behaviour
430430

431+
def test_iter_connection_type_hook_ui_metadata_matches_field_behaviours(self):
432+
"""iter_connection_type_hook_ui_metadata should expose the same standard-field behaviour dict."""
433+
pm = ProvidersManager()
434+
pm.initialize_providers_hooks()
435+
by_type = {m.connection_type: m for m in pm.iter_connection_type_hook_ui_metadata()}
436+
assert "http" in by_type
437+
assert by_type["http"].field_behaviour == pm._field_behaviours["http"]
438+
431439
def test_ui_metadata_loading_without_hook_import(self):
432440
"""Test that UI metadata loads from provider info without importing hook classes."""
433441
with patch("airflow.providers_manager.import_string") as mock_import:

providers/airbyte/provider.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ triggers:
9797

9898
connection-types:
9999
- hook-class-name: airflow.providers.airbyte.hooks.airbyte.AirbyteHook
100+
hook-name: "Airbyte"
100101
connection-type: airbyte
101102
ui-field-behaviour:
102103
hidden-fields:

providers/airbyte/src/airflow/providers/airbyte/get_provider_info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def get_provider_info():
5050
"connection-types": [
5151
{
5252
"hook-class-name": "airflow.providers.airbyte.hooks.airbyte.AirbyteHook",
53+
"hook-name": "Airbyte",
5354
"connection-type": "airbyte",
5455
"ui-field-behaviour": {
5556
"hidden-fields": ["extra", "port"],

providers/alibaba/provider.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,13 @@ hooks:
123123

124124
connection-types:
125125
- hook-class-name: airflow.providers.alibaba.cloud.hooks.oss.OSSHook
126+
hook-name: "OSS"
126127
connection-type: oss
127128
- hook-class-name: airflow.providers.alibaba.cloud.hooks.analyticdb_spark.AnalyticDBSparkHook
129+
hook-name: "AnalyticDB Spark"
128130
connection-type: adb_spark
129131
- hook-class-name: airflow.providers.alibaba.cloud.hooks.base_alibaba.AlibabaBaseHook
132+
hook-name: "Alibaba Cloud"
130133
connection-type: alibaba_cloud
131134
conn-fields:
132135
access_key_id:
@@ -144,6 +147,7 @@ connection-types:
144147
- 'null'
145148
format: password
146149
- hook-class-name: airflow.providers.alibaba.cloud.hooks.maxcompute.MaxComputeHook
150+
hook-name: "MaxCompute"
147151
connection-type: maxcompute
148152
ui-field-behaviour:
149153
hidden-fields:

providers/alibaba/src/airflow/providers/alibaba/get_provider_info.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,17 @@ def get_provider_info():
9191
"connection-types": [
9292
{
9393
"hook-class-name": "airflow.providers.alibaba.cloud.hooks.oss.OSSHook",
94+
"hook-name": "OSS",
9495
"connection-type": "oss",
9596
},
9697
{
9798
"hook-class-name": "airflow.providers.alibaba.cloud.hooks.analyticdb_spark.AnalyticDBSparkHook",
99+
"hook-name": "AnalyticDB Spark",
98100
"connection-type": "adb_spark",
99101
},
100102
{
101103
"hook-class-name": "airflow.providers.alibaba.cloud.hooks.base_alibaba.AlibabaBaseHook",
104+
"hook-name": "Alibaba Cloud",
102105
"connection-type": "alibaba_cloud",
103106
"conn-fields": {
104107
"access_key_id": {
@@ -113,6 +116,7 @@ def get_provider_info():
113116
},
114117
{
115118
"hook-class-name": "airflow.providers.alibaba.cloud.hooks.maxcompute.MaxComputeHook",
119+
"hook-name": "MaxCompute",
116120
"connection-type": "maxcompute",
117121
"ui-field-behaviour": {
118122
"hidden-fields": ["host", "schema", "login", "password", "port", "extra"],

providers/amazon/provider.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ extra-links:
931931

932932
connection-types:
933933
- hook-class-name: airflow.providers.amazon.aws.hooks.base_aws.AwsGenericHook
934+
hook-name: "Amazon Web Services"
934935
connection-type: aws
935936
ui-field-behaviour:
936937
hidden-fields:
@@ -955,6 +956,7 @@ connection-types:
955956
"endpoint_url": "http://localhost:4566"
956957
}
957958
- hook-class-name: airflow.providers.amazon.aws.hooks.chime.ChimeWebhookHook
959+
hook-name: "Amazon Chime Webhook"
958960
connection-type: chime
959961
ui-field-behaviour:
960962
hidden-fields:
@@ -969,6 +971,7 @@ connection-types:
969971
host: hooks.chime.aws/incomingwebhook/
970972
password: T00000000?token=XXXXXXXXXXXXXXXXXXXXXXXX
971973
- hook-class-name: airflow.providers.amazon.aws.hooks.emr.EmrHook
974+
hook-name: "Amazon Elastic MapReduce"
972975
connection-type: emr
973976
ui-field-behaviour:
974977
hidden-fields:
@@ -999,12 +1002,14 @@ connection-types:
9991002
"StepConcurrencyLevel": 2
10001003
}
10011004
- hook-class-name: airflow.providers.amazon.aws.hooks.redshift_sql.RedshiftSQLHook
1005+
hook-name: "Amazon Redshift"
10021006
connection-type: redshift
10031007
ui-field-behaviour:
10041008
relabeling:
10051009
login: User
10061010
schema: Database
10071011
- hook-class-name: airflow.providers.amazon.aws.hooks.athena_sql.AthenaSQLHook
1012+
hook-name: "Amazon Athena"
10081013
connection-type: athena
10091014
ui-field-behaviour:
10101015
hidden-fields:

0 commit comments

Comments
 (0)