Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/core_api/services/ui/connections.py: 25%
123 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 14:22 +0000
1# Licensed to the Apache Software Foundation (ASF) under one
2# or more contributor license agreements. See the NOTICE file
3# distributed with this work for additional information
4# regarding copyright ownership. The ASF licenses this file
5# to you under the Apache License, Version 2.0 (the
6# "License"); you may not use this file except in compliance
7# with the License. You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing,
12# software distributed under the License is distributed on an
13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14# KIND, either express or implied. See the License for the
15# specific language governing permissions and limitations
16# under the License.
18from __future__ import annotations
20import logging
21from collections.abc import MutableMapping
22from functools import cache
23from typing import TYPE_CHECKING, Literal
25from airflow.api_fastapi.core_api.datamodels.connections import (
26 ConnectionHookFieldBehavior,
27 ConnectionHookMetaData,
28 StandardHookFields,
29)
30from airflow.providers_manager import HookInfo, ProvidersManager
31from airflow.serialization.definitions.param import SerializedParam
33if TYPE_CHECKING: 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
34 from airflow.providers_manager import ConnectionFormWidgetInfo
36log = logging.getLogger(__name__)
39class HookMetaService:
40 """Service for retrieving details about hooks to render UI."""
42 class MockOptional:
43 """Mock for wtforms.validators.Optional."""
45 def __init__(
46 self,
47 *args,
48 **kwargs,
49 ):
50 pass
52 def __call__(self, form, field):
53 """No-op call to satisfy WTForms validator protocol."""
54 return None
56 class MockEnum:
57 """Mock for wtforms.validators.AnyOf."""
59 def __init__(self, allowed_values):
60 self.allowed_values = allowed_values
62 class MockBaseField:
63 """Mock of WTForms Field."""
65 param_type: str = "UNDEFINED"
66 param_format: str | None = None
67 widget = None
69 def __init__(
70 self,
71 label: str | None = None,
72 validators=None,
73 description: str = "",
74 default: str | None = None,
75 widget=None,
76 source: Literal["dag", "task"] | None = None,
77 ):
78 type: str | list[str] = [self.param_type, "null"]
79 enum = {}
80 format = {"format": self.param_format} if self.param_format else {}
81 if validators:
82 if any(isinstance(v, HookMetaService.MockOptional) for v in validators):
83 type = [self.param_type, "null"]
84 for v in validators:
85 if isinstance(v, HookMetaService.MockEnum):
86 enum = {"enum": v.allowed_values}
87 self.param = SerializedParam(
88 default=default,
89 title=label,
90 description=description or None,
91 source=source or None,
92 type=type,
93 **format,
94 **enum,
95 )
96 self.widget = widget
97 self.field_class = self.__class__
99 class MockStringField(MockBaseField):
100 """Mock of WTForms StringField."""
102 param_type: str = "string"
104 class MockIntegerField(MockBaseField):
105 """Mock of WTForms IntegerField."""
107 param_type: str = "integer"
109 class MockPasswordField(MockBaseField):
110 """Mock of WTForms PasswordField."""
112 param_type: str = "string"
113 param_format: str | None = "password"
115 class MockBooleanField(MockBaseField):
116 """Mock of WTForms BooleanField."""
118 param_type: str = "boolean"
120 class MockAnyWidget:
121 """Mock any flask appbuilder widget."""
123 @staticmethod
124 def _get_hooks_with_mocked_fab() -> tuple[
125 MutableMapping[str, HookInfo | None], dict[str, ConnectionFormWidgetInfo], dict[str, dict]
126 ]:
127 """Get hooks with all details w/o FAB needing to be installed."""
128 from unittest import mock
130 def mock_lazy_gettext(txt: str) -> str:
131 """Mock for flask_babel.lazy_gettext."""
132 return txt
134 def mock_any_of(allowed_values: list) -> HookMetaService.MockEnum:
135 """Mock for wtforms.validators.any_of."""
136 return HookMetaService.MockEnum(allowed_values)
138 # Before importing ProvidersManager, we need to mock all FAB and WTForms
139 # dependencies to avoid ImportErrors when FAB is not installed.
140 import sys
141 from importlib.util import find_spec
142 from unittest.mock import MagicMock
144 for mod_name in [
145 "wtforms",
146 "wtforms.csrf",
147 "wtforms.fields",
148 "wtforms.fields.simple",
149 "wtforms.validators",
150 "flask_babel",
151 "flask_appbuilder",
152 "flask_appbuilder.fieldwidgets",
153 ]:
154 try:
155 if not find_spec(mod_name):
156 raise ModuleNotFoundError(f"No module named {mod_name!r}", name=mod_name)
157 except ModuleNotFoundError:
158 sys.modules[mod_name] = MagicMock()
160 # We conditionally inject mock classes for missing dependencies
161 # to ensure `ProvidersManager` can initialize hook connection widgets
162 # without crashing when FAB/WTForms are not installed.
163 if isinstance(sys.modules.get("wtforms"), MagicMock):
164 # Only apply mocks if the actual module wasn't loaded beforehand.
165 # This avoids thread-safety issues caused by `unittest.mock.patch` mutating global states.
166 with (
167 mock.patch("wtforms.StringField", HookMetaService.MockStringField),
168 mock.patch("wtforms.fields.StringField", HookMetaService.MockStringField),
169 mock.patch("wtforms.fields.simple.StringField", HookMetaService.MockStringField),
170 mock.patch("wtforms.IntegerField", HookMetaService.MockIntegerField),
171 mock.patch("wtforms.fields.IntegerField", HookMetaService.MockIntegerField),
172 mock.patch("wtforms.PasswordField", HookMetaService.MockPasswordField),
173 mock.patch("wtforms.BooleanField", HookMetaService.MockBooleanField),
174 mock.patch("wtforms.fields.BooleanField", HookMetaService.MockBooleanField),
175 mock.patch("wtforms.fields.simple.BooleanField", HookMetaService.MockBooleanField),
176 mock.patch("flask_babel.lazy_gettext", mock_lazy_gettext),
177 mock.patch("flask_appbuilder.fieldwidgets.BS3TextFieldWidget", HookMetaService.MockAnyWidget),
178 mock.patch(
179 "flask_appbuilder.fieldwidgets.BS3TextAreaFieldWidget", HookMetaService.MockAnyWidget
180 ),
181 mock.patch(
182 "flask_appbuilder.fieldwidgets.BS3PasswordFieldWidget", HookMetaService.MockAnyWidget
183 ),
184 mock.patch("wtforms.validators.Optional", HookMetaService.MockOptional),
185 mock.patch("wtforms.validators.any_of", mock_any_of),
186 # Prevent poisoning the global ProvidersManager singleton with mocks
187 mock.patch("airflow.providers_manager.ProvidersManager._instance", None),
188 mock.patch("airflow.providers_manager.ProvidersManager.initialized", return_value=False),
189 ):
190 pm = ProvidersManager()
191 return pm.hooks, pm.connection_form_widgets, pm.field_behaviours # Will init providers hooks
192 else:
193 pm = ProvidersManager()
194 return pm.hooks, pm.connection_form_widgets, pm.field_behaviours # Will init providers hooks
196 @staticmethod
197 def _make_standard_fields(field_behaviour: dict | None) -> StandardHookFields | None:
198 if not field_behaviour:
199 return None
201 def make_field(field_name: str, field_behaviour: dict) -> ConnectionHookFieldBehavior | None:
202 hidden_fields = field_behaviour.get("hidden_fields", [])
203 relabeling = field_behaviour.get("relabeling", {}).get(field_name)
204 placeholder = field_behaviour.get("placeholders", {}).get(field_name)
205 if any([field_name in hidden_fields, relabeling, placeholder]):
206 return ConnectionHookFieldBehavior(
207 hidden=field_name in hidden_fields,
208 title=relabeling,
209 placeholder=placeholder,
210 )
211 return None
213 return StandardHookFields(
214 description=make_field("description", field_behaviour),
215 url_schema=make_field("schema", field_behaviour),
216 host=make_field("host", field_behaviour),
217 port=make_field("port", field_behaviour),
218 login=make_field("login", field_behaviour),
219 password=make_field("password", field_behaviour),
220 )
222 @staticmethod
223 def _convert_extra_fields(form_widgets: dict[str, ConnectionFormWidgetInfo]) -> dict[str, MutableMapping]:
224 result: dict[str, MutableMapping] = {}
225 for key, form_widget in form_widgets.items():
226 hook_key = key.split("__")[1]
227 hook_widgets = result.get(hook_key, {})
229 if isinstance(form_widget.field, dict):
230 # yaml path, form widgets read from yaml and already present in SerializedParam.dump() format
231 hook_widgets[form_widget.field_name] = form_widget.field
232 elif isinstance(form_widget.field, HookMetaService.MockBaseField):
233 # legacy path, form widgets created using mocked WTForms fields, need to convert to SerializedParam.dump()
234 hook_widgets[form_widget.field_name] = form_widget.field.param.dump()
235 elif type(form_widget.field).__name__ == "UnboundField":
236 # handle real WTForms fields gracefully without needing mock patches
237 field_class_name = getattr(form_widget.field.field_class, "__name__", "")
238 param_type = "string"
239 param_format = None
240 if field_class_name == "BooleanField":
241 param_type = "boolean"
242 elif field_class_name == "IntegerField":
243 param_type = "integer"
244 elif field_class_name == "PasswordField":
245 param_format = "password"
247 label = (
248 form_widget.field.args[0]
249 if len(form_widget.field.args) > 0
250 else form_widget.field.kwargs.get("label")
251 )
252 validators = form_widget.field.kwargs.get("validators", [])
253 description = form_widget.field.kwargs.get("description", "")
254 default = form_widget.field.kwargs.get("default", None)
256 enum = {}
257 for v in validators:
258 if type(v).__name__ == "AnyOf":
259 enum["enum"] = getattr(v, "values", [])
261 types = [param_type, "null"]
262 format_dict = {"format": param_format} if param_format else {}
264 param = SerializedParam(
265 default=default,
266 title=str(label) if label is not None else None,
267 description=str(description) if description else None,
268 source=None,
269 type=types,
270 **format_dict,
271 **enum,
272 ).dump()
273 hook_widgets[form_widget.field_name] = param
274 else:
275 log.error("Unknown form widget in %s: %s", hook_key, form_widget)
276 continue
278 result[hook_key] = hook_widgets
279 return result
281 @staticmethod
282 @cache
283 def hook_meta_data() -> list[ConnectionHookMetaData]:
284 pm = ProvidersManager()
285 widgets = HookMetaService._convert_extra_fields(pm._connection_form_widgets_from_metadata)
286 return [
287 ConnectionHookMetaData(
288 connection_type=meta.connection_type,
289 hook_class_name=meta.hook_class_name,
290 default_conn_name=None,
291 hook_name=meta.hook_name,
292 standard_fields=HookMetaService._make_standard_fields(meta.field_behaviour),
293 extra_fields=widgets.get(meta.connection_type),
294 )
295 for meta in pm.iter_connection_type_hook_ui_metadata()
296 ]