main.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. from __future__ import annotations as _annotations
  2. import asyncio
  3. import inspect
  4. import threading
  5. from argparse import Namespace
  6. from types import SimpleNamespace
  7. from typing import Any, ClassVar, TypeVar
  8. from pydantic import ConfigDict
  9. from pydantic._internal._config import config_keys
  10. from pydantic._internal._signature import _field_name_for_signature
  11. from pydantic._internal._utils import deep_update, is_model_class
  12. from pydantic.dataclasses import is_pydantic_dataclass
  13. from pydantic.main import BaseModel
  14. from .exceptions import SettingsError
  15. from .sources import (
  16. ENV_FILE_SENTINEL,
  17. CliSettingsSource,
  18. DefaultSettingsSource,
  19. DotEnvSettingsSource,
  20. DotenvType,
  21. EnvSettingsSource,
  22. InitSettingsSource,
  23. PathType,
  24. PydanticBaseSettingsSource,
  25. PydanticModel,
  26. SecretsSettingsSource,
  27. get_subcommand,
  28. )
  29. T = TypeVar('T')
  30. class SettingsConfigDict(ConfigDict, total=False):
  31. case_sensitive: bool
  32. nested_model_default_partial_update: bool | None
  33. env_prefix: str
  34. env_file: DotenvType | None
  35. env_file_encoding: str | None
  36. env_ignore_empty: bool
  37. env_nested_delimiter: str | None
  38. env_nested_max_split: int | None
  39. env_parse_none_str: str | None
  40. env_parse_enums: bool | None
  41. cli_prog_name: str | None
  42. cli_parse_args: bool | list[str] | tuple[str, ...] | None
  43. cli_parse_none_str: str | None
  44. cli_hide_none_type: bool
  45. cli_avoid_json: bool
  46. cli_enforce_required: bool
  47. cli_use_class_docs_for_groups: bool
  48. cli_exit_on_error: bool
  49. cli_prefix: str
  50. cli_flag_prefix_char: str
  51. cli_implicit_flags: bool | None
  52. cli_ignore_unknown_args: bool | None
  53. cli_kebab_case: bool | None
  54. secrets_dir: PathType | None
  55. json_file: PathType | None
  56. json_file_encoding: str | None
  57. yaml_file: PathType | None
  58. yaml_file_encoding: str | None
  59. pyproject_toml_depth: int
  60. """
  61. Number of levels **up** from the current working directory to attempt to find a pyproject.toml
  62. file.
  63. This is only used when a pyproject.toml file is not found in the current working directory.
  64. """
  65. pyproject_toml_table_header: tuple[str, ...]
  66. """
  67. Header of the TOML table within a pyproject.toml file to use when filling variables.
  68. This is supplied as a `tuple[str, ...]` instead of a `str` to accommodate for headers
  69. containing a `.`.
  70. For example, `toml_table_header = ("tool", "my.tool", "foo")` can be used to fill variable
  71. values from a table with header `[tool."my.tool".foo]`.
  72. To use the root table, exclude this config setting or provide an empty tuple.
  73. """
  74. toml_file: PathType | None
  75. enable_decoding: bool
  76. # Extend `config_keys` by pydantic settings config keys to
  77. # support setting config through class kwargs.
  78. # Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model`
  79. # to extract config keys from model kwargs, So, by adding pydantic settings keys to
  80. # `config_keys`, they will be considered as valid config keys and will be collected
  81. # by Pydantic.
  82. config_keys |= set(SettingsConfigDict.__annotations__.keys())
  83. class BaseSettings(BaseModel):
  84. """
  85. Base class for settings, allowing values to be overridden by environment variables.
  86. This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose),
  87. Heroku and any 12 factor app design.
  88. All the below attributes can be set via `model_config`.
  89. Args:
  90. _case_sensitive: Whether environment and CLI variable names should be read with case-sensitivity.
  91. Defaults to `None`.
  92. _nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields.
  93. Defaults to `False`.
  94. _env_prefix: Prefix for all environment variables. Defaults to `None`.
  95. _env_file: The env file(s) to load settings values from. Defaults to `Path('')`, which
  96. means that the value from `model_config['env_file']` should be used. You can also pass
  97. `None` to indicate that environment variables should not be loaded from an env file.
  98. _env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`.
  99. _env_ignore_empty: Ignore environment variables where the value is an empty string. Default to `False`.
  100. _env_nested_delimiter: The nested env values delimiter. Defaults to `None`.
  101. _env_nested_max_split: The nested env values maximum nesting. Defaults to `None`, which means no limit.
  102. _env_parse_none_str: The env string value that should be parsed (e.g. "null", "void", "None", etc.)
  103. into `None` type(None). Defaults to `None` type(None), which means no parsing should occur.
  104. _env_parse_enums: Parse enum field names to values. Defaults to `None.`, which means no parsing should occur.
  105. _cli_prog_name: The CLI program name to display in help text. Defaults to `None` if _cli_parse_args is `None`.
  106. Otherwise, defaults to sys.argv[0].
  107. _cli_parse_args: The list of CLI arguments to parse. Defaults to None.
  108. If set to `True`, defaults to sys.argv[1:].
  109. _cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to None.
  110. _cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into
  111. `None` type(None). Defaults to _env_parse_none_str value if set. Otherwise, defaults to "null" if
  112. _cli_avoid_json is `False`, and "None" if _cli_avoid_json is `True`.
  113. _cli_hide_none_type: Hide `None` values in CLI help text. Defaults to `False`.
  114. _cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to `False`.
  115. _cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`.
  116. _cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions.
  117. Defaults to `False`.
  118. _cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs.
  119. Defaults to `True`.
  120. _cli_prefix: The root parser command line arguments prefix. Defaults to "".
  121. _cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'.
  122. _cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
  123. (e.g. --flag, --no-flag). Defaults to `False`.
  124. _cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
  125. _cli_kebab_case: CLI args use kebab case. Defaults to `False`.
  126. _secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`.
  127. """
  128. def __init__(
  129. __pydantic_self__,
  130. _case_sensitive: bool | None = None,
  131. _nested_model_default_partial_update: bool | None = None,
  132. _env_prefix: str | None = None,
  133. _env_file: DotenvType | None = ENV_FILE_SENTINEL,
  134. _env_file_encoding: str | None = None,
  135. _env_ignore_empty: bool | None = None,
  136. _env_nested_delimiter: str | None = None,
  137. _env_nested_max_split: int | None = None,
  138. _env_parse_none_str: str | None = None,
  139. _env_parse_enums: bool | None = None,
  140. _cli_prog_name: str | None = None,
  141. _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None,
  142. _cli_settings_source: CliSettingsSource[Any] | None = None,
  143. _cli_parse_none_str: str | None = None,
  144. _cli_hide_none_type: bool | None = None,
  145. _cli_avoid_json: bool | None = None,
  146. _cli_enforce_required: bool | None = None,
  147. _cli_use_class_docs_for_groups: bool | None = None,
  148. _cli_exit_on_error: bool | None = None,
  149. _cli_prefix: str | None = None,
  150. _cli_flag_prefix_char: str | None = None,
  151. _cli_implicit_flags: bool | None = None,
  152. _cli_ignore_unknown_args: bool | None = None,
  153. _cli_kebab_case: bool | None = None,
  154. _secrets_dir: PathType | None = None,
  155. **values: Any,
  156. ) -> None:
  157. super().__init__(
  158. **__pydantic_self__._settings_build_values(
  159. values,
  160. _case_sensitive=_case_sensitive,
  161. _nested_model_default_partial_update=_nested_model_default_partial_update,
  162. _env_prefix=_env_prefix,
  163. _env_file=_env_file,
  164. _env_file_encoding=_env_file_encoding,
  165. _env_ignore_empty=_env_ignore_empty,
  166. _env_nested_delimiter=_env_nested_delimiter,
  167. _env_nested_max_split=_env_nested_max_split,
  168. _env_parse_none_str=_env_parse_none_str,
  169. _env_parse_enums=_env_parse_enums,
  170. _cli_prog_name=_cli_prog_name,
  171. _cli_parse_args=_cli_parse_args,
  172. _cli_settings_source=_cli_settings_source,
  173. _cli_parse_none_str=_cli_parse_none_str,
  174. _cli_hide_none_type=_cli_hide_none_type,
  175. _cli_avoid_json=_cli_avoid_json,
  176. _cli_enforce_required=_cli_enforce_required,
  177. _cli_use_class_docs_for_groups=_cli_use_class_docs_for_groups,
  178. _cli_exit_on_error=_cli_exit_on_error,
  179. _cli_prefix=_cli_prefix,
  180. _cli_flag_prefix_char=_cli_flag_prefix_char,
  181. _cli_implicit_flags=_cli_implicit_flags,
  182. _cli_ignore_unknown_args=_cli_ignore_unknown_args,
  183. _cli_kebab_case=_cli_kebab_case,
  184. _secrets_dir=_secrets_dir,
  185. )
  186. )
  187. @classmethod
  188. def settings_customise_sources(
  189. cls,
  190. settings_cls: type[BaseSettings],
  191. init_settings: PydanticBaseSettingsSource,
  192. env_settings: PydanticBaseSettingsSource,
  193. dotenv_settings: PydanticBaseSettingsSource,
  194. file_secret_settings: PydanticBaseSettingsSource,
  195. ) -> tuple[PydanticBaseSettingsSource, ...]:
  196. """
  197. Define the sources and their order for loading the settings values.
  198. Args:
  199. settings_cls: The Settings class.
  200. init_settings: The `InitSettingsSource` instance.
  201. env_settings: The `EnvSettingsSource` instance.
  202. dotenv_settings: The `DotEnvSettingsSource` instance.
  203. file_secret_settings: The `SecretsSettingsSource` instance.
  204. Returns:
  205. A tuple containing the sources and their order for loading the settings values.
  206. """
  207. return init_settings, env_settings, dotenv_settings, file_secret_settings
  208. def _settings_build_values(
  209. self,
  210. init_kwargs: dict[str, Any],
  211. _case_sensitive: bool | None = None,
  212. _nested_model_default_partial_update: bool | None = None,
  213. _env_prefix: str | None = None,
  214. _env_file: DotenvType | None = None,
  215. _env_file_encoding: str | None = None,
  216. _env_ignore_empty: bool | None = None,
  217. _env_nested_delimiter: str | None = None,
  218. _env_nested_max_split: int | None = None,
  219. _env_parse_none_str: str | None = None,
  220. _env_parse_enums: bool | None = None,
  221. _cli_prog_name: str | None = None,
  222. _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None,
  223. _cli_settings_source: CliSettingsSource[Any] | None = None,
  224. _cli_parse_none_str: str | None = None,
  225. _cli_hide_none_type: bool | None = None,
  226. _cli_avoid_json: bool | None = None,
  227. _cli_enforce_required: bool | None = None,
  228. _cli_use_class_docs_for_groups: bool | None = None,
  229. _cli_exit_on_error: bool | None = None,
  230. _cli_prefix: str | None = None,
  231. _cli_flag_prefix_char: str | None = None,
  232. _cli_implicit_flags: bool | None = None,
  233. _cli_ignore_unknown_args: bool | None = None,
  234. _cli_kebab_case: bool | None = None,
  235. _secrets_dir: PathType | None = None,
  236. ) -> dict[str, Any]:
  237. # Determine settings config values
  238. case_sensitive = _case_sensitive if _case_sensitive is not None else self.model_config.get('case_sensitive')
  239. env_prefix = _env_prefix if _env_prefix is not None else self.model_config.get('env_prefix')
  240. nested_model_default_partial_update = (
  241. _nested_model_default_partial_update
  242. if _nested_model_default_partial_update is not None
  243. else self.model_config.get('nested_model_default_partial_update')
  244. )
  245. env_file = _env_file if _env_file != ENV_FILE_SENTINEL else self.model_config.get('env_file')
  246. env_file_encoding = (
  247. _env_file_encoding if _env_file_encoding is not None else self.model_config.get('env_file_encoding')
  248. )
  249. env_ignore_empty = (
  250. _env_ignore_empty if _env_ignore_empty is not None else self.model_config.get('env_ignore_empty')
  251. )
  252. env_nested_delimiter = (
  253. _env_nested_delimiter
  254. if _env_nested_delimiter is not None
  255. else self.model_config.get('env_nested_delimiter')
  256. )
  257. env_nested_max_split = (
  258. _env_nested_max_split
  259. if _env_nested_max_split is not None
  260. else self.model_config.get('env_nested_max_split')
  261. )
  262. env_parse_none_str = (
  263. _env_parse_none_str if _env_parse_none_str is not None else self.model_config.get('env_parse_none_str')
  264. )
  265. env_parse_enums = _env_parse_enums if _env_parse_enums is not None else self.model_config.get('env_parse_enums')
  266. cli_prog_name = _cli_prog_name if _cli_prog_name is not None else self.model_config.get('cli_prog_name')
  267. cli_parse_args = _cli_parse_args if _cli_parse_args is not None else self.model_config.get('cli_parse_args')
  268. cli_settings_source = (
  269. _cli_settings_source if _cli_settings_source is not None else self.model_config.get('cli_settings_source')
  270. )
  271. cli_parse_none_str = (
  272. _cli_parse_none_str if _cli_parse_none_str is not None else self.model_config.get('cli_parse_none_str')
  273. )
  274. cli_parse_none_str = cli_parse_none_str if not env_parse_none_str else env_parse_none_str
  275. cli_hide_none_type = (
  276. _cli_hide_none_type if _cli_hide_none_type is not None else self.model_config.get('cli_hide_none_type')
  277. )
  278. cli_avoid_json = _cli_avoid_json if _cli_avoid_json is not None else self.model_config.get('cli_avoid_json')
  279. cli_enforce_required = (
  280. _cli_enforce_required
  281. if _cli_enforce_required is not None
  282. else self.model_config.get('cli_enforce_required')
  283. )
  284. cli_use_class_docs_for_groups = (
  285. _cli_use_class_docs_for_groups
  286. if _cli_use_class_docs_for_groups is not None
  287. else self.model_config.get('cli_use_class_docs_for_groups')
  288. )
  289. cli_exit_on_error = (
  290. _cli_exit_on_error if _cli_exit_on_error is not None else self.model_config.get('cli_exit_on_error')
  291. )
  292. cli_prefix = _cli_prefix if _cli_prefix is not None else self.model_config.get('cli_prefix')
  293. cli_flag_prefix_char = (
  294. _cli_flag_prefix_char
  295. if _cli_flag_prefix_char is not None
  296. else self.model_config.get('cli_flag_prefix_char')
  297. )
  298. cli_implicit_flags = (
  299. _cli_implicit_flags if _cli_implicit_flags is not None else self.model_config.get('cli_implicit_flags')
  300. )
  301. cli_ignore_unknown_args = (
  302. _cli_ignore_unknown_args
  303. if _cli_ignore_unknown_args is not None
  304. else self.model_config.get('cli_ignore_unknown_args')
  305. )
  306. cli_kebab_case = _cli_kebab_case if _cli_kebab_case is not None else self.model_config.get('cli_kebab_case')
  307. secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
  308. # Configure built-in sources
  309. default_settings = DefaultSettingsSource(
  310. self.__class__, nested_model_default_partial_update=nested_model_default_partial_update
  311. )
  312. init_settings = InitSettingsSource(
  313. self.__class__,
  314. init_kwargs=init_kwargs,
  315. nested_model_default_partial_update=nested_model_default_partial_update,
  316. )
  317. env_settings = EnvSettingsSource(
  318. self.__class__,
  319. case_sensitive=case_sensitive,
  320. env_prefix=env_prefix,
  321. env_nested_delimiter=env_nested_delimiter,
  322. env_nested_max_split=env_nested_max_split,
  323. env_ignore_empty=env_ignore_empty,
  324. env_parse_none_str=env_parse_none_str,
  325. env_parse_enums=env_parse_enums,
  326. )
  327. dotenv_settings = DotEnvSettingsSource(
  328. self.__class__,
  329. env_file=env_file,
  330. env_file_encoding=env_file_encoding,
  331. case_sensitive=case_sensitive,
  332. env_prefix=env_prefix,
  333. env_nested_delimiter=env_nested_delimiter,
  334. env_nested_max_split=env_nested_max_split,
  335. env_ignore_empty=env_ignore_empty,
  336. env_parse_none_str=env_parse_none_str,
  337. env_parse_enums=env_parse_enums,
  338. )
  339. file_secret_settings = SecretsSettingsSource(
  340. self.__class__, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix
  341. )
  342. # Provide a hook to set built-in sources priority and add / remove sources
  343. sources = self.settings_customise_sources(
  344. self.__class__,
  345. init_settings=init_settings,
  346. env_settings=env_settings,
  347. dotenv_settings=dotenv_settings,
  348. file_secret_settings=file_secret_settings,
  349. ) + (default_settings,)
  350. if not any([source for source in sources if isinstance(source, CliSettingsSource)]):
  351. if isinstance(cli_settings_source, CliSettingsSource):
  352. sources = (cli_settings_source,) + sources
  353. elif cli_parse_args is not None:
  354. cli_settings = CliSettingsSource[Any](
  355. self.__class__,
  356. cli_prog_name=cli_prog_name,
  357. cli_parse_args=cli_parse_args,
  358. cli_parse_none_str=cli_parse_none_str,
  359. cli_hide_none_type=cli_hide_none_type,
  360. cli_avoid_json=cli_avoid_json,
  361. cli_enforce_required=cli_enforce_required,
  362. cli_use_class_docs_for_groups=cli_use_class_docs_for_groups,
  363. cli_exit_on_error=cli_exit_on_error,
  364. cli_prefix=cli_prefix,
  365. cli_flag_prefix_char=cli_flag_prefix_char,
  366. cli_implicit_flags=cli_implicit_flags,
  367. cli_ignore_unknown_args=cli_ignore_unknown_args,
  368. cli_kebab_case=cli_kebab_case,
  369. case_sensitive=case_sensitive,
  370. )
  371. sources = (cli_settings,) + sources
  372. if sources:
  373. state: dict[str, Any] = {}
  374. states: dict[str, dict[str, Any]] = {}
  375. for source in sources:
  376. if isinstance(source, PydanticBaseSettingsSource):
  377. source._set_current_state(state)
  378. source._set_settings_sources_data(states)
  379. source_name = source.__name__ if hasattr(source, '__name__') else type(source).__name__
  380. source_state = source()
  381. states[source_name] = source_state
  382. state = deep_update(source_state, state)
  383. return state
  384. else:
  385. # no one should mean to do this, but I think returning an empty dict is marginally preferable
  386. # to an informative error and much better than a confusing error
  387. return {}
  388. model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
  389. extra='forbid',
  390. arbitrary_types_allowed=True,
  391. validate_default=True,
  392. case_sensitive=False,
  393. env_prefix='',
  394. nested_model_default_partial_update=False,
  395. env_file=None,
  396. env_file_encoding=None,
  397. env_ignore_empty=False,
  398. env_nested_delimiter=None,
  399. env_nested_max_split=None,
  400. env_parse_none_str=None,
  401. env_parse_enums=None,
  402. cli_prog_name=None,
  403. cli_parse_args=None,
  404. cli_parse_none_str=None,
  405. cli_hide_none_type=False,
  406. cli_avoid_json=False,
  407. cli_enforce_required=False,
  408. cli_use_class_docs_for_groups=False,
  409. cli_exit_on_error=True,
  410. cli_prefix='',
  411. cli_flag_prefix_char='-',
  412. cli_implicit_flags=False,
  413. cli_ignore_unknown_args=False,
  414. cli_kebab_case=False,
  415. json_file=None,
  416. json_file_encoding=None,
  417. yaml_file=None,
  418. yaml_file_encoding=None,
  419. toml_file=None,
  420. secrets_dir=None,
  421. protected_namespaces=('model_validate', 'model_dump', 'settings_customise_sources'),
  422. enable_decoding=True,
  423. )
  424. class CliApp:
  425. """
  426. A utility class for running Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as
  427. CLI applications.
  428. """
  429. @staticmethod
  430. def _run_cli_cmd(model: Any, cli_cmd_method_name: str, is_required: bool) -> Any:
  431. command = getattr(type(model), cli_cmd_method_name, None)
  432. if command is None:
  433. if is_required:
  434. raise SettingsError(f'Error: {type(model).__name__} class is missing {cli_cmd_method_name} entrypoint')
  435. return model
  436. # If the method is asynchronous, we handle its execution based on the current event loop status.
  437. if inspect.iscoroutinefunction(command):
  438. # For asynchronous methods, we have two execution scenarios:
  439. # 1. If no event loop is running in the current thread, run the coroutine directly with asyncio.run().
  440. # 2. If an event loop is already running in the current thread, run the coroutine in a separate thread to avoid conflicts.
  441. try:
  442. # Check if an event loop is currently running in this thread.
  443. loop = asyncio.get_running_loop()
  444. except RuntimeError:
  445. loop = None
  446. if loop and loop.is_running():
  447. # We're in a context with an active event loop (e.g., Jupyter Notebook).
  448. # Running asyncio.run() here would cause conflicts, so we use a separate thread.
  449. exception_container = []
  450. def run_coro() -> None:
  451. try:
  452. # Execute the coroutine in a new event loop in this separate thread.
  453. asyncio.run(command(model))
  454. except Exception as e:
  455. exception_container.append(e)
  456. thread = threading.Thread(target=run_coro)
  457. thread.start()
  458. thread.join()
  459. if exception_container:
  460. # Propagate exceptions from the separate thread.
  461. raise exception_container[0]
  462. else:
  463. # No event loop is running; safe to run the coroutine directly.
  464. asyncio.run(command(model))
  465. else:
  466. # For synchronous methods, call them directly.
  467. command(model)
  468. return model
  469. @staticmethod
  470. def run(
  471. model_cls: type[T],
  472. cli_args: list[str] | Namespace | SimpleNamespace | dict[str, Any] | None = None,
  473. cli_settings_source: CliSettingsSource[Any] | None = None,
  474. cli_exit_on_error: bool | None = None,
  475. cli_cmd_method_name: str = 'cli_cmd',
  476. **model_init_data: Any,
  477. ) -> T:
  478. """
  479. Runs a Pydantic `BaseSettings`, `BaseModel`, or `pydantic.dataclasses.dataclass` as a CLI application.
  480. Running a model as a CLI application requires the `cli_cmd` method to be defined in the model class.
  481. Args:
  482. model_cls: The model class to run as a CLI application.
  483. cli_args: The list of CLI arguments to parse. If `cli_settings_source` is specified, this may
  484. also be a namespace or dictionary of pre-parsed CLI arguments. Defaults to `sys.argv[1:]`.
  485. cli_settings_source: Override the default CLI settings source with a user defined instance.
  486. Defaults to `None`.
  487. cli_exit_on_error: Determines whether this function exits on error. If model is subclass of
  488. `BaseSettings`, defaults to BaseSettings `cli_exit_on_error` value. Otherwise, defaults to
  489. `True`.
  490. cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd".
  491. model_init_data: The model init data.
  492. Returns:
  493. The ran instance of model.
  494. Raises:
  495. SettingsError: If model_cls is not subclass of `BaseModel` or `pydantic.dataclasses.dataclass`.
  496. SettingsError: If model_cls does not have a `cli_cmd` entrypoint defined.
  497. """
  498. if not (is_pydantic_dataclass(model_cls) or is_model_class(model_cls)):
  499. raise SettingsError(
  500. f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass'
  501. )
  502. cli_settings = None
  503. cli_parse_args = True if cli_args is None else cli_args
  504. if cli_settings_source is not None:
  505. if isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)):
  506. cli_settings = cli_settings_source(parsed_args=cli_parse_args)
  507. else:
  508. cli_settings = cli_settings_source(args=cli_parse_args)
  509. elif isinstance(cli_parse_args, (Namespace, SimpleNamespace, dict)):
  510. raise SettingsError('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used')
  511. model_init_data['_cli_parse_args'] = cli_parse_args
  512. model_init_data['_cli_exit_on_error'] = cli_exit_on_error
  513. model_init_data['_cli_settings_source'] = cli_settings
  514. if not issubclass(model_cls, BaseSettings):
  515. class CliAppBaseSettings(BaseSettings, model_cls): # type: ignore
  516. __doc__ = model_cls.__doc__
  517. model_config = SettingsConfigDict(
  518. nested_model_default_partial_update=True,
  519. case_sensitive=True,
  520. cli_hide_none_type=True,
  521. cli_avoid_json=True,
  522. cli_enforce_required=True,
  523. cli_implicit_flags=True,
  524. cli_kebab_case=True,
  525. )
  526. model = CliAppBaseSettings(**model_init_data)
  527. model_init_data = {}
  528. for field_name, field_info in type(model).model_fields.items():
  529. model_init_data[_field_name_for_signature(field_name, field_info)] = getattr(model, field_name)
  530. return CliApp._run_cli_cmd(model_cls(**model_init_data), cli_cmd_method_name, is_required=False)
  531. @staticmethod
  532. def run_subcommand(
  533. model: PydanticModel, cli_exit_on_error: bool | None = None, cli_cmd_method_name: str = 'cli_cmd'
  534. ) -> PydanticModel:
  535. """
  536. Runs the model subcommand. Running a model subcommand requires the `cli_cmd` method to be defined in
  537. the nested model subcommand class.
  538. Args:
  539. model: The model to run the subcommand from.
  540. cli_exit_on_error: Determines whether this function exits with error if no subcommand is found.
  541. Defaults to model_config `cli_exit_on_error` value if set. Otherwise, defaults to `True`.
  542. cli_cmd_method_name: The CLI command method name to run. Defaults to "cli_cmd".
  543. Returns:
  544. The ran subcommand model.
  545. Raises:
  546. SystemExit: When no subcommand is found and cli_exit_on_error=`True` (the default).
  547. SettingsError: When no subcommand is found and cli_exit_on_error=`False`.
  548. """
  549. subcommand = get_subcommand(model, is_required=True, cli_exit_on_error=cli_exit_on_error)
  550. return CliApp._run_cli_cmd(subcommand, cli_cmd_method_name, is_required=True)