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.
- fred_pod-4.1.0/PKG-INFO +104 -0
- fred_pod-4.1.0/README.md +76 -0
- fred_pod-4.1.0/fred_pod/__init__.py +135 -0
- fred_pod-4.1.0/fred_pod/common/__init__.py +85 -0
- fred_pod-4.1.0/fred_pod/common/config_files.py +135 -0
- fred_pod-4.1.0/fred_pod/common/config_loader.py +91 -0
- fred_pod-4.1.0/fred_pod/common/naming.py +110 -0
- fred_pod-4.1.0/fred_pod/common/structures.py +247 -0
- fred_pod-4.1.0/fred_pod/py.typed +0 -0
- fred_pod-4.1.0/fred_pod/security/__init__.py +51 -0
- fred_pod-4.1.0/fred_pod/security/backend_to_backend_auth.py +152 -0
- fred_pod-4.1.0/fred_pod/security/structure.py +153 -0
- fred_pod-4.1.0/fred_pod.egg-info/PKG-INFO +104 -0
- fred_pod-4.1.0/fred_pod.egg-info/SOURCES.txt +20 -0
- fred_pod-4.1.0/fred_pod.egg-info/dependency_links.txt +1 -0
- fred_pod-4.1.0/fred_pod.egg-info/requires.txt +14 -0
- fred_pod-4.1.0/fred_pod.egg-info/top_level.txt +1 -0
- fred_pod-4.1.0/pyproject.toml +84 -0
- fred_pod-4.1.0/setup.cfg +4 -0
- fred_pod-4.1.0/tests/test_config_loader.py +110 -0
- fred_pod-4.1.0/tests/test_naming.py +113 -0
- fred_pod-4.1.0/tests/test_structures.py +46 -0
fred_pod-4.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
fred_pod-4.1.0/README.md
ADDED
|
@@ -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")
|