Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/airflow/_shared/configuration/parser.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

908 statements  

1# 

2# Licensed to the Apache Software Foundation (ASF) under one 

3# or more contributor license agreements. See the NOTICE file 

4# distributed with this work for additional information 

5# regarding copyright ownership. The ASF licenses this file 

6# to you under the Apache License, Version 2.0 (the 

7# "License"); you may not use this file except in compliance 

8# with the License. You may obtain a copy of the License at 

9# 

10# http://www.apache.org/licenses/LICENSE-2.0 

11# 

12# Unless required by applicable law or agreed to in writing, 

13# software distributed under the License is distributed on an 

14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 

15# KIND, either express or implied. See the License for the 

16# specific language governing permissions and limitations 

17# under the License. 

18"""Base configuration parser with pure parsing logic.""" 

19 

20from __future__ import annotations 

21 

22import contextlib 

23import datetime 

24import functools 

25import itertools 

26import json 

27import logging 

28import os 

29import shlex 

30import subprocess 

31import sys 

32import warnings 

33from collections.abc import Callable, Generator, Iterable 

34from configparser import ConfigParser, NoOptionError, NoSectionError 

35from contextlib import contextmanager 

36from copy import deepcopy 

37from enum import Enum 

38from json.decoder import JSONDecodeError 

39from re import Pattern 

40from typing import IO, TYPE_CHECKING, Any, TypeVar, overload 

41 

42from .exceptions import AirflowConfigException 

43 

44log = logging.getLogger(__name__) 

45 

46 

47def _build_kwarg_env_prefix(section: str, kwargs_key: str) -> str: 

48 """ 

49 Build env prefix for per-key backend kwargs. 

50 

51 ("secrets", "backend_kwargs") -> "AIRFLOW__SECRETS__BACKEND_KWARG__" 

52 ("workers", "secrets_backend_kwargs") -> "AIRFLOW__WORKERS__SECRETS_BACKEND_KWARG__" 

53 """ 

54 singular_key = kwargs_key.replace("_kwargs", "_kwarg") 

55 return f"{ENV_VAR_PREFIX}{section.upper()}__{singular_key.upper()}__" 

56 

57 

58def _collect_kwarg_env_vars(prefix: str) -> dict[str, str]: 

59 """ 

60 Scan os.environ for per-key secrets backend kwargs. 

61 

62 AIRFLOW__SECRETS__BACKEND_KWARG__ROLE_ID -> {"role_id": value} 

63 Values are raw strings (not JSON-parsed). 

64 Empty keys (trailing __ with no suffix) are ignored. 

65 """ 

66 overrides: dict[str, str] = {} 

67 for env_var, value in os.environ.items(): 

68 if env_var.startswith(prefix): 

69 kwarg_key = env_var[len(prefix) :].lower() 

70 if kwarg_key: 

71 overrides[kwarg_key] = value 

72 return overrides 

73 

74 

75ConfigType = str | int | float | bool 

76ConfigOptionsDictType = dict[str, ConfigType] 

77ConfigSectionSourcesType = dict[str, str | tuple[str, str]] 

78ConfigSourcesType = dict[str, ConfigSectionSourcesType] 

79ENV_VAR_PREFIX = "AIRFLOW__" 

80# Separates the team name from the base section name in a team scoped config file section. 

81TEAM_SECTION_SEPARATOR = "=" 

82 

83 

84def team_section_name(team_name: str, section: str) -> str: 

85 """ 

86 Build the config file section name that holds the team scoped overrides of ``section``. 

87 

88 :param team_name: name of the team the overrides belong to 

89 :param section: base section name that is being overridden 

90 :return: the team scoped section name, e.g. ``team_a=celery`` 

91 """ 

92 return f"{team_name}{TEAM_SECTION_SEPARATOR}{section}" 

93 

94 

95def base_section_name(section: str) -> str: 

96 """ 

97 Return the base section name of a possibly team scoped config file section. 

98 

99 Team scoped sections are built by :func:`team_section_name`. Base section names never contain 

100 the separator, so the name is split on the last one - that way the base section is recovered 

101 even for a team name that contains the separator itself. 

102 

103 :param section: section name, either a base one or a team scoped one 

104 :return: the base section name, which is ``section`` itself when it is not team scoped 

105 """ 

106 _, separator, base_section = section.rpartition(TEAM_SECTION_SEPARATOR) 

107 return base_section if separator else section 

108 

109 

110if TYPE_CHECKING: 

111 from airflow.providers_manager import ProvidersManager 

112 from airflow.sdk.providers_manager_runtime import ProvidersManagerTaskRuntime 

113 

114 

115class ValueNotFound: 

116 """Object of this is raised when a configuration value cannot be found.""" 

117 

118 pass 

119 

120 

121VALUE_NOT_FOUND_SENTINEL = ValueNotFound() 

122 

123 

124@overload 

125def expand_env_var(env_var: None) -> None: ... 

126@overload 

127def expand_env_var(env_var: str) -> str: ... 

128 

129 

130def expand_env_var(env_var: str | None) -> str | None: 

131 """ 

132 Expand (potentially nested) env vars. 

133 

134 Repeat and apply `expandvars` and `expanduser` until 

135 interpolation stops having any effect. 

136 """ 

137 if not env_var or not isinstance(env_var, str): 

138 return env_var 

139 while True: 

140 interpolated = os.path.expanduser(os.path.expandvars(str(env_var))) 

141 if interpolated == env_var: 

142 return interpolated 

143 env_var = interpolated 

144 

145 

146def run_command(command: str) -> str: 

147 """Run command and returns stdout.""" 

148 process = subprocess.Popen( 

149 shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True 

150 ) 

151 output, stderr = (stream.decode(sys.getdefaultencoding(), "ignore") for stream in process.communicate()) 

152 

153 if process.returncode != 0: 

154 raise AirflowConfigException( 

155 f"Cannot execute {command}. Error code is: {process.returncode}. " 

156 f"Output: {output}, Stderr: {stderr}" 

157 ) 

158 

159 return output 

160 

161 

162def _is_template(configuration_description: dict[str, dict[str, Any]], section: str, key: str) -> bool: 

163 """ 

164 Check if the config is a template. 

165 

166 :param configuration_description: description of configuration 

167 :param section: section 

168 :param key: key 

169 :return: True if the config is a template 

170 """ 

171 return configuration_description.get(section, {}).get(key, {}).get("is_template", False) 

172 

173 

174def configure_parser_from_configuration_description( 

175 parser: ConfigParser, 

176 configuration_description: dict[str, dict[str, Any]], 

177 all_vars: dict[str, Any], 

178) -> None: 

179 """ 

180 Configure a ConfigParser based on configuration description. 

181 

182 :param parser: ConfigParser to configure 

183 :param configuration_description: configuration description from config.yml 

184 """ 

185 for section, section_desc in configuration_description.items(): 

186 parser.add_section(section) 

187 options = section_desc["options"] 

188 for key in options: 

189 default_value = options[key]["default"] 

190 is_template = options[key].get("is_template", False) 

191 if (default_value is not None) and not ( 

192 options[key].get("version_deprecated") or options[key].get("deprecation_reason") 

193 ): 

194 if is_template or not isinstance(default_value, str): 

195 parser.set(section, key, str(default_value)) 

196 else: 

197 try: 

198 parser.set(section, key, default_value.format(**all_vars)) 

199 except (KeyError, ValueError): 

200 parser.set(section, key, default_value) 

201 

202 

203def create_provider_cfg_config_fallback_defaults( 

204 provider_config_fallback_defaults_cfg_path: str, 

205) -> ConfigParser: 

206 """ 

207 Create fallback defaults for configuration. 

208 

209 This parser contains provider defaults for Airflow configuration, containing fallback default values 

210 that might be needed when provider classes are being imported - before provider's configuration 

211 is loaded. 

212 

213 Unfortunately airflow currently performs a lot of stuff during importing and some of that might lead 

214 to retrieving provider configuration before the defaults for the provider are loaded. 

215 

216 Those are only defaults, so if you have "real" values configured in your configuration (.cfg file or 

217 environment variables) those will be used as usual. 

218 

219 NOTE!! Do NOT attempt to remove those default fallbacks thinking that they are unnecessary duplication, 

220 at least not until we fix the way how airflow imports "do stuff". This is unlikely to succeed. 

221 

222 You've been warned! 

223 

224 :param provider_config_fallback_defaults_cfg_path: path to the provider config fallback defaults .cfg file 

225 """ 

226 config_parser = ConfigParser() 

227 config_parser.read(provider_config_fallback_defaults_cfg_path) 

228 return config_parser 

229 

230 

231class AirflowConfigParser(ConfigParser): 

232 """ 

233 Base configuration parser with pure parsing logic. 

234 

235 This class provides the core parsing methods that work with: 

236 - configuration_description: dict describing config options (required in __init__) 

237 - _default_values: ConfigParser with default values (required in __init__) 

238 - deprecated_options: class attribute mapping new -> old options 

239 - deprecated_sections: class attribute mapping new -> old sections 

240 """ 

241 

242 # A mapping of section -> setting -> { old, replace } for deprecated default values. 

243 # Subclasses can override this to define deprecated values that should be upgraded. 

244 deprecated_values: dict[str, dict[str, tuple[Pattern, str]]] = {} 

245 

246 # A mapping of (new section, new option) -> (old section, old option, since_version). 

247 # When reading new option, the old option will be checked to see if it exists. If it does a 

248 # DeprecationWarning will be issued and the old option will be used instead 

249 deprecated_options: dict[tuple[str, str], tuple[str, str, str]] = { 

250 ("dag_processor", "dag_file_processor_timeout"): ("core", "dag_file_processor_timeout", "3.0"), 

251 ("dag_processor", "refresh_interval"): ("scheduler", "dag_dir_list_interval", "3.0"), 

252 ("api", "base_url"): ("webserver", "base_url", "3.0"), 

253 ("api", "host"): ("webserver", "web_server_host", "3.0"), 

254 ("api", "port"): ("webserver", "web_server_port", "3.0"), 

255 ("api", "workers"): ("webserver", "workers", "3.0"), 

256 ("api", "worker_timeout"): ("webserver", "web_server_worker_timeout", "3.0"), 

257 ("api", "ssl_cert"): ("webserver", "web_server_ssl_cert", "3.0"), 

258 ("api", "ssl_key"): ("webserver", "web_server_ssl_key", "3.0"), 

259 ("api", "access_logfile"): ("webserver", "access_logfile", "3.0"), 

260 ("triggerer", "capacity"): ("triggerer", "default_capacity", "3.0"), 

261 ("api", "expose_config"): ("webserver", "expose_config", "3.0.1"), 

262 ("fab", "access_denied_message"): ("webserver", "access_denied_message", "3.0.2"), 

263 ("fab", "expose_hostname"): ("webserver", "expose_hostname", "3.0.2"), 

264 ("fab", "navbar_color"): ("webserver", "navbar_color", "3.0.2"), 

265 ("fab", "navbar_text_color"): ("webserver", "navbar_text_color", "3.0.2"), 

266 ("fab", "navbar_hover_color"): ("webserver", "navbar_hover_color", "3.0.2"), 

267 ("fab", "navbar_text_hover_color"): ("webserver", "navbar_text_hover_color", "3.0.2"), 

268 ("api", "secret_key"): ("webserver", "secret_key", "3.0.2"), 

269 ("api", "enable_swagger_ui"): ("webserver", "enable_swagger_ui", "3.0.2"), 

270 ("dag_processor", "parsing_pre_import_modules"): ("scheduler", "parsing_pre_import_modules", "3.0.4"), 

271 ("api", "grid_view_sorting_order"): ("webserver", "grid_view_sorting_order", "3.1.0"), 

272 ("api", "log_fetch_timeout_sec"): ("webserver", "log_fetch_timeout_sec", "3.1.0"), 

273 ("api", "hide_paused_dags_by_default"): ("webserver", "hide_paused_dags_by_default", "3.1.0"), 

274 ("core", "num_dag_runs_to_retain_rendered_fields"): ( 

275 "core", 

276 "max_num_rendered_ti_fields_per_task", 

277 "3.2.0", 

278 ), 

279 ("api", "page_size"): ("webserver", "page_size", "3.1.0"), 

280 ("api", "default_wrap"): ("webserver", "default_wrap", "3.1.0"), 

281 ("api", "auto_refresh_interval"): ("webserver", "auto_refresh_interval", "3.1.0"), 

282 ("api", "require_confirmation_dag_change"): ("webserver", "require_confirmation_dag_change", "3.1.0"), 

283 ("api", "instance_name"): ("webserver", "instance_name", "3.1.0"), 

284 ("api", "log_config"): ("api", "access_logfile", "3.1.0"), 

285 ("scheduler", "ti_metrics_interval"): ("scheduler", "running_metrics_interval", "3.2.0"), 

286 ("api", "fallback_page_limit"): ("api", "page_size", "3.2.0"), 

287 ("workers", "missing_dag_retries"): ("workers", "missing_dag_retires", "3.1.8"), 

288 ("core", "execution_api_server_url"): ("workers", "execution_api_server_url", "3.0"), 

289 ("database", "sql_alchemy_conn"): ("core", "sql_alchemy_conn", "3.0"), 

290 } 

291 

292 # A mapping of new section -> (old section, since_version). 

293 deprecated_sections: dict[str, tuple[str, str]] = {} 

294 

295 @property 

296 def _lookup_sequence(self) -> list[Callable]: 

297 """ 

298 Define the sequence of lookup methods for get(). The definition here does not have provider lookup. 

299 

300 Subclasses can override this to customise lookup order. 

301 """ 

302 lookup_methods = [ 

303 self._get_environment_variables, 

304 self._get_option_from_config_file, 

305 self._get_option_from_commands, 

306 self._get_option_from_secrets, 

307 self._get_option_from_defaults, 

308 ] 

309 if self._use_providers_configuration: 

310 # Provider fallback lookups are last so they have the lowest priority in the lookup sequence. 

311 lookup_methods += [ 

312 self._get_option_from_provider_metadata_config_fallbacks, 

313 self._get_option_from_provider_cfg_config_fallbacks, 

314 ] 

315 return lookup_methods 

316 

317 @functools.cached_property 

318 def configuration_description(self) -> dict[str, dict[str, Any]]: 

319 """ 

320 Return configuration description from multiple sources. 

321 

322 Respects the ``_use_providers_configuration`` flag to decide whether to include 

323 provider configuration. 

324 

325 The merged description is built as follows: 

326 

327 1. Start from the base configuration description provided in ``__init__``, usually 

328 loaded from ``config.yml`` in core. Values defined here are never overridden. 

329 2. Merge provider metadata from ``_provider_metadata_configuration_description``, 

330 loaded from provider packages' ``get_provider_info`` method. Only adds missing 

331 sections/options; does not overwrite existing entries from the base configuration. 

332 3. Merge default values from ``_provider_cfg_config_fallback_default_values``, 

333 loaded from ``provider_config_fallback_defaults.cfg``. Only sets ``"default"`` 

334 (and heuristically ``"sensitive"``) for options that do not already define them. 

335 

336 Base configuration takes precedence, then provider metadata fills in missing 

337 descriptions/options, and finally cfg-based fallbacks provide defaults only where 

338 none are defined. 

339 

340 We use ``cached_property`` to cache the merged result; clear this cache (via 

341 ``invalidate_cache``) when toggling ``_use_providers_configuration``. 

342 """ 

343 if not self._use_providers_configuration: 

344 return self._configuration_description 

345 

346 merged_description: dict[str, dict[str, Any]] = deepcopy(self._configuration_description) 

347 

348 # Merge full provider config descriptions (with metadata like sensitive, description, etc.) 

349 # from provider packages' get_provider_info method, reusing the cached raw dict. 

350 for section, section_content in self._provider_metadata_configuration_description.items(): 

351 if section not in merged_description: 

352 merged_description[section] = deepcopy(section_content) 

353 else: 

354 existing_options = merged_description[section].setdefault("options", {}) 

355 for option, option_content in section_content.get("options", {}).items(): 

356 if option not in existing_options: 

357 existing_options[option] = deepcopy(option_content) 

358 

359 # Merge default values from cfg-based fallbacks (key=value only, no metadata). 

360 # Uses setdefault so provider metadata values above take priority. 

361 cfg = self._provider_cfg_config_fallback_default_values 

362 for section in cfg.sections(): 

363 section_options = merged_description.setdefault(section, {"options": {}}).setdefault( 

364 "options", {} 

365 ) 

366 for option in cfg.options(section): 

367 opt_dict = section_options.setdefault(option, {}) 

368 opt_dict.setdefault("default", cfg.get(section, option)) 

369 # For cfg-only options with no provider metadata, infer sensitivity from name. 

370 if "sensitive" not in opt_dict and option.endswith(("password", "secret")): 

371 opt_dict["sensitive"] = True 

372 

373 return merged_description 

374 

375 @property 

376 def _config_sources_for_as_dict(self) -> list[tuple[str, ConfigParser]]: 

377 """Override the base method to add provider fallbacks when providers are loaded.""" 

378 sources: list[tuple[str, ConfigParser]] = [] 

379 if self._use_providers_configuration: 

380 # Provider fallback defaults are listed first so they have the lowest priority 

381 # in as_dict()'s "last source wins" semantics. 

382 sources += [ 

383 ("provider-cfg-fallback-defaults", self._provider_cfg_config_fallback_default_values), 

384 ( 

385 "provider-metadata-fallback-defaults", 

386 self._provider_metadata_config_fallback_default_values, 

387 ), 

388 ] 

389 sources += [ 

390 ("default", self._default_values), 

391 ("airflow.cfg", self), 

392 ] 

393 return sources 

394 

395 def _get_option_from_provider_cfg_config_fallbacks( 

396 self, 

397 deprecated_key: str | None, 

398 deprecated_section: str | None, 

399 key: str, 

400 section: str, 

401 issue_warning: bool = True, 

402 extra_stacklevel: int = 0, 

403 **kwargs, 

404 ) -> str | ValueNotFound: 

405 """Get config option from provider fallback defaults.""" 

406 value = self.get_from_provider_cfg_config_fallback_defaults(section, key, **kwargs) 

407 if value is not VALUE_NOT_FOUND_SENTINEL: 

408 return value 

409 return VALUE_NOT_FOUND_SENTINEL 

410 

411 def _get_option_from_provider_metadata_config_fallbacks( 

412 self, 

413 deprecated_key: str | None, 

414 deprecated_section: str | None, 

415 key: str, 

416 section: str, 

417 issue_warning: bool = True, 

418 extra_stacklevel: int = 0, 

419 **kwargs, 

420 ) -> str | ValueNotFound: 

421 """Get config option from provider metadata fallback defaults.""" 

422 value = self.get_from_provider_metadata_config_fallback_defaults(section, key, **kwargs) 

423 if value is not VALUE_NOT_FOUND_SENTINEL: 

424 return value 

425 return VALUE_NOT_FOUND_SENTINEL 

426 

427 def get_from_provider_cfg_config_fallback_defaults(self, section: str, key: str, **kwargs) -> Any: 

428 """Get provider config fallback default values.""" 

429 raw = kwargs.get("raw", False) 

430 vars_ = kwargs.get("vars") 

431 return self._provider_cfg_config_fallback_default_values.get( 

432 section, key, fallback=VALUE_NOT_FOUND_SENTINEL, raw=raw, vars=vars_ 

433 ) 

434 

435 @functools.cached_property 

436 def _provider_metadata_configuration_description(self) -> dict[str, dict[str, Any]]: 

437 """Raw provider configuration descriptions with full metadata (sensitive, description, etc.).""" 

438 result: dict[str, dict[str, Any]] = {} 

439 for _, config in self._provider_manager_type().provider_configs: 

440 result.update(config) 

441 return result 

442 

443 @functools.cached_property 

444 def _provider_metadata_config_fallback_default_values(self) -> ConfigParser: 

445 """Return Provider metadata config fallback default values.""" 

446 return self._create_default_config_parser_callable(self._provider_metadata_configuration_description) 

447 

448 def get_from_provider_metadata_config_fallback_defaults(self, section: str, key: str, **kwargs) -> Any: 

449 """Get provider metadata config fallback default values.""" 

450 raw = kwargs.get("raw", False) 

451 vars_ = kwargs.get("vars") 

452 return self._provider_metadata_config_fallback_default_values.get( 

453 section, key, fallback=VALUE_NOT_FOUND_SENTINEL, raw=raw, vars=vars_ 

454 ) 

455 

456 @property 

457 def _validators(self) -> list[Callable[[], None]]: 

458 """ 

459 Return list of validators defined on a config parser class. Base class will return an empty list. 

460 

461 Subclasses can override this to customize the validators that are run during validation on the 

462 config parser instance. 

463 """ 

464 return [] 

465 

466 def validate(self) -> None: 

467 """Run all registered validators.""" 

468 for validator in self._validators: 

469 validator() 

470 self.is_validated = True 

471 

472 def _validate_deprecated_values(self) -> None: 

473 """Validate and upgrade deprecated default values.""" 

474 for section, replacement in self.deprecated_values.items(): 

475 for name, info in replacement.items(): 

476 old, new = info 

477 current_value = self.get(section, name, fallback="") 

478 if self._using_old_value(old, current_value): 

479 self.upgraded_values[(section, name)] = current_value 

480 new_value = old.sub(new, current_value) 

481 self._update_env_var(section=section, name=name, new_value=new_value) 

482 self._create_future_warning( 

483 name=name, 

484 section=section, 

485 current_value=current_value, 

486 new_value=new_value, 

487 ) 

488 

489 def _using_old_value(self, old: Pattern, current_value: str) -> bool: 

490 """Check if current_value matches the old pattern.""" 

491 return old.search(current_value) is not None 

492 

493 def _update_env_var(self, section: str, name: str, new_value: str) -> None: 

494 """Update environment variable with new value.""" 

495 env_var = self._env_var_name(section, name) 

496 # Set it as an env var so that any subprocesses keep the same override! 

497 os.environ[env_var] = new_value 

498 

499 @staticmethod 

500 def _create_future_warning(name: str, section: str, current_value: Any, new_value: Any) -> None: 

501 """Create a FutureWarning for deprecated default values.""" 

502 warnings.warn( 

503 f"The {name!r} setting in [{section}] has the old default value of {current_value!r}. " 

504 f"This value has been changed to {new_value!r} in the running config, but please update your config.", 

505 FutureWarning, 

506 stacklevel=3, 

507 ) 

508 

509 def __init__( 

510 self, 

511 configuration_description: dict[str, dict[str, Any]], 

512 _default_values: ConfigParser, 

513 provider_manager_type: type[ProvidersManager] | type[ProvidersManagerTaskRuntime], 

514 create_default_config_parser_callable: Callable[[dict[str, dict[str, Any]]], ConfigParser], 

515 provider_config_fallback_defaults_cfg_path: str, 

516 *args, 

517 **kwargs, 

518 ): 

519 """ 

520 Initialize the parser. 

521 

522 :param configuration_description: Description of configuration options 

523 :param _default_values: ConfigParser with default values 

524 :param provider_manager_type: Either ProvidersManager or ProvidersManagerTaskRuntime, depending on the context of the caller. 

525 :param create_default_config_parser_callable: The `create_default_config_parser` function from core or SDK, depending on the context of the caller. 

526 :param provider_config_fallback_defaults_cfg_path: Path to the `provider_config_fallback_defaults.cfg` file. 

527 """ 

528 super().__init__(*args, **kwargs) 

529 self._configuration_description = configuration_description 

530 self._default_values = _default_values 

531 self._provider_manager_type = provider_manager_type 

532 self._create_default_config_parser_callable = create_default_config_parser_callable 

533 self._provider_cfg_config_fallback_default_values = create_provider_cfg_config_fallback_defaults( 

534 provider_config_fallback_defaults_cfg_path 

535 ) 

536 self._suppress_future_warnings = False 

537 self.upgraded_values: dict[tuple[str, str], str] = {} 

538 # The _use_providers_configuration flag will always be True unless we call `write(include_providers=False)` or `with self.make_sure_configuration_loaded(with_providers=False)`. 

539 # Even when we call those methods, the flag will be set back to True after the method is done, so it only affects the current call to `as_dict()` and does not have any effect on subsequent calls. 

540 self._use_providers_configuration = True 

541 

542 def invalidate_cache(self) -> None: 

543 """ 

544 Clear all ``functools.cached_property`` entries on this instance. 

545 

546 Call this after mutating class-level attributes (e.g. ``deprecated_options``) 

547 so that derived cached properties are recomputed on next access. 

548 """ 

549 for attr_name in ( 

550 name 

551 for name in dir(type(self)) 

552 if isinstance(getattr(type(self), name, None), functools.cached_property) 

553 ): 

554 self.__dict__.pop(attr_name, None) 

555 

556 def _invalidate_provider_flag_caches(self) -> None: 

557 """Invalidate caches related to provider configuration flags.""" 

558 self.__dict__.pop("configuration_description", None) 

559 self.__dict__.pop("sensitive_config_values", None) 

560 

561 @functools.cached_property 

562 def inversed_deprecated_options(self): 

563 """Build inverse mapping from old options to new options.""" 

564 return {(sec, name): key for key, (sec, name, ver) in self.deprecated_options.items()} 

565 

566 @functools.cached_property 

567 def inversed_deprecated_sections(self): 

568 """Build inverse mapping from old sections to new sections.""" 

569 return { 

570 old_section: new_section for new_section, (old_section, ver) in self.deprecated_sections.items() 

571 } 

572 

573 @functools.cached_property 

574 def sensitive_config_values(self) -> set[tuple[str, str]]: 

575 """Get set of sensitive config values that should be masked.""" 

576 flattened = { 

577 (s, k): item 

578 for s, s_c in self.configuration_description.items() 

579 for k, item in s_c.get("options", {}).items() 

580 } 

581 sensitive = { 

582 (section.lower(), key.lower()) 

583 for (section, key), v in flattened.items() 

584 if v.get("sensitive") is True 

585 } 

586 depr_option = {self.deprecated_options[x][:-1] for x in sensitive if x in self.deprecated_options} 

587 depr_section = { 

588 (self.deprecated_sections[s][0], k) for s, k in sensitive if s in self.deprecated_sections 

589 } 

590 sensitive.update(depr_section, depr_option) 

591 return sensitive 

592 

593 def _names_sensitive_team_env_var(self, env_var: str) -> bool: 

594 """ 

595 Check whether an environment variable name is a team scoped override of a sensitive option. 

596 

597 Team scoped variables are named ``AIRFLOW__<TEAM>___<SECTION>__<KEY>`` (see 

598 :meth:`_env_var_name`). A team name may contain underscores itself, so the team name is not 

599 parsed out of the variable name; the name is matched against the tail that each option 

600 registered as sensitive contributes instead. The ``_CMD`` / ``_SECRET`` fallbacks are 

601 matched too, because - unlike for a base section - they are never resolved into their value. 

602 

603 :param env_var: environment variable name 

604 :return: True if the variable holds a team scoped value of an option registered as sensitive 

605 """ 

606 env_var = env_var.upper() 

607 if not env_var.startswith(ENV_VAR_PREFIX): 

608 return False 

609 # Every tail matched below starts with the ``___`` separating the team name from the 

610 # section, so a name not containing it cannot be a team scoped one. 

611 if "___" not in env_var: 

612 return False 

613 for section, key in self.sensitive_config_values: 

614 option_tail = self._env_var_name(section, key).removeprefix(ENV_VAR_PREFIX) 

615 for tail in (f"___{option_tail}", f"___{option_tail}_CMD", f"___{option_tail}_SECRET"): 

616 # The team name sits between the prefix and the tail, so it must not be empty. 

617 if env_var.endswith(tail) and len(env_var) > len(ENV_VAR_PREFIX) + len(tail): 

618 return True 

619 return False 

620 

621 def is_sensitive_option(self, section: str, key: str) -> bool: 

622 """ 

623 Check whether the value of ``key`` in ``section`` is registered as sensitive. 

624 

625 Options are registered as sensitive under their base section name, while a team scoped 

626 override of the very same option is held by a ``<team name>=<section>`` config file section 

627 or by an ``AIRFLOW__<TEAM>___<SECTION>__<KEY>`` environment variable. Both of those 

628 spellings are resolved back to the base option here, so that a team scoped value is treated 

629 exactly like the base one. A name that does not resolve to a registered option is not 

630 sensitive - so this only ever recognises more options as sensitive, never fewer. 

631 

632 :param section: section name, either a base one or a team scoped one 

633 :param key: option name 

634 :return: True if the value of the option should be treated as sensitive 

635 """ 

636 section = section.lower() 

637 key = key.lower() 

638 if (section, key) in self.sensitive_config_values: 

639 return True 

640 base_section = base_section_name(section) 

641 if base_section != section: 

642 if (base_section, key) in self.sensitive_config_values: 

643 return True 

644 # A team scoped ``_cmd`` / ``_secret`` fallback is not resolved into its value, so it 

645 # stays in the output as configured and has to be recognised on its own. 

646 for fallback_suffix in ("_cmd", "_secret"): 

647 if not key.endswith(fallback_suffix): 

648 continue 

649 if (base_section, key.removesuffix(fallback_suffix)) in self.sensitive_config_values: 

650 return True 

651 # A team scoped environment variable is reported under the section and key its name splits 

652 # into, which is neither the base nor the team scoped section name. 

653 return self._names_sensitive_team_env_var(self._env_var_name(section, key)) 

654 

655 def _update_defaults_from_string(self, config_string: str) -> None: 

656 """ 

657 Update the defaults in _default_values based on values in config_string ("ini" format). 

658 

659 Override shared parser's method to add validation for template variables. 

660 Note that those values are not validated and cannot contain variables because we are using 

661 regular config parser to load them. This method is used to test the config parser in unit tests. 

662 

663 :param config_string: ini-formatted config string 

664 """ 

665 parser = ConfigParser() 

666 parser.read_string(config_string) 

667 for section in parser.sections(): 

668 if section not in self._default_values.sections(): 

669 self._default_values.add_section(section) 

670 errors = False 

671 for key, value in parser.items(section): 

672 if not self.is_template(section, key) and "{" in value: 

673 errors = True 

674 log.error( 

675 "The %s.%s value %s read from string contains variable. This is not supported", 

676 section, 

677 key, 

678 value, 

679 ) 

680 self._default_values.set(section, key, value) 

681 if errors: 

682 raise AirflowConfigException( 

683 f"The string config passed as default contains variables. " 

684 f"This is not supported. String config: {config_string}" 

685 ) 

686 

687 def get_default_value(self, section: str, key: str, fallback: Any = None, raw=False, **kwargs) -> Any: 

688 """ 

689 Retrieve default value from default config parser, including provider fallbacks. 

690 

691 This will retrieve the default value from the core default config parser first. If not found 

692 and providers configuration is loaded, it also checks provider fallback defaults. 

693 Optionally a raw, stored value can be retrieved by setting skip_interpolation to True. 

694 This is useful for example when we want to write the default value to a file, and we don't 

695 want the interpolation to happen as it is going to be done later when the config is read. 

696 

697 :param section: section of the config 

698 :param key: key to use 

699 :param fallback: fallback value to use 

700 :param raw: if raw, then interpolation will be reversed 

701 :param kwargs: other args 

702 :return: 

703 """ 

704 value = self._default_values.get(section, key, fallback=VALUE_NOT_FOUND_SENTINEL, **kwargs) 

705 # Provider metadata has higher priority than cfg fallback — check it first. 

706 if value is VALUE_NOT_FOUND_SENTINEL and self._use_providers_configuration: 

707 value = self._provider_metadata_config_fallback_default_values.get( 

708 section, key, fallback=VALUE_NOT_FOUND_SENTINEL, **kwargs 

709 ) 

710 if value is VALUE_NOT_FOUND_SENTINEL and self._use_providers_configuration: 

711 value = self._provider_cfg_config_fallback_default_values.get( 

712 section, key, fallback=VALUE_NOT_FOUND_SENTINEL, **kwargs 

713 ) 

714 if value is VALUE_NOT_FOUND_SENTINEL: 

715 value = fallback 

716 if raw and isinstance(value, str): 

717 return value.replace("%", "%%") 

718 return value 

719 

720 def _get_custom_secret_backend(self, worker_mode: bool = False) -> Any | None: 

721 """ 

722 Get Secret Backend if defined in airflow.cfg. 

723 

724 Conditionally selects the section, key and kwargs key based on whether it is called from worker or not. 

725 """ 

726 section = "workers" if worker_mode else "secrets" 

727 key = "secrets_backend" if worker_mode else "backend" 

728 kwargs_key = "secrets_backend_kwargs" if worker_mode else "backend_kwargs" 

729 

730 secrets_backend_cls = self.getimport(section=section, key=key) 

731 

732 if not secrets_backend_cls: 

733 if worker_mode: 

734 # if we find no secrets backend for worker, return that of secrets backend 

735 secrets_backend_cls = self.getimport(section="secrets", key="backend") 

736 if not secrets_backend_cls: 

737 return None 

738 # When falling back to secrets backend, use its kwargs 

739 kwargs_key = "backend_kwargs" 

740 section = "secrets" 

741 else: 

742 return None 

743 

744 try: 

745 backend_kwargs = self.getjson(section=section, key=kwargs_key) 

746 if not backend_kwargs: 

747 backend_kwargs = {} 

748 elif not isinstance(backend_kwargs, dict): 

749 raise ValueError("not a dict") 

750 except AirflowConfigException: 

751 log.warning("Failed to parse [%s] %s as JSON, defaulting to no kwargs.", section, kwargs_key) 

752 backend_kwargs = {} 

753 except ValueError: 

754 log.warning("Failed to parse [%s] %s into a dict, defaulting to no kwargs.", section, kwargs_key) 

755 backend_kwargs = {} 

756 

757 # Collect per-key overrides; they take precedence over the JSON blob. 

758 env_prefix = _build_kwarg_env_prefix(section, kwargs_key) 

759 backend_kwargs.update(_collect_kwarg_env_vars(env_prefix)) 

760 

761 return secrets_backend_cls(**backend_kwargs) 

762 

763 def _get_config_value_from_secret_backend(self, config_key: str) -> str | None: 

764 """ 

765 Get Config option values from Secret Backend. 

766 

767 Called by the shared parser's _get_secret_option() method as part of the lookup chain. 

768 Uses _get_custom_secret_backend() to get the backend instance. 

769 

770 :param config_key: the config key to retrieve 

771 :return: config value or None 

772 """ 

773 try: 

774 secrets_client = self._get_custom_secret_backend() 

775 if not secrets_client: 

776 return None 

777 return secrets_client.get_config(config_key) 

778 except Exception as e: 

779 raise AirflowConfigException( 

780 "Cannot retrieve config from alternative secrets backend. " 

781 "Make sure it is configured properly and that the Backend " 

782 "is accessible.\n" 

783 f"{e}" 

784 ) 

785 

786 def _get_cmd_option_from_config_sources( 

787 self, config_sources: ConfigSourcesType, section: str, key: str 

788 ) -> str | None: 

789 fallback_key = key + "_cmd" 

790 if (section, key) in self.sensitive_config_values: 

791 section_dict = config_sources.get(section) 

792 if section_dict is not None: 

793 command_value = section_dict.get(fallback_key) 

794 if command_value is not None: 

795 if isinstance(command_value, str): 

796 command = command_value 

797 else: 

798 command = command_value[0] 

799 return run_command(command) 

800 return None 

801 

802 def _get_secret_option_from_config_sources( 

803 self, config_sources: ConfigSourcesType, section: str, key: str 

804 ) -> str | None: 

805 fallback_key = key + "_secret" 

806 if (section, key) in self.sensitive_config_values: 

807 section_dict = config_sources.get(section) 

808 if section_dict is not None: 

809 secrets_path_value = section_dict.get(fallback_key) 

810 if secrets_path_value is not None: 

811 if isinstance(secrets_path_value, str): 

812 secrets_path = secrets_path_value 

813 else: 

814 secrets_path = secrets_path_value[0] 

815 return self._get_config_value_from_secret_backend(secrets_path) 

816 return None 

817 

818 def _include_secrets( 

819 self, 

820 config_sources: ConfigSourcesType, 

821 display_sensitive: bool, 

822 display_source: bool, 

823 raw: bool, 

824 ): 

825 for section, key in self.sensitive_config_values: 

826 value: str | None = self._get_secret_option_from_config_sources(config_sources, section, key) 

827 if value: 

828 if not display_sensitive: 

829 value = "< hidden >" 

830 if display_source: 

831 opt: str | tuple[str, str] = (value, "secret") 

832 elif raw: 

833 opt = value.replace("%", "%%") 

834 else: 

835 opt = value 

836 config_sources.setdefault(section, {}).update({key: opt}) 

837 del config_sources[section][key + "_secret"] 

838 

839 def _include_commands( 

840 self, 

841 config_sources: ConfigSourcesType, 

842 display_sensitive: bool, 

843 display_source: bool, 

844 raw: bool, 

845 ): 

846 for section, key in self.sensitive_config_values: 

847 opt = self._get_cmd_option_from_config_sources(config_sources, section, key) 

848 if not opt: 

849 continue 

850 opt_to_set: str | tuple[str, str] | None = opt 

851 if not display_sensitive: 

852 opt_to_set = "< hidden >" 

853 if display_source: 

854 opt_to_set = (str(opt_to_set), "cmd") 

855 elif raw: 

856 opt_to_set = str(opt_to_set).replace("%", "%%") 

857 if opt_to_set is not None: 

858 dict_to_update: dict[str, str | tuple[str, str]] = {key: opt_to_set} 

859 config_sources.setdefault(section, {}).update(dict_to_update) 

860 del config_sources[section][key + "_cmd"] 

861 

862 def _include_envs( 

863 self, 

864 config_sources: ConfigSourcesType, 

865 display_sensitive: bool, 

866 display_source: bool, 

867 raw: bool, 

868 ): 

869 for env_var in [ 

870 os_environment for os_environment in os.environ if os_environment.startswith(ENV_VAR_PREFIX) 

871 ]: 

872 try: 

873 _, section, key = env_var.split("__", 2) 

874 opt = self._get_env_var_option(section, key) 

875 except ValueError: 

876 continue 

877 if opt is None: 

878 log.warning("Ignoring unknown env var '%s'", env_var) 

879 continue 

880 if not display_sensitive and env_var != self._env_var_name("core", "unit_test_mode"): 

881 if self._names_sensitive_team_env_var(env_var): 

882 # Covers the cmd/secret variants too; see is_sensitive_option. 

883 opt = "< hidden >" 

884 # Don't hide cmd/secret values here 

885 elif not env_var.lower().endswith(("cmd", "secret")): 

886 if (section, key) in self.sensitive_config_values: 

887 opt = "< hidden >" 

888 elif raw: 

889 opt = opt.replace("%", "%%") 

890 if display_source: 

891 opt = (opt, "env var") 

892 

893 section = section.lower() 

894 key = key.lower() 

895 config_sources.setdefault(section, {}).update({key: opt}) 

896 

897 def _filter_by_source( 

898 self, 

899 config_sources: ConfigSourcesType, 

900 display_source: bool, 

901 getter_func, 

902 ): 

903 """ 

904 Delete default configs from current configuration. 

905 

906 An OrderedDict of OrderedDicts, if it would conflict with special sensitive_config_values. 

907 

908 This is necessary because bare configs take precedence over the command 

909 or secret key equivalents so if the current running config is 

910 materialized with Airflow defaults they in turn override user set 

911 command or secret key configs. 

912 

913 :param config_sources: The current configuration to operate on 

914 :param display_source: If False, configuration options contain raw 

915 values. If True, options are a tuple of (option_value, source). 

916 Source is either 'airflow.cfg', 'default', 'env var', or 'cmd'. 

917 :param getter_func: A callback function that gets the user configured 

918 override value for a particular sensitive_config_values config. 

919 :return: None, the given config_sources is filtered if necessary, 

920 otherwise untouched. 

921 """ 

922 for section, key in self.sensitive_config_values: 

923 # Don't bother if we don't have section / key 

924 if section not in config_sources or key not in config_sources[section]: 

925 continue 

926 # Check that there is something to override defaults 

927 try: 

928 getter_opt = getter_func(section, key) 

929 except ValueError: 

930 continue 

931 if not getter_opt: 

932 continue 

933 # Check to see that there is a default value 

934 if self.get_default_value(section, key) is None: 

935 continue 

936 # Check to see if bare setting is the same as defaults 

937 if display_source: 

938 # when display_source = true, we know that the config_sources contains tuple 

939 opt, source = config_sources[section][key] # type: ignore 

940 else: 

941 opt = config_sources[section][key] # type: ignore[assignment] 

942 if opt == self.get_default_value(section, key): 

943 del config_sources[section][key] 

944 

945 @staticmethod 

946 def _deprecated_value_is_set_in_config( 

947 deprecated_section: str, 

948 deprecated_key: str, 

949 configs: Iterable[tuple[str, ConfigParser]], 

950 ) -> bool: 

951 for config_type, config in configs: 

952 if config_type != "default": 

953 with contextlib.suppress(NoSectionError): 

954 deprecated_section_array = config.items(section=deprecated_section, raw=True) 

955 if any(key == deprecated_key for key, _ in deprecated_section_array): 

956 return True 

957 return False 

958 

959 @staticmethod 

960 def _deprecated_variable_is_set(deprecated_section: str, deprecated_key: str) -> bool: 

961 return ( 

962 os.environ.get(f"{ENV_VAR_PREFIX}{deprecated_section.upper()}__{deprecated_key.upper()}") 

963 is not None 

964 ) 

965 

966 @staticmethod 

967 def _deprecated_command_is_set_in_config( 

968 deprecated_section: str, 

969 deprecated_key: str, 

970 configs: Iterable[tuple[str, ConfigParser]], 

971 ) -> bool: 

972 return AirflowConfigParser._deprecated_value_is_set_in_config( 

973 deprecated_section=deprecated_section, deprecated_key=deprecated_key + "_cmd", configs=configs 

974 ) 

975 

976 @staticmethod 

977 def _deprecated_variable_command_is_set(deprecated_section: str, deprecated_key: str) -> bool: 

978 return ( 

979 os.environ.get(f"{ENV_VAR_PREFIX}{deprecated_section.upper()}__{deprecated_key.upper()}_CMD") 

980 is not None 

981 ) 

982 

983 @staticmethod 

984 def _deprecated_secret_is_set_in_config( 

985 deprecated_section: str, 

986 deprecated_key: str, 

987 configs: Iterable[tuple[str, ConfigParser]], 

988 ) -> bool: 

989 return AirflowConfigParser._deprecated_value_is_set_in_config( 

990 deprecated_section=deprecated_section, deprecated_key=deprecated_key + "_secret", configs=configs 

991 ) 

992 

993 @staticmethod 

994 def _deprecated_variable_secret_is_set(deprecated_section: str, deprecated_key: str) -> bool: 

995 return ( 

996 os.environ.get(f"{ENV_VAR_PREFIX}{deprecated_section.upper()}__{deprecated_key.upper()}_SECRET") 

997 is not None 

998 ) 

999 

1000 @staticmethod 

1001 def _replace_config_with_display_sources( 

1002 config_sources: ConfigSourcesType, 

1003 configs: Iterable[tuple[str, ConfigParser]], 

1004 configuration_description: dict[str, dict[str, Any]], 

1005 display_source: bool, 

1006 raw: bool, 

1007 deprecated_options: dict[tuple[str, str], tuple[str, str, str]], 

1008 include_env: bool, 

1009 include_cmds: bool, 

1010 include_secret: bool, 

1011 ): 

1012 for source_name, config in configs: 

1013 sections = config.sections() 

1014 for section in sections: 

1015 AirflowConfigParser._replace_section_config_with_display_sources( 

1016 config, 

1017 config_sources, 

1018 configuration_description, 

1019 display_source, 

1020 raw, 

1021 section, 

1022 source_name, 

1023 deprecated_options, 

1024 configs, 

1025 include_env=include_env, 

1026 include_cmds=include_cmds, 

1027 include_secret=include_secret, 

1028 ) 

1029 

1030 @staticmethod 

1031 def _replace_section_config_with_display_sources( 

1032 config: ConfigParser, 

1033 config_sources: ConfigSourcesType, 

1034 configuration_description: dict[str, dict[str, Any]], 

1035 display_source: bool, 

1036 raw: bool, 

1037 section: str, 

1038 source_name: str, 

1039 deprecated_options: dict[tuple[str, str], tuple[str, str, str]], 

1040 configs: Iterable[tuple[str, ConfigParser]], 

1041 include_env: bool, 

1042 include_cmds: bool, 

1043 include_secret: bool, 

1044 ): 

1045 sect = config_sources.setdefault(section, {}) 

1046 if isinstance(config, AirflowConfigParser): 

1047 with config.suppress_future_warnings(): 

1048 items: Iterable[tuple[str, Any]] = config.items(section=section, raw=raw) 

1049 else: 

1050 items = config.items(section=section, raw=raw) 

1051 for k, val in items: 

1052 deprecated_section, deprecated_key, _ = deprecated_options.get((section, k), (None, None, None)) 

1053 if deprecated_section and deprecated_key: 

1054 if source_name == "default": 

1055 # If deprecated entry has some non-default value set for any of the sources requested, 

1056 # We should NOT set default for the new entry (because it will override anything 

1057 # coming from the deprecated ones) 

1058 if AirflowConfigParser._deprecated_value_is_set_in_config( 

1059 deprecated_section, deprecated_key, configs 

1060 ): 

1061 continue 

1062 if include_env and AirflowConfigParser._deprecated_variable_is_set( 

1063 deprecated_section, deprecated_key 

1064 ): 

1065 continue 

1066 if include_cmds and ( 

1067 AirflowConfigParser._deprecated_variable_command_is_set( 

1068 deprecated_section, deprecated_key 

1069 ) 

1070 or AirflowConfigParser._deprecated_command_is_set_in_config( 

1071 deprecated_section, deprecated_key, configs 

1072 ) 

1073 ): 

1074 continue 

1075 if include_secret and ( 

1076 AirflowConfigParser._deprecated_variable_secret_is_set( 

1077 deprecated_section, deprecated_key 

1078 ) 

1079 or AirflowConfigParser._deprecated_secret_is_set_in_config( 

1080 deprecated_section, deprecated_key, configs 

1081 ) 

1082 ): 

1083 continue 

1084 if display_source: 

1085 updated_source_name = source_name 

1086 if source_name == "default": 

1087 # defaults can come from other sources (default-<PROVIDER>) that should be used here 

1088 source_description_section = configuration_description.get(section, {}) 

1089 source_description_key = source_description_section.get("options", {}).get(k, {}) 

1090 if source_description_key is not None: 

1091 updated_source_name = source_description_key.get("source", source_name) 

1092 sect[k] = (val, updated_source_name) 

1093 else: 

1094 sect[k] = val 

1095 

1096 def _warn_deprecate( 

1097 self, section: str, key: str, deprecated_section: str, deprecated_name: str, extra_stacklevel: int 

1098 ): 

1099 """Warn about deprecated config option usage.""" 

1100 if section == deprecated_section: 

1101 warnings.warn( 

1102 f"The {deprecated_name} option in [{section}] has been renamed to {key} - " 

1103 f"the old setting has been used, but please update your config.", 

1104 DeprecationWarning, 

1105 stacklevel=4 + extra_stacklevel, 

1106 ) 

1107 else: 

1108 warnings.warn( 

1109 f"The {deprecated_name} option in [{deprecated_section}] has been moved to the {key} option " 

1110 f"in [{section}] - the old setting has been used, but please update your config.", 

1111 DeprecationWarning, 

1112 stacklevel=4 + extra_stacklevel, 

1113 ) 

1114 

1115 @contextmanager 

1116 def suppress_future_warnings(self): 

1117 """ 

1118 Context manager to temporarily suppress future warnings. 

1119 

1120 This is a stub used by the shared parser's lookup methods when checking deprecated options. 

1121 Subclasses can override this to customize warning suppression behavior. 

1122 

1123 :return: context manager that suppresses future warnings 

1124 """ 

1125 suppress_future_warnings = self._suppress_future_warnings 

1126 self._suppress_future_warnings = True 

1127 yield self 

1128 self._suppress_future_warnings = suppress_future_warnings 

1129 

1130 def _env_var_name(self, section: str, key: str, team_name: str | None = None) -> str: 

1131 """Generate environment variable name for a config option.""" 

1132 team_component: str = f"{team_name.upper()}___" if team_name else "" 

1133 return f"{ENV_VAR_PREFIX}{team_component}{section.replace('.', '_').upper()}__{key.upper()}" 

1134 

1135 def _get_env_var_option(self, section: str, key: str, team_name: str | None = None): 

1136 """Get config option from environment variable.""" 

1137 env_var: str = self._env_var_name(section, key, team_name=team_name) 

1138 if env_var in os.environ: 

1139 return expand_env_var(os.environ[env_var]) 

1140 # alternatively AIRFLOW__{SECTION}__{KEY}_CMD (for a command) 

1141 env_var_cmd = env_var + "_CMD" 

1142 if env_var_cmd in os.environ: 

1143 # if this is a valid command key... 

1144 if (section, key) in self.sensitive_config_values: 

1145 return run_command(os.environ[env_var_cmd]) 

1146 # alternatively AIRFLOW__{SECTION}__{KEY}_SECRET (to get from Secrets Backend) 

1147 env_var_secret_path = env_var + "_SECRET" 

1148 if env_var_secret_path in os.environ: 

1149 # if this is a valid secret path... 

1150 if (section, key) in self.sensitive_config_values: 

1151 return self._get_config_value_from_secret_backend(os.environ[env_var_secret_path]) 

1152 return None 

1153 

1154 def _get_cmd_option(self, section: str, key: str): 

1155 """Get config option from command execution.""" 

1156 fallback_key = key + "_cmd" 

1157 if (section, key) in self.sensitive_config_values: 

1158 if super().has_option(section, fallback_key): 

1159 command = super().get(section, fallback_key) 

1160 try: 

1161 cmd_output = run_command(command) 

1162 except AirflowConfigException as e: 

1163 raise e 

1164 except Exception as e: 

1165 raise AirflowConfigException( 

1166 f"Cannot run the command for the config section [{section}]{fallback_key}_cmd." 

1167 f" Please check the {fallback_key} value." 

1168 ) from e 

1169 return cmd_output 

1170 return None 

1171 

1172 def _get_secret_option(self, section: str, key: str) -> str | None: 

1173 """Get Config option values from Secret Backend.""" 

1174 fallback_key = key + "_secret" 

1175 if (section, key) in self.sensitive_config_values: 

1176 if super().has_option(section, fallback_key): 

1177 secrets_path = super().get(section, fallback_key) 

1178 return self._get_config_value_from_secret_backend(secrets_path) 

1179 return None 

1180 

1181 def _get_environment_variables( 

1182 self, 

1183 deprecated_key: str | None, 

1184 deprecated_section: str | None, 

1185 key: str, 

1186 section: str, 

1187 issue_warning: bool = True, 

1188 extra_stacklevel: int = 0, 

1189 **kwargs, 

1190 ) -> str | ValueNotFound: 

1191 """Get config option from environment variables.""" 

1192 team_name = kwargs.get("team_name", None) 

1193 option = self._get_env_var_option(section, key, team_name=team_name) 

1194 if option is not None: 

1195 return option 

1196 if deprecated_section and deprecated_key: 

1197 with self.suppress_future_warnings(): 

1198 option = self._get_env_var_option(deprecated_section, deprecated_key, team_name=team_name) 

1199 if option is not None: 

1200 if issue_warning: 

1201 self._warn_deprecate(section, key, deprecated_section, deprecated_key, extra_stacklevel) 

1202 return option 

1203 return VALUE_NOT_FOUND_SENTINEL 

1204 

1205 def _get_option_from_config_file( 

1206 self, 

1207 deprecated_key: str | None, 

1208 deprecated_section: str | None, 

1209 key: str, 

1210 section: str, 

1211 issue_warning: bool = True, 

1212 extra_stacklevel: int = 0, 

1213 **kwargs, 

1214 ) -> str | ValueNotFound: 

1215 """Get config option from config file.""" 

1216 if team_name := kwargs.get("team_name", None): 

1217 section = team_section_name(team_name, section) 

1218 # since this is the last lookup that supports team_name, pop it 

1219 kwargs.pop("team_name") 

1220 if super().has_option(section, key): 

1221 return expand_env_var(super().get(section, key, **kwargs)) 

1222 if deprecated_section and deprecated_key: 

1223 if super().has_option(deprecated_section, deprecated_key): 

1224 if issue_warning: 

1225 self._warn_deprecate(section, key, deprecated_section, deprecated_key, extra_stacklevel) 

1226 with self.suppress_future_warnings(): 

1227 return expand_env_var(super().get(deprecated_section, deprecated_key, **kwargs)) 

1228 return VALUE_NOT_FOUND_SENTINEL 

1229 

1230 def _get_option_from_commands( 

1231 self, 

1232 deprecated_key: str | None, 

1233 deprecated_section: str | None, 

1234 key: str, 

1235 section: str, 

1236 issue_warning: bool = True, 

1237 extra_stacklevel: int = 0, 

1238 **kwargs, 

1239 ) -> str | ValueNotFound: 

1240 """Get config option from command execution.""" 

1241 if kwargs.get("team_name", None): 

1242 # Commands based team config fetching is not currently supported 

1243 return VALUE_NOT_FOUND_SENTINEL 

1244 option = self._get_cmd_option(section, key) 

1245 if option: 

1246 return option 

1247 if deprecated_section and deprecated_key: 

1248 with self.suppress_future_warnings(): 

1249 option = self._get_cmd_option(deprecated_section, deprecated_key) 

1250 if option: 

1251 if issue_warning: 

1252 self._warn_deprecate(section, key, deprecated_section, deprecated_key, extra_stacklevel) 

1253 return option 

1254 return VALUE_NOT_FOUND_SENTINEL 

1255 

1256 def _get_option_from_secrets( 

1257 self, 

1258 deprecated_key: str | None, 

1259 deprecated_section: str | None, 

1260 key: str, 

1261 section: str, 

1262 issue_warning: bool = True, 

1263 extra_stacklevel: int = 0, 

1264 **kwargs, 

1265 ) -> str | ValueNotFound: 

1266 """Get config option from secrets backend.""" 

1267 if kwargs.get("team_name", None): 

1268 # Secrets based team config fetching is not currently supported 

1269 return VALUE_NOT_FOUND_SENTINEL 

1270 option = self._get_secret_option(section, key) 

1271 if option: 

1272 return option 

1273 if deprecated_section and deprecated_key: 

1274 with self.suppress_future_warnings(): 

1275 option = self._get_secret_option(deprecated_section, deprecated_key) 

1276 if option: 

1277 if issue_warning: 

1278 self._warn_deprecate(section, key, deprecated_section, deprecated_key, extra_stacklevel) 

1279 return option 

1280 return VALUE_NOT_FOUND_SENTINEL 

1281 

1282 def _get_option_from_defaults( 

1283 self, 

1284 deprecated_key: str | None, 

1285 deprecated_section: str | None, 

1286 key: str, 

1287 section: str, 

1288 issue_warning: bool = True, 

1289 extra_stacklevel: int = 0, 

1290 team_name: str | None = None, 

1291 **kwargs, 

1292 ) -> str | ValueNotFound: 

1293 """Get config option from default values.""" 

1294 if self.get_default_value(section, key) is not None or "fallback" in kwargs: 

1295 return expand_env_var(self.get_default_value(section, key, **kwargs)) 

1296 return VALUE_NOT_FOUND_SENTINEL 

1297 

1298 def _resolve_deprecated_lookup( 

1299 self, 

1300 section: str, 

1301 key: str, 

1302 lookup_from_deprecated: bool, 

1303 extra_stacklevel: int = 0, 

1304 ) -> tuple[str, str, str | None, str | None, bool]: 

1305 """ 

1306 Resolve deprecated section/key mappings and determine deprecated values. 

1307 

1308 :param section: Section name (will be lowercased) 

1309 :param key: Key name (will be lowercased) 

1310 :param lookup_from_deprecated: Whether to lookup from deprecated options 

1311 :param extra_stacklevel: Extra stack level for warnings 

1312 :return: Tuple of (resolved_section, resolved_key, deprecated_section, deprecated_key, warning_emitted) 

1313 """ 

1314 section = section.lower() 

1315 key = key.lower() 

1316 warning_emitted = False 

1317 deprecated_section: str | None = None 

1318 deprecated_key: str | None = None 

1319 

1320 if not lookup_from_deprecated: 

1321 return section, key, deprecated_section, deprecated_key, warning_emitted 

1322 

1323 option_description = self.configuration_description.get(section, {}).get("options", {}).get(key, {}) 

1324 if option_description.get("deprecated"): 

1325 deprecation_reason = option_description.get("deprecation_reason", "") 

1326 warnings.warn( 

1327 f"The '{key}' option in section {section} is deprecated. {deprecation_reason}", 

1328 DeprecationWarning, 

1329 stacklevel=2 + extra_stacklevel, 

1330 ) 

1331 # For the cases in which we rename whole sections 

1332 if section in self.inversed_deprecated_sections: 

1333 deprecated_section, deprecated_key = (section, key) 

1334 section = self.inversed_deprecated_sections[section] 

1335 if not self._suppress_future_warnings: 

1336 warnings.warn( 

1337 f"The config section [{deprecated_section}] has been renamed to " 

1338 f"[{section}]. Please update your `conf.get*` call to use the new name", 

1339 FutureWarning, 

1340 stacklevel=2 + extra_stacklevel, 

1341 ) 

1342 # Don't warn about individual rename if the whole section is renamed 

1343 warning_emitted = True 

1344 elif (section, key) in self.inversed_deprecated_options: 

1345 # Handle using deprecated section/key instead of the new section/key 

1346 new_section, new_key = self.inversed_deprecated_options[(section, key)] 

1347 if not self._suppress_future_warnings and not warning_emitted: 

1348 warnings.warn( 

1349 f"section/key [{section}/{key}] has been deprecated, you should use" 

1350 f"[{new_section}/{new_key}] instead. Please update your `conf.get*` call to use the " 

1351 "new name", 

1352 FutureWarning, 

1353 stacklevel=2 + extra_stacklevel, 

1354 ) 

1355 warning_emitted = True 

1356 deprecated_section, deprecated_key = section, key 

1357 section, key = (new_section, new_key) 

1358 elif section in self.deprecated_sections: 

1359 # When accessing the new section name, make sure we check under the old config name 

1360 deprecated_key = key 

1361 deprecated_section = self.deprecated_sections[section][0] 

1362 else: 

1363 deprecated_section, deprecated_key, _ = self.deprecated_options.get( 

1364 (section, key), (None, None, None) 

1365 ) 

1366 

1367 return section, key, deprecated_section, deprecated_key, warning_emitted 

1368 

1369 def load_providers_configuration(self) -> None: 

1370 """ 

1371 Load configuration for providers. 

1372 

1373 .. deprecated:: 3.2.0 

1374 Provider configuration is now loaded lazily via the ``configuration_description`` 

1375 cached property. This method is kept for backwards compatibility and will be 

1376 removed in a future version. 

1377 """ 

1378 warnings.warn( 

1379 "load_providers_configuration() is deprecated. " 

1380 "Provider configuration is now loaded lazily via the " 

1381 "`configuration_description` cached property.", 

1382 DeprecationWarning, 

1383 stacklevel=2, 

1384 ) 

1385 self._use_providers_configuration = True 

1386 self._invalidate_provider_flag_caches() 

1387 

1388 def restore_core_default_configuration(self) -> None: 

1389 """ 

1390 Restore the parser state before provider-contributed sections were loaded. 

1391 

1392 .. deprecated:: 3.2.0 

1393 Use ``make_sure_configuration_loaded(with_providers=False)`` context manager 

1394 instead. This method is kept for backwards compatibility and will be removed 

1395 in a future version. 

1396 """ 

1397 warnings.warn( 

1398 "restore_core_default_configuration() is deprecated. " 

1399 "Use `make_sure_configuration_loaded(with_providers=False)` instead.", 

1400 DeprecationWarning, 

1401 stacklevel=2, 

1402 ) 

1403 self._use_providers_configuration = False 

1404 self._invalidate_provider_flag_caches() 

1405 

1406 @overload # type: ignore[override] 

1407 def get(self, section: str, key: str, fallback: str = ..., **kwargs) -> str: ... 

1408 

1409 @overload # type: ignore[override] 

1410 def get(self, section: str, key: str, **kwargs) -> str | None: ... 

1411 

1412 def get( # type: ignore[misc, override] 

1413 self, 

1414 section: str, 

1415 key: str, 

1416 suppress_warnings: bool = False, 

1417 lookup_from_deprecated: bool = True, 

1418 _extra_stacklevel: int = 0, 

1419 team_name: str | None = None, 

1420 **kwargs, 

1421 ) -> str | None: 

1422 """ 

1423 Get config value by iterating through lookup sequence. 

1424 

1425 Priority order is defined by _lookup_sequence property. 

1426 """ 

1427 section, key, deprecated_section, deprecated_key, warning_emitted = self._resolve_deprecated_lookup( 

1428 section=section, 

1429 key=key, 

1430 lookup_from_deprecated=lookup_from_deprecated, 

1431 extra_stacklevel=_extra_stacklevel, 

1432 ) 

1433 

1434 if team_name is not None: 

1435 kwargs["team_name"] = team_name 

1436 

1437 for lookup_method in self._lookup_sequence: 

1438 value = lookup_method( 

1439 deprecated_key=deprecated_key, 

1440 deprecated_section=deprecated_section, 

1441 key=key, 

1442 section=section, 

1443 issue_warning=not warning_emitted, 

1444 extra_stacklevel=_extra_stacklevel, 

1445 **kwargs, 

1446 ) 

1447 if value is not VALUE_NOT_FOUND_SENTINEL: 

1448 return value 

1449 

1450 # Check if fallback was explicitly provided (even if None) 

1451 if "fallback" in kwargs: 

1452 return kwargs["fallback"] 

1453 

1454 if not suppress_warnings: 

1455 log.warning("section/key [%s/%s] not found in config", section, key) 

1456 

1457 raise AirflowConfigException(f"section/key [{section}/{key}] not found in config") 

1458 

1459 def getboolean(self, section: str, key: str, **kwargs) -> bool: # type: ignore[override] 

1460 """Get config value as boolean.""" 

1461 val = str(self.get(section, key, _extra_stacklevel=1, **kwargs)).lower().strip() 

1462 if "#" in val: 

1463 val = val.split("#")[0].strip() 

1464 if val in ("t", "true", "1"): 

1465 return True 

1466 if val in ("f", "false", "0"): 

1467 return False 

1468 raise AirflowConfigException( 

1469 f'Failed to convert value to bool. Please check "{key}" key in "{section}" section. ' 

1470 f'Current value: "{val}".' 

1471 ) 

1472 

1473 def getint(self, section: str, key: str, **kwargs) -> int: # type: ignore[override] 

1474 """Get config value as integer.""" 

1475 val = self.get(section, key, _extra_stacklevel=1, **kwargs) 

1476 if val is None: 

1477 raise AirflowConfigException( 

1478 f"Failed to convert value None to int. " 

1479 f'Please check "{key}" key in "{section}" section is set.' 

1480 ) 

1481 try: 

1482 return int(val) 

1483 except ValueError: 

1484 try: 

1485 if (float_val := float(val)) != (int_val := int(float_val)): 

1486 raise ValueError 

1487 return int_val 

1488 except (ValueError, OverflowError): 

1489 raise AirflowConfigException( 

1490 f'Failed to convert value to int. Please check "{key}" key in "{section}" section. ' 

1491 f'Current value: "{val}".' 

1492 ) 

1493 

1494 def getfloat(self, section: str, key: str, **kwargs) -> float: # type: ignore[override] 

1495 """Get config value as float.""" 

1496 val = self.get(section, key, _extra_stacklevel=1, **kwargs) 

1497 if val is None: 

1498 raise AirflowConfigException( 

1499 f"Failed to convert value None to float. " 

1500 f'Please check "{key}" key in "{section}" section is set.' 

1501 ) 

1502 try: 

1503 return float(val) 

1504 except ValueError: 

1505 raise AirflowConfigException( 

1506 f'Failed to convert value to float. Please check "{key}" key in "{section}" section. ' 

1507 f'Current value: "{val}".' 

1508 ) 

1509 

1510 def getlist(self, section: str, key: str, delimiter=",", **kwargs): 

1511 """Get config value as list.""" 

1512 val = self.get(section, key, **kwargs) 

1513 

1514 if isinstance(val, list) or val is None: 

1515 # `get` will always return a (possibly-empty) string, so the only way we can 

1516 # have these types is with `fallback=` was specified. So just return it. 

1517 return val 

1518 

1519 if val == "": 

1520 return [] 

1521 

1522 try: 

1523 return [item.strip() for item in val.split(delimiter)] 

1524 except Exception: 

1525 raise AirflowConfigException( 

1526 f'Failed to parse value to a list. Please check "{key}" key in "{section}" section. ' 

1527 f'Current value: "{val}".' 

1528 ) 

1529 

1530 E = TypeVar("E", bound=Enum) 

1531 

1532 def getenum(self, section: str, key: str, enum_class: type[E], **kwargs) -> E: 

1533 """Get config value as enum.""" 

1534 val = self.get(section, key, **kwargs) 

1535 enum_names = [enum_item.name for enum_item in enum_class] 

1536 

1537 if val is None: 

1538 raise AirflowConfigException( 

1539 f'Failed to convert value. Please check "{key}" key in "{section}" section. ' 

1540 f'Current value: "{val}" and it must be one of {", ".join(enum_names)}' 

1541 ) 

1542 

1543 try: 

1544 return enum_class[val] 

1545 except KeyError: 

1546 if "fallback" in kwargs and kwargs["fallback"] in enum_names: 

1547 return enum_class[kwargs["fallback"]] 

1548 raise AirflowConfigException( 

1549 f'Failed to convert value. Please check "{key}" key in "{section}" section. ' 

1550 f"the value must be one of {', '.join(enum_names)}" 

1551 ) 

1552 

1553 def getenumlist(self, section: str, key: str, enum_class: type[E], delimiter=",", **kwargs) -> list[E]: 

1554 """Get config value as list of enums.""" 

1555 kwargs.setdefault("fallback", []) 

1556 string_list = self.getlist(section, key, delimiter, **kwargs) 

1557 

1558 enum_names = [enum_item.name for enum_item in enum_class] 

1559 enum_list = [] 

1560 

1561 for val in string_list: 

1562 try: 

1563 enum_list.append(enum_class[val]) 

1564 except KeyError: 

1565 log.warning( 

1566 "Failed to convert value %r. Please check %s key in %s section. " 

1567 "it must be one of %s, if not the value is ignored", 

1568 val, 

1569 key, 

1570 section, 

1571 ", ".join(enum_names), 

1572 ) 

1573 

1574 return enum_list 

1575 

1576 def getimport(self, section: str, key: str, **kwargs) -> Any: 

1577 """ 

1578 Read options, import the full qualified name, and return the object. 

1579 

1580 In case of failure, it throws an exception with the key and section names 

1581 

1582 :return: The object or None, if the option is empty 

1583 """ 

1584 # Fixed: use self.get() instead of conf.get() 

1585 full_qualified_path = self.get(section=section, key=key, **kwargs) 

1586 if not full_qualified_path: 

1587 return None 

1588 

1589 try: 

1590 # Import here to avoid circular dependency 

1591 from ..module_loading import import_string 

1592 

1593 return import_string(full_qualified_path) 

1594 except ImportError as e: 

1595 log.warning(e) 

1596 raise AirflowConfigException( 

1597 f'The object could not be loaded. Please check "{key}" key in "{section}" section. ' 

1598 f'Current value: "{full_qualified_path}".' 

1599 ) 

1600 

1601 def getjson( 

1602 self, section: str, key: str, fallback=None, **kwargs 

1603 ) -> dict | list | str | int | float | None: 

1604 """ 

1605 Return a config value parsed from a JSON string. 

1606 

1607 ``fallback`` is *not* JSON parsed but used verbatim when no config value is given. 

1608 """ 

1609 try: 

1610 data = self.get(section=section, key=key, fallback=None, _extra_stacklevel=1, **kwargs) 

1611 except (NoSectionError, NoOptionError): 

1612 data = None 

1613 

1614 if data is None or data == "": 

1615 return fallback 

1616 

1617 try: 

1618 return json.loads(data) 

1619 except JSONDecodeError as e: 

1620 raise AirflowConfigException(f"Unable to parse [{section}] {key!r} as valid json") from e 

1621 

1622 def gettimedelta( 

1623 self, section: str, key: str, fallback: Any = None, **kwargs 

1624 ) -> datetime.timedelta | None: 

1625 """ 

1626 Get the config value for the given section and key, and convert it into datetime.timedelta object. 

1627 

1628 If the key is missing, then it is considered as `None`. 

1629 

1630 :param section: the section from the config 

1631 :param key: the key defined in the given section 

1632 :param fallback: fallback value when no config value is given, defaults to None 

1633 :raises AirflowConfigException: raised because ValueError or OverflowError 

1634 :return: datetime.timedelta(seconds=<config_value>) or None 

1635 """ 

1636 val = self.get(section, key, fallback=fallback, _extra_stacklevel=1, **kwargs) 

1637 

1638 if val: 

1639 # the given value must be convertible to integer 

1640 try: 

1641 int_val = int(val) 

1642 except ValueError: 

1643 raise AirflowConfigException( 

1644 f'Failed to convert value to int. Please check "{key}" key in "{section}" section. ' 

1645 f'Current value: "{val}".' 

1646 ) 

1647 

1648 try: 

1649 return datetime.timedelta(seconds=int_val) 

1650 except OverflowError as err: 

1651 raise AirflowConfigException( 

1652 f"Failed to convert value to timedelta in `seconds`. " 

1653 f"{err}. " 

1654 f'Please check "{key}" key in "{section}" section. Current value: "{val}".' 

1655 ) 

1656 

1657 return fallback 

1658 

1659 def get_mandatory_value(self, section: str, key: str, **kwargs) -> str: 

1660 """Get mandatory config value, raising ValueError if not found.""" 

1661 value = self.get(section, key, _extra_stacklevel=1, **kwargs) 

1662 if value is None: 

1663 raise ValueError(f"The value {section}/{key} should be set!") 

1664 return value 

1665 

1666 def get_mandatory_list_value(self, section: str, key: str, **kwargs) -> list[str]: 

1667 """Get mandatory config value as list, raising ValueError if not found.""" 

1668 value = self.getlist(section, key, **kwargs) 

1669 if value is None: 

1670 raise ValueError(f"The value {section}/{key} should be set!") 

1671 return value 

1672 

1673 def read( # type: ignore[override] 

1674 self, 

1675 filenames: str | bytes | os.PathLike | Iterable[str | bytes | os.PathLike], 

1676 encoding: str | None = None, 

1677 ) -> list[str]: 

1678 return super().read(filenames=filenames, encoding=encoding) # type: ignore[arg-type,return-value] 

1679 

1680 def read_dict( # type: ignore[override] 

1681 self, dictionary: dict[str, dict[str, Any]], source: str = "<dict>" 

1682 ) -> None: 

1683 """ 

1684 We define a different signature here to add better type hints and checking. 

1685 

1686 :param dictionary: dictionary to read from 

1687 :param source: source to be used to store the configuration 

1688 :return: 

1689 """ 

1690 super().read_dict(dictionary=dictionary, source=source) 

1691 

1692 def _has_section_in_any_defaults(self, section: str) -> bool: 

1693 """Check if section exists in core defaults or provider fallback defaults.""" 

1694 if self._default_values.has_section(section): 

1695 return True 

1696 if self._use_providers_configuration: 

1697 if self._provider_cfg_config_fallback_default_values.has_section(section): 

1698 return True 

1699 if self._provider_metadata_config_fallback_default_values.has_section(section): 

1700 return True 

1701 return False 

1702 

1703 def get_sections_including_defaults(self) -> list[str]: 

1704 """ 

1705 Retrieve all sections from the configuration parser, including sections defined by built-in defaults. 

1706 

1707 :return: list of section names 

1708 """ 

1709 sections_from_config = self.sections() 

1710 sections_from_description = list(self.configuration_description.keys()) 

1711 return list(dict.fromkeys(itertools.chain(sections_from_description, sections_from_config))) 

1712 

1713 def get_options_including_defaults(self, section: str) -> list[str]: 

1714 """ 

1715 Retrieve all possible options from the configuration parser for the section given. 

1716 

1717 Includes options defined by built-in defaults. 

1718 

1719 :param section: section name 

1720 :return: list of option names for the section given 

1721 """ 

1722 my_own_options = self.options(section) if self.has_section(section) else [] 

1723 all_options_from_defaults = list( 

1724 self.configuration_description.get(section, {}).get("options", {}).keys() 

1725 ) 

1726 return list(dict.fromkeys(itertools.chain(all_options_from_defaults, my_own_options))) 

1727 

1728 def has_option( # type: ignore[override] 

1729 self, section: str, option: str, lookup_from_deprecated: bool = True, **kwargs 

1730 ) -> bool: 

1731 """ 

1732 Check if option is defined. 

1733 

1734 Uses self.get() to avoid reimplementing the priority order of config variables 

1735 (env, config, cmd, defaults). 

1736 

1737 :param section: section to get option from 

1738 :param option: option to get 

1739 :param lookup_from_deprecated: If True, check if the option is defined in deprecated sections 

1740 :param kwargs: additional keyword arguments to pass to get(), such as team_name 

1741 :return: 

1742 """ 

1743 try: 

1744 value = self.get( 

1745 section, 

1746 option, 

1747 fallback=VALUE_NOT_FOUND_SENTINEL, 

1748 _extra_stacklevel=1, 

1749 suppress_warnings=True, 

1750 lookup_from_deprecated=lookup_from_deprecated, 

1751 **kwargs, 

1752 ) 

1753 if value is VALUE_NOT_FOUND_SENTINEL: 

1754 return False 

1755 return True 

1756 except (NoOptionError, NoSectionError, AirflowConfigException): 

1757 return False 

1758 

1759 def set(self, section: str, option: str, value: str | None = None) -> None: # type: ignore[override] 

1760 """ 

1761 Set an option to the given value. 

1762 

1763 This override just makes sure the section and option are lower case, to match what we do in `get`. 

1764 """ 

1765 section = section.lower() 

1766 option = option.lower() 

1767 defaults = self.configuration_description or {} 

1768 if not self.has_section(section) and section in defaults: 

1769 # Trying to set a key in a section that exists in default, but not in the user config; 

1770 # automatically create it 

1771 self.add_section(section) 

1772 super().set(section, option, value) 

1773 

1774 def remove_option(self, section: str, option: str, remove_default: bool = True): # type: ignore[override] 

1775 """ 

1776 Remove an option if it exists in config from a file or default config. 

1777 

1778 If both of config have the same option, this removes the option 

1779 in both configs unless remove_default=False. 

1780 """ 

1781 section = section.lower() 

1782 option = option.lower() 

1783 if super().has_option(section, option): 

1784 super().remove_option(section, option) 

1785 

1786 if remove_default and self._default_values.has_option(section, option): 

1787 self._default_values.remove_option(section, option) 

1788 

1789 def optionxform(self, optionstr: str) -> str: 

1790 """ 

1791 Transform option names on every read, get, or set operation. 

1792 

1793 This changes from the default behaviour of ConfigParser from lower-casing 

1794 to instead be case-preserving. 

1795 

1796 :param optionstr: 

1797 :return: 

1798 """ 

1799 return optionstr 

1800 

1801 def as_dict( 

1802 self, 

1803 display_source: bool = False, 

1804 display_sensitive: bool = False, 

1805 raw: bool = False, 

1806 include_env: bool = True, 

1807 include_cmds: bool = True, 

1808 include_secret: bool = True, 

1809 ) -> ConfigSourcesType: 

1810 """ 

1811 Return the current configuration as an OrderedDict of OrderedDicts. 

1812 

1813 When materializing current configuration Airflow defaults are 

1814 materialized along with user set configs. If any of the `include_*` 

1815 options are False then the result of calling command or secret key 

1816 configs do not override Airflow defaults and instead are passed through. 

1817 In order to then avoid Airflow defaults from overwriting user set 

1818 command or secret key configs we filter out bare sensitive_config_values 

1819 that are set to Airflow defaults when command or secret key configs 

1820 produce different values. 

1821 

1822 :param display_source: If False, the option value is returned. If True, 

1823 a tuple of (option_value, source) is returned. Source is either 

1824 'airflow.cfg', 'default', 'env var', or 'cmd'. 

1825 :param display_sensitive: If True, the values of options set by env 

1826 vars and bash commands will be displayed. If False, those options 

1827 are shown as '< hidden >' 

1828 :param raw: Should the values be output as interpolated values, or the 

1829 "raw" form that can be fed back in to ConfigParser 

1830 :param include_env: Should the value of configuration from AIRFLOW__ 

1831 environment variables be included or not 

1832 :param include_cmds: Should the result of calling any ``*_cmd`` config be 

1833 set (True, default), or should the _cmd options be left as the 

1834 command to run (False) 

1835 :param include_secret: Should the result of calling any ``*_secret`` config be 

1836 set (True, default), or should the _secret options be left as the 

1837 path to get the secret from (False) 

1838 :return: Dictionary, where the key is the name of the section and the content is 

1839 the dictionary with the name of the parameter and its value. 

1840 """ 

1841 if not display_sensitive: 

1842 # We want to hide the sensitive values at the appropriate methods 

1843 # since envs from cmds, secrets can be read at _include_envs method 

1844 if not all([include_env, include_cmds, include_secret]): 

1845 raise ValueError( 

1846 "If display_sensitive is false, then include_env, " 

1847 "include_cmds, include_secret must all be set as True" 

1848 ) 

1849 

1850 config_sources: ConfigSourcesType = {} 

1851 

1852 # We check sequentially all those sources and the last one we saw it in will "win" 

1853 configs = self._config_sources_for_as_dict 

1854 

1855 self._replace_config_with_display_sources( 

1856 config_sources, 

1857 configs, 

1858 self.configuration_description, 

1859 display_source, 

1860 raw, 

1861 self.deprecated_options, 

1862 include_cmds=include_cmds, 

1863 include_env=include_env, 

1864 include_secret=include_secret, 

1865 ) 

1866 

1867 # add env vars and overwrite because they have priority 

1868 if include_env: 

1869 self._include_envs(config_sources, display_sensitive, display_source, raw) 

1870 else: 

1871 self._filter_by_source(config_sources, display_source, self._get_env_var_option) 

1872 

1873 # add bash commands 

1874 if include_cmds: 

1875 self._include_commands(config_sources, display_sensitive, display_source, raw) 

1876 else: 

1877 self._filter_by_source(config_sources, display_source, self._get_cmd_option) 

1878 

1879 # add config from secret backends 

1880 if include_secret: 

1881 self._include_secrets(config_sources, display_sensitive, display_source, raw) 

1882 else: 

1883 self._filter_by_source(config_sources, display_source, self._get_secret_option) 

1884 

1885 if not display_sensitive: 

1886 # This ensures the ones from config file is hidden too 

1887 # if they are not provided through env, cmd and secret 

1888 # The collected options are walked (rather than the registered sensitive ones) so that 

1889 # team scoped sections are covered as well - they are named after the team, not after 

1890 # the base section the option is registered under. 

1891 hidden = "< hidden >" 

1892 for section, options in config_sources.items(): 

1893 for key, value in list(options.items()): 

1894 if not value or not self.is_sensitive_option(section, key): 

1895 continue 

1896 if display_source: 

1897 source = value[1] 

1898 options[key] = (hidden, source) 

1899 else: 

1900 options[key] = hidden 

1901 

1902 return config_sources 

1903 

1904 def _write_option_header( 

1905 self, 

1906 file: IO[str], 

1907 option: str, 

1908 extra_spacing: bool, 

1909 include_descriptions: bool, 

1910 include_env_vars: bool, 

1911 include_examples: bool, 

1912 include_sources: bool, 

1913 section_config_description: dict[str, dict[str, Any]], 

1914 section_to_write: str, 

1915 sources_dict: ConfigSourcesType, 

1916 ) -> tuple[bool, bool]: 

1917 """ 

1918 Write header for configuration option. 

1919 

1920 Returns tuple of (should_continue, needs_separation) where needs_separation should be 

1921 set if the option needs additional separation to visually separate it from the next option. 

1922 """ 

1923 option_config_description = ( 

1924 section_config_description.get("options", {}).get(option, {}) 

1925 if section_config_description 

1926 else {} 

1927 ) 

1928 description = option_config_description.get("description") 

1929 needs_separation = False 

1930 if description and include_descriptions: 

1931 for line in description.splitlines(): 

1932 file.write(f"# {line}\n") 

1933 needs_separation = True 

1934 example = option_config_description.get("example") 

1935 if example is not None and include_examples: 

1936 if extra_spacing: 

1937 file.write("#\n") 

1938 example_lines = example.splitlines() 

1939 example = "\n# ".join(example_lines) 

1940 file.write(f"# Example: {option} = {example}\n") 

1941 needs_separation = True 

1942 if include_sources and sources_dict: 

1943 sources_section = sources_dict.get(section_to_write) 

1944 value_with_source = sources_section.get(option) if sources_section else None 

1945 if value_with_source is None: 

1946 file.write("#\n# Source: not defined\n") 

1947 else: 

1948 file.write(f"#\n# Source: {value_with_source[1]}\n") 

1949 needs_separation = True 

1950 if include_env_vars: 

1951 file.write(f"#\n# Variable: AIRFLOW__{section_to_write.upper()}__{option.upper()}\n") 

1952 if extra_spacing: 

1953 file.write("#\n") 

1954 needs_separation = True 

1955 return True, needs_separation 

1956 

1957 def is_template(self, section: str, key) -> bool: 

1958 """ 

1959 Return whether the value is templated. 

1960 

1961 :param section: section of the config 

1962 :param key: key in the section 

1963 :return: True if the value is templated 

1964 """ 

1965 return _is_template(self.configuration_description, section, key) 

1966 

1967 def getsection(self, section: str, team_name: str | None = None) -> ConfigOptionsDictType | None: 

1968 """ 

1969 Return the section as a dict. 

1970 

1971 Values are converted to int, float, bool as required. 

1972 

1973 :param section: section from the config 

1974 :param team_name: optional team name for team-specific configuration lookup 

1975 """ 

1976 # Handle team-specific section lookup for config file 

1977 config_section = team_section_name(team_name, section) if team_name else section 

1978 

1979 if not self.has_section(config_section) and not self._has_section_in_any_defaults(config_section): 

1980 return None 

1981 if self._default_values.has_section(config_section): 

1982 _section: ConfigOptionsDictType = dict(self._default_values.items(config_section)) 

1983 else: 

1984 _section = {} 

1985 

1986 if self.has_section(config_section): 

1987 _section.update(self.items(config_section)) 

1988 

1989 # Use section (not config_section) for env var lookup - team_name is handled by _env_var_name 

1990 section_prefix = self._env_var_name(section, "", team_name=team_name) 

1991 for env_var in sorted(os.environ.keys()): 

1992 if env_var.startswith(section_prefix): 

1993 key = env_var.replace(section_prefix, "") 

1994 if key.endswith("_CMD"): 

1995 key = key[:-4] 

1996 key = key.lower() 

1997 _section[key] = self._get_env_var_option(section, key, team_name=team_name) 

1998 

1999 for key, val in _section.items(): 

2000 if val is None: 

2001 raise AirflowConfigException( 

2002 f"Failed to convert value automatically. " 

2003 f'Please check "{key}" key in "{section}" section is set.' 

2004 ) 

2005 try: 

2006 _section[key] = int(val) 

2007 except ValueError: 

2008 try: 

2009 _section[key] = float(val) 

2010 except ValueError: 

2011 if isinstance(val, str) and val.lower() in ("t", "true"): 

2012 _section[key] = True 

2013 elif isinstance(val, str) and val.lower() in ("f", "false"): 

2014 _section[key] = False 

2015 return _section 

2016 

2017 @staticmethod 

2018 def _write_section_header( 

2019 file: IO[str], 

2020 include_descriptions: bool, 

2021 section_config_description: dict[str, str], 

2022 section_to_write: str, 

2023 ) -> None: 

2024 """Write header for configuration section.""" 

2025 file.write(f"[{section_to_write}]\n") 

2026 section_description = section_config_description.get("description") 

2027 if section_description and include_descriptions: 

2028 for line in section_description.splitlines(): 

2029 file.write(f"# {line}\n") 

2030 file.write("\n") 

2031 

2032 def _write_value( 

2033 self, 

2034 file: IO[str], 

2035 option: str, 

2036 comment_out_everything: bool, 

2037 needs_separation: bool, 

2038 only_defaults: bool, 

2039 section_to_write: str, 

2040 hide_sensitive: bool, 

2041 is_sensitive: bool, 

2042 show_values: bool = False, 

2043 ): 

2044 default_value = self.get_default_value(section_to_write, option, raw=True) 

2045 if only_defaults: 

2046 value = default_value 

2047 else: 

2048 value = self.get(section_to_write, option, fallback=default_value, raw=True) 

2049 if not show_values: 

2050 file.write(f"# {option} = \n") 

2051 else: 

2052 if hide_sensitive and is_sensitive: 

2053 value = "< hidden >" 

2054 else: 

2055 pass 

2056 if value is None: 

2057 file.write(f"# {option} = \n") 

2058 else: 

2059 if comment_out_everything: 

2060 value_lines = value.splitlines() 

2061 value = "\n# ".join(value_lines) 

2062 file.write(f"# {option} = {value}\n") 

2063 else: 

2064 if "\n" in value: 

2065 try: 

2066 value = json.dumps(json.loads(value), indent=4) 

2067 value = value.replace( 

2068 "\n", "\n " 

2069 ) # indent multi-line JSON to satisfy configparser format 

2070 except JSONDecodeError: 

2071 pass 

2072 file.write(f"{option} = {value}\n") 

2073 if needs_separation: 

2074 file.write("\n") 

2075 

2076 def write( # type: ignore[override] 

2077 self, 

2078 file: IO[str], 

2079 section: str | None = None, 

2080 include_examples: bool = True, 

2081 include_descriptions: bool = True, 

2082 include_sources: bool = True, 

2083 include_env_vars: bool = True, 

2084 include_providers: bool = True, 

2085 comment_out_everything: bool = False, 

2086 hide_sensitive: bool = False, 

2087 extra_spacing: bool = True, 

2088 only_defaults: bool = False, 

2089 show_values: bool = False, 

2090 **kwargs: Any, 

2091 ) -> None: 

2092 """ 

2093 Write configuration with comments and examples to a file. 

2094 

2095 :param file: file to write to 

2096 :param section: section of the config to write, defaults to all sections 

2097 :param include_examples: Include examples in the output 

2098 :param include_descriptions: Include descriptions in the output 

2099 :param include_sources: Include the source of each config option 

2100 :param include_env_vars: Include environment variables corresponding to each config option 

2101 :param include_providers: Include providers configuration 

2102 :param comment_out_everything: Comment out all values 

2103 :param hide_sensitive_values: Include sensitive values in the output 

2104 :param extra_spacing: Add extra spacing before examples and after variables 

2105 :param only_defaults: Only include default values when writing the config, not the actual values 

2106 """ 

2107 with self.make_sure_configuration_loaded(with_providers=include_providers): 

2108 sources_dict = {} 

2109 if include_sources: 

2110 sources_dict = self.as_dict(display_source=True) 

2111 for section_to_write in self.get_sections_including_defaults(): 

2112 section_config_description = self.configuration_description.get(section_to_write, {}) 

2113 if section_to_write != section and section is not None: 

2114 continue 

2115 if self._has_section_in_any_defaults(section_to_write) or self.has_section(section_to_write): 

2116 self._write_section_header( 

2117 file, include_descriptions, section_config_description, section_to_write 

2118 ) 

2119 for option in self.get_options_including_defaults(section_to_write): 

2120 should_continue, needs_separation = self._write_option_header( 

2121 file=file, 

2122 option=option, 

2123 extra_spacing=extra_spacing, 

2124 include_descriptions=include_descriptions, 

2125 include_env_vars=include_env_vars, 

2126 include_examples=include_examples, 

2127 include_sources=include_sources, 

2128 section_config_description=section_config_description, 

2129 section_to_write=section_to_write, 

2130 sources_dict=sources_dict, 

2131 ) 

2132 is_sensitive = self.is_sensitive_option(section_to_write, option) 

2133 self._write_value( 

2134 file=file, 

2135 option=option, 

2136 comment_out_everything=comment_out_everything, 

2137 needs_separation=needs_separation, 

2138 only_defaults=only_defaults, 

2139 section_to_write=section_to_write, 

2140 hide_sensitive=hide_sensitive, 

2141 is_sensitive=is_sensitive, 

2142 show_values=show_values, 

2143 ) 

2144 if include_descriptions and not needs_separation: 

2145 # extra separation between sections in case last option did not need it 

2146 file.write("\n") 

2147 

2148 @contextmanager 

2149 def make_sure_configuration_loaded(self, with_providers: bool) -> Generator[None, None, None]: 

2150 """ 

2151 Make sure configuration is loaded with or without providers. 

2152 

2153 The context manager will only toggle the `self._use_providers_configuration` flag if `with_providers` is False, and will reset `self._use_providers_configuration` to True after the context block. 

2154 Nop for `with_providers=True` as the configuration already loads providers configuration by default. 

2155 

2156 :param with_providers: whether providers should be loaded 

2157 """ 

2158 if not with_providers: 

2159 self._use_providers_configuration = False 

2160 # Only invalidate cached properties that depend on _use_providers_configuration. 

2161 # Do NOT use invalidate_cache() here — it would also evict expensive provider-discovery 

2162 # caches (_provider_metadata_configuration_description, _provider_metadata_config_fallback_default_values) 

2163 # that don't depend on this flag. 

2164 self._invalidate_provider_flag_caches() 

2165 try: 

2166 yield 

2167 finally: 

2168 if not with_providers: 

2169 self._use_providers_configuration = True 

2170 self._invalidate_provider_flag_caches()