fred-pod 4.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: fred-pod
3
+ Version: 4.1.0
4
+ Summary: What a Fred component needs to be a pod: configuration, identity, naming.
5
+ Author-email: Thales <noreply@thalesgroup.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://site.fredlab.dev
8
+ Project-URL: Repository, https://github.com/ThalesGroup/fred
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pydantic<3.0.0,>=2.5.2
16
+ Requires-Dist: python-dotenv<2.0.0,>=1.0.1
17
+ Requires-Dist: pyyaml>=6.0.1
18
+ Requires-Dist: httpx>=0.28.1
19
+ Provides-Extra: dev
20
+ Requires-Dist: bandit>=1.8.6; extra == "dev"
21
+ Requires-Dist: basedpyright==1.31.0; extra == "dev"
22
+ Requires-Dist: detect-secrets>=1.5.0; extra == "dev"
23
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
26
+ Requires-Dist: pytest-socket>=0.7.0; extra == "dev"
27
+ Requires-Dist: ruff<0.16,>=0.12.5; extra == "dev"
28
+
29
+ # fred-pod
30
+
31
+ What a Fred component needs to *be* a pod: **configuration, identity, naming**.
32
+
33
+ A Knowledge Base pod, a capability pod, an MCP server pod and an agent pod all read a
34
+ `configuration.yaml`, get a machine-to-machine token, and name things under a prefix they
35
+ own. That floor is this distribution.
36
+
37
+ | Question | Answer |
38
+ | --- | --- |
39
+ | What do I need to *run* a Fred component? | `fred-pod` |
40
+ | What do I need to *build an agent*? | `fred-core` / `fred-sdk` |
41
+
42
+ ## Why it is its own distribution
43
+
44
+ `fred-core` declares 31 runtime dependencies — pandas, pyarrow, google-cloud-storage, minio,
45
+ opensearch-py, sqlalchemy, asyncpg, azure-identity, fastapi, and a client for every LLM
46
+ provider. That is the right list for the agents/LLM platform `fred-core` is. It is the wrong
47
+ list for a pod whose job is one PROPFIND and a few GETs.
48
+
49
+ Extras were the obvious alternative and were rejected for one reason: **a boundary the build
50
+ does not enforce erodes.** `fred-core` reached 31 dependencies precisely because nothing
51
+ stopped it. Nothing would stop an `import pandas` landing in `structures.py` next month
52
+ either, and nobody would notice. A separate distribution cannot import what it does not
53
+ depend on — the rule keeps itself.
54
+
55
+ It showed its worth immediately: `config_loader.py` has always done `import yaml`, and
56
+ `fred-core` never declared PyYAML. It worked because something else happened to pull it in.
57
+ Here it is declared, because here it had to be.
58
+
59
+ ## The dependency list is the contract
60
+
61
+ ```
62
+ pydantic python-dotenv pyyaml httpx
63
+ ```
64
+
65
+ Four packages. Adding a fifth is a decision someone makes on purpose, in a diff, and that is
66
+ the whole point. If a change to this library needs a heavier import, the change belongs in
67
+ `fred-core`, not here.
68
+
69
+ ## Layout
70
+
71
+ ```
72
+ fred_pod/
73
+ ├── common/
74
+ │ ├── naming.py contributed names, prefixes, catalog ids
75
+ │ ├── structures.py configuration models (scheduler, stores, KPI sinks)
76
+ │ ├── config_files.py resolving configuration.yaml and its .env
77
+ │ └── config_loader.py loading and validating it into a Pydantic model
78
+ └── security/
79
+ ├── structure.py security configuration models, KeycloakUser
80
+ └── backend_to_backend_auth.py M2M token provider and httpx auth
81
+ ```
82
+
83
+ The paths mirror `fred_core`'s on purpose: a call site migrates by swapping the prefix
84
+ `fred_core.` → `fred_pod.` and nothing else.
85
+
86
+ ## Compatibility
87
+
88
+ `fred-core` depends on `fred-pod` and re-exports every name it moved, at both the package
89
+ top level and the original submodule paths. Existing code keeps working unchanged; imports
90
+ migrate opportunistically.
91
+
92
+ ```python
93
+ from fred_pod import ConfigFiles, KeycloakUser, M2MTokenProvider
94
+ from fred_pod.common.naming import require_contributed_name
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ```
100
+ make dev # install with the local monorepo checkout
101
+ make test # offline unit tests
102
+ make code-quality # ruff, bandit, detect-secrets, basedpyright
103
+ make publish # build and upload to PyPI (needs PYPI_TOKEN)
104
+ ```
@@ -0,0 +1,76 @@
1
+ # fred-pod
2
+
3
+ What a Fred component needs to *be* a pod: **configuration, identity, naming**.
4
+
5
+ A Knowledge Base pod, a capability pod, an MCP server pod and an agent pod all read a
6
+ `configuration.yaml`, get a machine-to-machine token, and name things under a prefix they
7
+ own. That floor is this distribution.
8
+
9
+ | Question | Answer |
10
+ | --- | --- |
11
+ | What do I need to *run* a Fred component? | `fred-pod` |
12
+ | What do I need to *build an agent*? | `fred-core` / `fred-sdk` |
13
+
14
+ ## Why it is its own distribution
15
+
16
+ `fred-core` declares 31 runtime dependencies — pandas, pyarrow, google-cloud-storage, minio,
17
+ opensearch-py, sqlalchemy, asyncpg, azure-identity, fastapi, and a client for every LLM
18
+ provider. That is the right list for the agents/LLM platform `fred-core` is. It is the wrong
19
+ list for a pod whose job is one PROPFIND and a few GETs.
20
+
21
+ Extras were the obvious alternative and were rejected for one reason: **a boundary the build
22
+ does not enforce erodes.** `fred-core` reached 31 dependencies precisely because nothing
23
+ stopped it. Nothing would stop an `import pandas` landing in `structures.py` next month
24
+ either, and nobody would notice. A separate distribution cannot import what it does not
25
+ depend on — the rule keeps itself.
26
+
27
+ It showed its worth immediately: `config_loader.py` has always done `import yaml`, and
28
+ `fred-core` never declared PyYAML. It worked because something else happened to pull it in.
29
+ Here it is declared, because here it had to be.
30
+
31
+ ## The dependency list is the contract
32
+
33
+ ```
34
+ pydantic python-dotenv pyyaml httpx
35
+ ```
36
+
37
+ Four packages. Adding a fifth is a decision someone makes on purpose, in a diff, and that is
38
+ the whole point. If a change to this library needs a heavier import, the change belongs in
39
+ `fred-core`, not here.
40
+
41
+ ## Layout
42
+
43
+ ```
44
+ fred_pod/
45
+ ├── common/
46
+ │ ├── naming.py contributed names, prefixes, catalog ids
47
+ │ ├── structures.py configuration models (scheduler, stores, KPI sinks)
48
+ │ ├── config_files.py resolving configuration.yaml and its .env
49
+ │ └── config_loader.py loading and validating it into a Pydantic model
50
+ └── security/
51
+ ├── structure.py security configuration models, KeycloakUser
52
+ └── backend_to_backend_auth.py M2M token provider and httpx auth
53
+ ```
54
+
55
+ The paths mirror `fred_core`'s on purpose: a call site migrates by swapping the prefix
56
+ `fred_core.` → `fred_pod.` and nothing else.
57
+
58
+ ## Compatibility
59
+
60
+ `fred-core` depends on `fred-pod` and re-exports every name it moved, at both the package
61
+ top level and the original submodule paths. Existing code keeps working unchanged; imports
62
+ migrate opportunistically.
63
+
64
+ ```python
65
+ from fred_pod import ConfigFiles, KeycloakUser, M2MTokenProvider
66
+ from fred_pod.common.naming import require_contributed_name
67
+ ```
68
+
69
+ ## Development
70
+
71
+ ```
72
+ make dev # install with the local monorepo checkout
73
+ make test # offline unit tests
74
+ make code-quality # ruff, bandit, detect-secrets, basedpyright
75
+ make publish # build and upload to PyPI (needs PYPI_TOKEN)
76
+ ```
@@ -0,0 +1,135 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ What a Fred component needs to *be* a pod: configuration, identity, naming.
17
+
18
+ A Knowledge Base pod, a capability pod, an MCP server pod and an agent pod all
19
+ read a `configuration.yaml`, get a machine-to-machine token and name things
20
+ under a prefix they own. That floor is this distribution, and it depends on
21
+ four third-party packages — pydantic, PyYAML, python-dotenv, httpx.
22
+
23
+ "What do I need to run a Fred component?" → `fred-pod`.
24
+ "What do I need to build an agent?" → `fred-core` / `fred-sdk`.
25
+
26
+ Unlike `fred-core`, importing this package is cheap and stays cheap: a
27
+ separate distribution cannot import what it does not depend on, so the
28
+ boundary is enforced by the build rather than by discipline.
29
+ """
30
+
31
+ from fred_pod.common.config_files import ConfigFiles
32
+ from fred_pod.common.config_loader import (
33
+ TConfig,
34
+ get_config,
35
+ load_configuration_with_config_files,
36
+ parse_yaml_mapping_file,
37
+ )
38
+ from fred_pod.common.naming import (
39
+ CONTRIBUTED_NAME_PATTERN,
40
+ KNOWLEDGE_BASE_CATALOG_NAMESPACE_PREFIX,
41
+ MAX_NAME_CHARS,
42
+ PREFIX_PATTERN,
43
+ InvalidContributedName,
44
+ knowledge_base_catalog_id,
45
+ knowledge_base_name_from_catalog_id,
46
+ prefix_covers,
47
+ require_contributed_name,
48
+ )
49
+ from fred_pod.common.structures import (
50
+ BaseModelWithId,
51
+ DuckdbStoreConfig,
52
+ InMemoryStoreConfig,
53
+ KpiLogSinkConfig,
54
+ KpiObservabilityConfig,
55
+ KpiOpenSearchSinkConfig,
56
+ KpiPrometheusSinkConfig,
57
+ LogStoreConfig,
58
+ ModelConfiguration,
59
+ OpenSearchIndexConfig,
60
+ OpenSearchStoreConfig,
61
+ OwnerFilter,
62
+ PostgresStoreConfig,
63
+ PostgresTableConfig,
64
+ StoreConfig,
65
+ TemporalSchedulerConfig,
66
+ )
67
+ from fred_pod.security.backend_to_backend_auth import (
68
+ M2MAuthConfig,
69
+ M2MBearerAuth,
70
+ M2MTokenProvider,
71
+ make_m2m_asgi_client,
72
+ )
73
+ from fred_pod.security.structure import (
74
+ LOCAL_DEV_CLIENT_ID,
75
+ SERVICE_AGENT_ROLE,
76
+ KeycloakUser,
77
+ M2MSecurity,
78
+ OpenFgaRebacConfig,
79
+ RebacBaseConfig,
80
+ RebacConfiguration,
81
+ SecurityConfiguration,
82
+ UserSecurity,
83
+ is_service_agent,
84
+ )
85
+
86
+ __all__ = [
87
+ # Configuration
88
+ "ConfigFiles",
89
+ "TConfig",
90
+ "get_config",
91
+ "load_configuration_with_config_files",
92
+ "parse_yaml_mapping_file",
93
+ # Naming
94
+ "CONTRIBUTED_NAME_PATTERN",
95
+ "KNOWLEDGE_BASE_CATALOG_NAMESPACE_PREFIX",
96
+ "MAX_NAME_CHARS",
97
+ "PREFIX_PATTERN",
98
+ "InvalidContributedName",
99
+ "knowledge_base_catalog_id",
100
+ "knowledge_base_name_from_catalog_id",
101
+ "prefix_covers",
102
+ "require_contributed_name",
103
+ # Configuration models
104
+ "BaseModelWithId",
105
+ "DuckdbStoreConfig",
106
+ "InMemoryStoreConfig",
107
+ "KpiLogSinkConfig",
108
+ "KpiObservabilityConfig",
109
+ "KpiOpenSearchSinkConfig",
110
+ "KpiPrometheusSinkConfig",
111
+ "LogStoreConfig",
112
+ "ModelConfiguration",
113
+ "OpenSearchIndexConfig",
114
+ "OpenSearchStoreConfig",
115
+ "OwnerFilter",
116
+ "PostgresStoreConfig",
117
+ "PostgresTableConfig",
118
+ "StoreConfig",
119
+ "TemporalSchedulerConfig",
120
+ # Identity
121
+ "LOCAL_DEV_CLIENT_ID",
122
+ "SERVICE_AGENT_ROLE",
123
+ "KeycloakUser",
124
+ "M2MAuthConfig",
125
+ "M2MBearerAuth",
126
+ "M2MSecurity",
127
+ "M2MTokenProvider",
128
+ "OpenFgaRebacConfig",
129
+ "RebacBaseConfig",
130
+ "RebacConfiguration",
131
+ "SecurityConfiguration",
132
+ "UserSecurity",
133
+ "is_service_agent",
134
+ "make_m2m_asgi_client",
135
+ ]
@@ -0,0 +1,85 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Configuration, naming, and the models every component's configuration uses."""
16
+
17
+ from fred_pod.common.config_files import ConfigFiles
18
+ from fred_pod.common.config_loader import (
19
+ TConfig,
20
+ get_config,
21
+ load_configuration_with_config_files,
22
+ parse_yaml_mapping_file,
23
+ )
24
+ from fred_pod.common.naming import (
25
+ CONTRIBUTED_NAME_PATTERN,
26
+ KNOWLEDGE_BASE_CATALOG_NAMESPACE_PREFIX,
27
+ MAX_NAME_CHARS,
28
+ PREFIX_PATTERN,
29
+ InvalidContributedName,
30
+ knowledge_base_catalog_id,
31
+ knowledge_base_name_from_catalog_id,
32
+ prefix_covers,
33
+ require_contributed_name,
34
+ )
35
+ from fred_pod.common.structures import (
36
+ BaseModelWithId,
37
+ DuckdbStoreConfig,
38
+ InMemoryStoreConfig,
39
+ KpiLogSinkConfig,
40
+ KpiObservabilityConfig,
41
+ KpiOpenSearchSinkConfig,
42
+ KpiPrometheusSinkConfig,
43
+ LogStoreConfig,
44
+ ModelConfiguration,
45
+ OpenSearchIndexConfig,
46
+ OpenSearchStoreConfig,
47
+ OwnerFilter,
48
+ PostgresStoreConfig,
49
+ PostgresTableConfig,
50
+ StoreConfig,
51
+ TemporalSchedulerConfig,
52
+ )
53
+
54
+ __all__ = [
55
+ "CONTRIBUTED_NAME_PATTERN",
56
+ "KNOWLEDGE_BASE_CATALOG_NAMESPACE_PREFIX",
57
+ "MAX_NAME_CHARS",
58
+ "PREFIX_PATTERN",
59
+ "BaseModelWithId",
60
+ "ConfigFiles",
61
+ "DuckdbStoreConfig",
62
+ "InMemoryStoreConfig",
63
+ "InvalidContributedName",
64
+ "KpiLogSinkConfig",
65
+ "KpiObservabilityConfig",
66
+ "KpiOpenSearchSinkConfig",
67
+ "KpiPrometheusSinkConfig",
68
+ "LogStoreConfig",
69
+ "ModelConfiguration",
70
+ "OpenSearchIndexConfig",
71
+ "OpenSearchStoreConfig",
72
+ "OwnerFilter",
73
+ "PostgresStoreConfig",
74
+ "PostgresTableConfig",
75
+ "StoreConfig",
76
+ "TConfig",
77
+ "TemporalSchedulerConfig",
78
+ "get_config",
79
+ "knowledge_base_catalog_id",
80
+ "knowledge_base_name_from_catalog_id",
81
+ "load_configuration_with_config_files",
82
+ "parse_yaml_mapping_file",
83
+ "prefix_covers",
84
+ "require_contributed_name",
85
+ ]
@@ -0,0 +1,135 @@
1
+ # Copyright Thales 2025
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+
20
+ from dotenv import load_dotenv
21
+
22
+
23
+ class ConfigFiles:
24
+ """Resolve and track startup config paths for Fred backends.
25
+
26
+ Why this exists:
27
+ - Every backend starts the same way: load environment variables, then load a
28
+ YAML configuration file.
29
+ - Developers and operators should see the exact files that were used.
30
+
31
+ Example:
32
+ - `ENV_FILE=./config/.env.prod`
33
+ - `CONFIG_FILE=./config/configuration_prod.yaml`
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ logger: logging.Logger,
40
+ default_env_file: str = "./config/.env",
41
+ default_config_file: str = "./config/configuration.yaml",
42
+ env_var_name: str = "ENV_FILE",
43
+ config_var_name: str = "CONFIG_FILE",
44
+ log_prefix: str = "[CONFIG]",
45
+ ) -> None:
46
+ """Create a resolver with Fred defaults.
47
+
48
+ Example:
49
+ - Keep defaults for regular startup.
50
+ - Override `default_config_file` in tests to point to a fixture.
51
+ """
52
+ self._logger = logger
53
+ self._default_env_file = default_env_file
54
+ self._default_config_file = default_config_file
55
+ self._env_var_name = env_var_name
56
+ self._config_var_name = config_var_name
57
+ self._log_prefix = log_prefix
58
+ self._loaded_env_file_path: str | None = None
59
+ self._loaded_config_file_path: str | None = None
60
+
61
+ def get_loaded_env_file_path(self) -> str | None:
62
+ """Return the effective env file path used at runtime.
63
+
64
+ Example:
65
+ - Returns `./config/.env` when no override is provided.
66
+ - Returns `/etc/fred/agentic.env` in a production deployment override.
67
+ """
68
+ return self._loaded_env_file_path
69
+
70
+ def get_loaded_config_file_path(self) -> str | None:
71
+ """Return the effective YAML config file path used at runtime.
72
+
73
+ Example:
74
+ - `./config/configuration.yaml` in local mode.
75
+ - `./config/configuration_worker.yaml` for worker startup.
76
+ """
77
+ return self._loaded_config_file_path
78
+
79
+ def load_environment(self, dotenv_path: str | None = None) -> str:
80
+ """Load environment variables from the selected env file.
81
+
82
+ Selection order:
83
+ 1. Explicit `dotenv_path` argument.
84
+ 2. `ENV_FILE` environment variable.
85
+ 3. Default `./config/.env`.
86
+
87
+ Example:
88
+ - Calling `load_environment()` with `ENV_FILE=./config/.env.prod`
89
+ loads production secrets and returns that path.
90
+ """
91
+ env_path = dotenv_path or os.getenv(self._env_var_name, self._default_env_file)
92
+ if load_dotenv(env_path):
93
+ self._logger.info(
94
+ "%s Loaded environment variables from: %s",
95
+ self._log_prefix,
96
+ env_path,
97
+ )
98
+ else:
99
+ self._logger.warning(
100
+ "%s No .env file found at: %s",
101
+ self._log_prefix,
102
+ env_path,
103
+ )
104
+ self._loaded_env_file_path = env_path
105
+ return env_path
106
+
107
+ def resolve_config_file_path(self, config_file: str | None = None) -> str:
108
+ """Resolve and validate the YAML configuration path.
109
+
110
+ Selection order:
111
+ 1. Explicit `config_file` argument.
112
+ 2. `CONFIG_FILE` environment variable.
113
+ 3. Default `./config/configuration.yaml`.
114
+
115
+ Raises:
116
+ - `FileNotFoundError` if the resolved file does not exist.
117
+ """
118
+ resolved = config_file or os.getenv(
119
+ self._config_var_name, self._default_config_file
120
+ )
121
+ if not os.path.exists(resolved):
122
+ raise FileNotFoundError(f"Configuration file not found: {resolved}")
123
+ return resolved
124
+
125
+ def mark_config_loaded(self, config_file: str) -> None:
126
+ """Record and log the configuration file effectively loaded.
127
+
128
+ Example:
129
+ - After parsing `configuration_prod.yaml`, call this method so startup
130
+ logs and diagnostics expose the exact profile in use.
131
+ """
132
+ self._loaded_config_file_path = config_file
133
+ self._logger.info(
134
+ "%s Loaded configuration from: %s", self._log_prefix, config_file
135
+ )
@@ -0,0 +1,91 @@
1
+ # Copyright Thales 2025
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from typing import Callable, TypeVar
19
+
20
+ import yaml
21
+ from pydantic import ValidationError
22
+
23
+ from .config_files import ConfigFiles
24
+
25
+ TConfig = TypeVar("TConfig")
26
+
27
+
28
+ def _render_config_error_banner(config_file: str, error: Exception) -> None:
29
+ """Print a loud, unmissable configuration-error banner to stderr.
30
+
31
+ A misconfigured service must not start silently and fail later with an
32
+ opaque error inside a request handler. We surface the root cause in red at
33
+ startup. Colours are emitted only on a TTY so log files stay clean.
34
+ """
35
+ use_colour = sys.stderr.isatty()
36
+ red = "\033[1;31m" if use_colour else ""
37
+ reset = "\033[0m" if use_colour else ""
38
+ bar = "=" * 78
39
+
40
+ if isinstance(error, ValidationError):
41
+ details = "\n".join(
42
+ f" - {' -> '.join(str(p) for p in err['loc']) or '(root)'}: {err['msg']}"
43
+ for err in error.errors()
44
+ )
45
+ else:
46
+ details = f" - {error}"
47
+
48
+ print(
49
+ f"\n{red}{bar}\n"
50
+ f" CONFIGURATION ERROR — refusing to start\n"
51
+ f" file: {config_file}\n"
52
+ f"{bar}{reset}\n"
53
+ f"{details}\n"
54
+ f"{red}{bar}{reset}\n",
55
+ file=sys.stderr,
56
+ flush=True,
57
+ )
58
+
59
+
60
+ def parse_yaml_mapping_file(config_file: str) -> dict:
61
+ """Load a YAML file and ensure it is a non-empty mapping."""
62
+ with open(config_file, encoding="utf-8") as file:
63
+ payload = yaml.safe_load(file)
64
+ if payload is None:
65
+ raise ValueError(f"Configuration file is empty: {config_file}")
66
+ if not isinstance(payload, dict):
67
+ raise ValueError(f"Configuration file must be a mapping object: {config_file}")
68
+ return payload
69
+
70
+
71
+ def load_configuration_with_config_files(
72
+ config_files: ConfigFiles,
73
+ parser: Callable[[str], TConfig],
74
+ dotenv_path: str | None = None,
75
+ ) -> TConfig:
76
+ """Load env + config path using ConfigFiles and parse via callback."""
77
+ config_files.load_environment(dotenv_path)
78
+ config_file = config_files.resolve_config_file_path()
79
+ try:
80
+ configuration = parser(config_file)
81
+ except (ValidationError, ValueError) as exc:
82
+ # Render the root cause in red and stop, rather than letting an opaque
83
+ # traceback (or a deferred runtime 401) bury what is wrong.
84
+ _render_config_error_banner(config_file, exc)
85
+ raise SystemExit(1) from exc
86
+ config_files.mark_config_loaded(config_file)
87
+ return configuration
88
+
89
+
90
+ def get_config():
91
+ raise NotImplementedError("This dependency have to be override by the backend")