fred-core 1.3.1__py3-none-any.whl
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_core/__init__.py +166 -0
- fred_core/common/__init__.py +62 -0
- fred_core/common/config_files.py +135 -0
- fred_core/common/config_loader.py +47 -0
- fred_core/common/env.py +56 -0
- fred_core/common/fastapi_handlers.py +74 -0
- fred_core/common/lru_cache.py +43 -0
- fred_core/common/structures.py +203 -0
- fred_core/common/team_id.py +18 -0
- fred_core/common/utils.py +60 -0
- fred_core/filesystem/local_filesystem.py +240 -0
- fred_core/filesystem/minio_filesystem.py +382 -0
- fred_core/filesystem/structures.py +102 -0
- fred_core/kpi/__init__.py +65 -0
- fred_core/kpi/base_kpi_store.py +101 -0
- fred_core/kpi/base_kpi_writer.py +205 -0
- fred_core/kpi/kpi_phase_metric.py +60 -0
- fred_core/kpi/kpi_process.py +271 -0
- fred_core/kpi/kpi_reader_structures.py +117 -0
- fred_core/kpi/kpi_writer.py +750 -0
- fred_core/kpi/kpi_writer_structures.py +183 -0
- fred_core/kpi/log_kpi_store.py +67 -0
- fred_core/kpi/noop_kpi_writer.py +127 -0
- fred_core/kpi/opensearch_kpi_store.py +413 -0
- fred_core/kpi/prometheus_kpi_store.py +271 -0
- fred_core/logs/__init__.py +13 -0
- fred_core/logs/base_log_store.py +91 -0
- fred_core/logs/log_setup.py +292 -0
- fred_core/logs/log_structures.py +79 -0
- fred_core/logs/memory_log_store.py +126 -0
- fred_core/logs/null_log_store.py +36 -0
- fred_core/logs/opensearch_log_store.py +273 -0
- fred_core/model/factory.py +771 -0
- fred_core/model/http_clients.py +352 -0
- fred_core/model/models.py +26 -0
- fred_core/models/__init__.py +17 -0
- fred_core/models/base.py +29 -0
- fred_core/scheduler/__init__.py +27 -0
- fred_core/scheduler/backend.py +71 -0
- fred_core/scheduler/scheduler_structures.py +30 -0
- fred_core/scheduler/temporal_client_provider.py +63 -0
- fred_core/security/__init__.py +14 -0
- fred_core/security/authorization.py +73 -0
- fred_core/security/authorization_decorator.py +66 -0
- fred_core/security/backend_to_backend_auth.py +155 -0
- fred_core/security/keycloak/__init__.py +0 -0
- fred_core/security/keycloak/keycloack_admin_client.py +52 -0
- fred_core/security/models.py +95 -0
- fred_core/security/oidc.py +387 -0
- fred_core/security/outbound.py +129 -0
- fred_core/security/rbac.py +134 -0
- fred_core/security/rebac/noop_engine.py +81 -0
- fred_core/security/rebac/openfga_engine.py +408 -0
- fred_core/security/rebac/openfga_schema.py +18 -0
- fred_core/security/rebac/rebac_engine.py +618 -0
- fred_core/security/rebac/rebac_factory.py +33 -0
- fred_core/security/structure.py +99 -0
- fred_core/security/whitelist_access_control/access_control.py +100 -0
- fred_core/session/__init__.py +4 -0
- fred_core/session/session_schema.py +33 -0
- fred_core/session/stores/__init__.py +4 -0
- fred_core/session/stores/base_session_store.py +59 -0
- fred_core/session/stores/postgres_session_store.py +101 -0
- fred_core/session/stores/session_models.py +46 -0
- fred_core/sql/__init__.py +35 -0
- fred_core/sql/alembic_env.py +150 -0
- fred_core/sql/async_session.py +46 -0
- fred_core/sql/base_sql.py +499 -0
- fred_core/sql/mixin.py +56 -0
- fred_core/store/__init__.py +21 -0
- fred_core/store/base_content_store.py +69 -0
- fred_core/store/local_content_store.py +102 -0
- fred_core/store/minio_content_store.py +192 -0
- fred_core/store/opensearch_mapping_validator.py +203 -0
- fred_core/store/sql_store.py +198 -0
- fred_core/store/structures.py +22 -0
- fred_core/store/vector_search.py +69 -0
- fred_core/tests/__init__.py +13 -0
- fred_core/tests/common/test_config_loader.py +83 -0
- fred_core/tests/common/test_env.py +27 -0
- fred_core/tests/common/test_log_setup.py +40 -0
- fred_core/tests/integration/__init__.py +1 -0
- fred_core/tests/integration/test_rebac.py +964 -0
- fred_core/tests/model/test_vertex_model_garden_auth.py +177 -0
- fred_core/tests/scheduler/test_backend.py +24 -0
- fred_core/tests/security/__init__.py +13 -0
- fred_core/tests/security/test_authorization.py +133 -0
- fred_core/tests/security/test_rebac_engine_team_helpers.py +162 -0
- fred_core/tests/session/test_postgres_json_session_store_sqlite.py +57 -0
- fred_core-1.3.1.dist-info/METADATA +97 -0
- fred_core-1.3.1.dist-info/RECORD +93 -0
- fred_core-1.3.1.dist-info/WHEEL +5 -0
- fred_core-1.3.1.dist-info/top_level.txt +1 -0
fred_core/__init__.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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 fred_core.filesystem.local_filesystem import LocalFilesystem
|
|
16
|
+
from fred_core.filesystem.minio_filesystem import MinioFilesystem
|
|
17
|
+
from fred_core.filesystem.structures import (
|
|
18
|
+
BaseFilesystem,
|
|
19
|
+
FilesystemResourceInfo,
|
|
20
|
+
FilesystemResourceInfoResult,
|
|
21
|
+
)
|
|
22
|
+
from fred_core.logs.base_log_store import BaseLogStore
|
|
23
|
+
from fred_core.logs.log_setup import StoreEmitHandler, log_setup
|
|
24
|
+
from fred_core.logs.log_structures import (
|
|
25
|
+
InMemoryLogStorageConfig,
|
|
26
|
+
LogEventDTO,
|
|
27
|
+
LogFilter,
|
|
28
|
+
LogQuery,
|
|
29
|
+
LogQueryResult,
|
|
30
|
+
LogStorageConfig,
|
|
31
|
+
TailFileResponse,
|
|
32
|
+
)
|
|
33
|
+
from fred_core.logs.memory_log_store import RamLogStore
|
|
34
|
+
from fred_core.logs.opensearch_log_store import OpenSearchLogStore
|
|
35
|
+
from fred_core.model.factory import get_embeddings, get_model, get_structured_chain
|
|
36
|
+
from fred_core.model.models import ModelProvider
|
|
37
|
+
from fred_core.security.authorization import (
|
|
38
|
+
NO_AUTHZ_CHECK_USER,
|
|
39
|
+
TODO_PASS_REAL_USER,
|
|
40
|
+
Action,
|
|
41
|
+
AuthorizationError,
|
|
42
|
+
Resource,
|
|
43
|
+
authorize_or_raise,
|
|
44
|
+
is_authorized,
|
|
45
|
+
require_admin,
|
|
46
|
+
)
|
|
47
|
+
from fred_core.security.authorization_decorator import authorize
|
|
48
|
+
from fred_core.security.backend_to_backend_auth import (
|
|
49
|
+
M2MAuthConfig,
|
|
50
|
+
M2MBearerAuth,
|
|
51
|
+
M2MTokenProvider,
|
|
52
|
+
make_m2m_asgi_client,
|
|
53
|
+
)
|
|
54
|
+
from fred_core.security.keycloak.keycloack_admin_client import (
|
|
55
|
+
KeycloackDisabled,
|
|
56
|
+
create_keycloak_admin,
|
|
57
|
+
)
|
|
58
|
+
from fred_core.security.oidc import (
|
|
59
|
+
decode_jwt,
|
|
60
|
+
get_current_user,
|
|
61
|
+
get_keycloak_client_id,
|
|
62
|
+
get_keycloak_url,
|
|
63
|
+
initialize_user_security,
|
|
64
|
+
oauth2_scheme,
|
|
65
|
+
split_realm_url,
|
|
66
|
+
)
|
|
67
|
+
from fred_core.security.outbound import BearerAuth, ClientCredentialsProvider
|
|
68
|
+
from fred_core.security.rbac import RBACProvider
|
|
69
|
+
from fred_core.security.rebac.openfga_engine import OpenFgaRebacEngine
|
|
70
|
+
from fred_core.security.rebac.rebac_engine import (
|
|
71
|
+
ORGANIZATION_ID,
|
|
72
|
+
AgentPermission,
|
|
73
|
+
DocumentPermission,
|
|
74
|
+
OrganizationPermission,
|
|
75
|
+
RebacDisabledResult,
|
|
76
|
+
RebacEngine,
|
|
77
|
+
RebacPermission,
|
|
78
|
+
RebacReference,
|
|
79
|
+
Relation,
|
|
80
|
+
RelationType,
|
|
81
|
+
TagPermission,
|
|
82
|
+
TeamPermission,
|
|
83
|
+
)
|
|
84
|
+
from fred_core.security.rebac.rebac_factory import rebac_factory
|
|
85
|
+
from fred_core.security.structure import (
|
|
86
|
+
KeycloakUser,
|
|
87
|
+
M2MSecurity,
|
|
88
|
+
OpenFgaRebacConfig,
|
|
89
|
+
RebacConfiguration,
|
|
90
|
+
SecurityConfiguration,
|
|
91
|
+
UserSecurity,
|
|
92
|
+
)
|
|
93
|
+
from fred_core.session import SessionSchema
|
|
94
|
+
from fred_core.session.stores import BaseSessionStore, PostgresSessionStore
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"BaseLogStore",
|
|
98
|
+
"LogEventDTO",
|
|
99
|
+
"LogFilter",
|
|
100
|
+
"LogQuery",
|
|
101
|
+
"LogQueryResult",
|
|
102
|
+
"OpenSearchLogStore",
|
|
103
|
+
"RamLogStore",
|
|
104
|
+
"StoreEmitHandler",
|
|
105
|
+
"TailFileResponse",
|
|
106
|
+
"log_setup",
|
|
107
|
+
"LogStorageConfig",
|
|
108
|
+
"InMemoryLogStorageConfig",
|
|
109
|
+
"get_current_user",
|
|
110
|
+
"decode_jwt",
|
|
111
|
+
"initialize_user_security",
|
|
112
|
+
"KeycloakUser",
|
|
113
|
+
"SecurityConfiguration",
|
|
114
|
+
"M2MSecurity",
|
|
115
|
+
"RebacConfiguration",
|
|
116
|
+
"UserSecurity",
|
|
117
|
+
"TODO_PASS_REAL_USER",
|
|
118
|
+
"NO_AUTHZ_CHECK_USER",
|
|
119
|
+
"BaseFilesystem",
|
|
120
|
+
"LocalFilesystem",
|
|
121
|
+
"MinioFilesystem",
|
|
122
|
+
"FilesystemResourceInfoResult",
|
|
123
|
+
"FilesystemResourceInfo",
|
|
124
|
+
"RBACProvider",
|
|
125
|
+
"require_admin",
|
|
126
|
+
"Action",
|
|
127
|
+
"Resource",
|
|
128
|
+
"AuthorizationError",
|
|
129
|
+
"is_authorized",
|
|
130
|
+
"authorize_or_raise",
|
|
131
|
+
"authorize",
|
|
132
|
+
"oauth2_scheme",
|
|
133
|
+
"ClientCredentialsProvider",
|
|
134
|
+
"BearerAuth",
|
|
135
|
+
"M2MAuthConfig",
|
|
136
|
+
"M2MTokenProvider",
|
|
137
|
+
"M2MBearerAuth",
|
|
138
|
+
"make_m2m_asgi_client",
|
|
139
|
+
"split_realm_url",
|
|
140
|
+
"get_model",
|
|
141
|
+
"get_structured_chain",
|
|
142
|
+
"get_embeddings",
|
|
143
|
+
"ModelProvider",
|
|
144
|
+
"BaseSessionStore",
|
|
145
|
+
"PostgresSessionStore",
|
|
146
|
+
"SessionSchema",
|
|
147
|
+
"RebacReference",
|
|
148
|
+
"Relation",
|
|
149
|
+
"RelationType",
|
|
150
|
+
"TagPermission",
|
|
151
|
+
"DocumentPermission",
|
|
152
|
+
"TeamPermission",
|
|
153
|
+
"ORGANIZATION_ID",
|
|
154
|
+
"AgentPermission",
|
|
155
|
+
"OrganizationPermission",
|
|
156
|
+
"RebacPermission",
|
|
157
|
+
"RebacDisabledResult",
|
|
158
|
+
"RebacEngine",
|
|
159
|
+
"OpenFgaRebacEngine",
|
|
160
|
+
"OpenFgaRebacConfig",
|
|
161
|
+
"rebac_factory",
|
|
162
|
+
"get_keycloak_url",
|
|
163
|
+
"get_keycloak_client_id",
|
|
164
|
+
"KeycloackDisabled",
|
|
165
|
+
"create_keycloak_admin",
|
|
166
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
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 .config_files import ConfigFiles
|
|
16
|
+
from .config_loader import (
|
|
17
|
+
load_configuration_with_config_files,
|
|
18
|
+
parse_yaml_mapping_file,
|
|
19
|
+
)
|
|
20
|
+
from .env import coerce_bool, read_env_bool
|
|
21
|
+
from .fastapi_handlers import register_exception_handlers
|
|
22
|
+
from .lru_cache import ThreadSafeLRUCache
|
|
23
|
+
from .structures import (
|
|
24
|
+
BaseModelWithId,
|
|
25
|
+
DuckdbStoreConfig,
|
|
26
|
+
LogStoreConfig,
|
|
27
|
+
ModelConfiguration,
|
|
28
|
+
OpenSearchIndexConfig,
|
|
29
|
+
OpenSearchStoreConfig,
|
|
30
|
+
OwnerFilter,
|
|
31
|
+
PostgresStoreConfig,
|
|
32
|
+
PostgresTableConfig,
|
|
33
|
+
SQLStorageConfig,
|
|
34
|
+
StoreConfig,
|
|
35
|
+
TemporalSchedulerConfig,
|
|
36
|
+
)
|
|
37
|
+
from .team_id import TeamId
|
|
38
|
+
from .utils import raise_internal_error
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"BaseModelWithId",
|
|
42
|
+
"ConfigFiles",
|
|
43
|
+
"DuckdbStoreConfig",
|
|
44
|
+
"LogStoreConfig",
|
|
45
|
+
"ModelConfiguration",
|
|
46
|
+
"OpenSearchIndexConfig",
|
|
47
|
+
"OpenSearchStoreConfig",
|
|
48
|
+
"OwnerFilter",
|
|
49
|
+
"PostgresStoreConfig",
|
|
50
|
+
"PostgresTableConfig",
|
|
51
|
+
"SQLStorageConfig",
|
|
52
|
+
"StoreConfig",
|
|
53
|
+
"TeamId",
|
|
54
|
+
"TemporalSchedulerConfig",
|
|
55
|
+
"ThreadSafeLRUCache",
|
|
56
|
+
"coerce_bool",
|
|
57
|
+
"load_configuration_with_config_files",
|
|
58
|
+
"parse_yaml_mapping_file",
|
|
59
|
+
"raise_internal_error",
|
|
60
|
+
"read_env_bool",
|
|
61
|
+
"register_exception_handlers",
|
|
62
|
+
]
|
|
@@ -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,47 @@
|
|
|
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
|
+
from typing import Callable, TypeVar
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from .config_files import ConfigFiles
|
|
22
|
+
|
|
23
|
+
TConfig = TypeVar("TConfig")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_yaml_mapping_file(config_file: str) -> dict:
|
|
27
|
+
"""Load a YAML file and ensure it is a non-empty mapping."""
|
|
28
|
+
with open(config_file, encoding="utf-8") as file:
|
|
29
|
+
payload = yaml.safe_load(file)
|
|
30
|
+
if payload is None:
|
|
31
|
+
raise ValueError(f"Configuration file is empty: {config_file}")
|
|
32
|
+
if not isinstance(payload, dict):
|
|
33
|
+
raise ValueError(f"Configuration file must be a mapping object: {config_file}")
|
|
34
|
+
return payload
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_configuration_with_config_files(
|
|
38
|
+
config_files: ConfigFiles,
|
|
39
|
+
parser: Callable[[str], TConfig],
|
|
40
|
+
dotenv_path: str | None = None,
|
|
41
|
+
) -> TConfig:
|
|
42
|
+
"""Load env + config path using ConfigFiles and parse via callback."""
|
|
43
|
+
config_files.load_environment(dotenv_path)
|
|
44
|
+
config_file = config_files.resolve_config_file_path()
|
|
45
|
+
configuration = parser(config_file)
|
|
46
|
+
config_files.mark_config_loaded(config_file)
|
|
47
|
+
return configuration
|
fred_core/common/env.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Central boolean parsing helpers for environment flags and loose runtime payloads.
|
|
8
|
+
|
|
9
|
+
Why this file exists:
|
|
10
|
+
- The same string checks (`"1"`, `"true"`, `"yes"`, `"on"`) were duplicated in
|
|
11
|
+
multiple services.
|
|
12
|
+
- One shared helper keeps behavior consistent and removes repeated code.
|
|
13
|
+
|
|
14
|
+
How to use:
|
|
15
|
+
```python
|
|
16
|
+
from fred_core.common import read_env_bool, coerce_bool
|
|
17
|
+
|
|
18
|
+
docs_enabled = read_env_bool("PRODUCTION_FASTAPI_DOCS_ENABLED", default=True)
|
|
19
|
+
requires_approval = coerce_bool(payload.get("requires_approval"), default=False)
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_TRUTHY_STRINGS: frozenset[str] = frozenset({"1", "true", "yes", "on", "y"})
|
|
24
|
+
_FALSY_STRINGS: frozenset[str] = frozenset({"0", "false", "no", "off", "n"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def coerce_bool(value: Any, default: bool = False) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Convert common runtime inputs to bool.
|
|
30
|
+
|
|
31
|
+
Supported input types:
|
|
32
|
+
- `bool`: returned as-is
|
|
33
|
+
- `int` / `float`: zero => False, non-zero => True
|
|
34
|
+
- `str`: normalized and matched against known true/false tokens
|
|
35
|
+
- anything else: fallback to `default`
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
return value
|
|
39
|
+
if isinstance(value, (int, float)):
|
|
40
|
+
return value != 0
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
normalized = value.strip().lower()
|
|
43
|
+
if normalized in _TRUTHY_STRINGS:
|
|
44
|
+
return True
|
|
45
|
+
if normalized in _FALSY_STRINGS:
|
|
46
|
+
return False
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_env_bool(name: str, default: bool) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Read and parse a boolean environment variable.
|
|
53
|
+
|
|
54
|
+
Use this for runtime flags loaded from environment variables.
|
|
55
|
+
"""
|
|
56
|
+
return coerce_bool(os.getenv(name), default=default)
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, Request
|
|
18
|
+
from fastapi.responses import JSONResponse
|
|
19
|
+
|
|
20
|
+
from fred_core.security.models import AuthorizationError, Resource
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_TEAM_PERMISSION_MESSAGES: dict[str, str] = {
|
|
25
|
+
"can_update_resources": "You are not allowed to manage resources in this team. Ask a team owner or manager.",
|
|
26
|
+
"can_update_agents": "You are not allowed to manage agents in this team. Ask a team owner or manager.",
|
|
27
|
+
"can_administer_members": "You are not allowed to manage members in this team.",
|
|
28
|
+
"can_administer_managers": "You are not allowed to manage managers in this team.",
|
|
29
|
+
"can_administer_owners": "You are not allowed to manage owners in this team.",
|
|
30
|
+
"can_read_members": "You are not allowed to view team members.",
|
|
31
|
+
"can_update_info": "You are not allowed to update this team.",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _humanize_action(action: str) -> str:
|
|
36
|
+
return action.replace(":", " ").replace("_", " ")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _authorization_detail_for_client(exc: AuthorizationError) -> str:
|
|
40
|
+
action = str(exc.action)
|
|
41
|
+
if exc.resource == Resource.TEAM and action in _TEAM_PERMISSION_MESSAGES:
|
|
42
|
+
return _TEAM_PERMISSION_MESSAGES[action]
|
|
43
|
+
|
|
44
|
+
readable_action = _humanize_action(action)
|
|
45
|
+
readable_resource = exc.resource.value.replace("_", " ")
|
|
46
|
+
return f"You are not allowed to {readable_action} {readable_resource}."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
50
|
+
"""Register authorization and generic exception handlers for FastAPI application."""
|
|
51
|
+
|
|
52
|
+
@app.exception_handler(AuthorizationError)
|
|
53
|
+
async def authorization_error_handler(
|
|
54
|
+
request: Request, exc: AuthorizationError
|
|
55
|
+
) -> JSONResponse:
|
|
56
|
+
"""Handle AuthorizationError by returning a 403 Forbidden response."""
|
|
57
|
+
logger.warning(f"Authorization denied for user {exc.user_id}: {exc}")
|
|
58
|
+
return JSONResponse(
|
|
59
|
+
status_code=403,
|
|
60
|
+
content={"detail": _authorization_detail_for_client(exc)},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@app.exception_handler(Exception)
|
|
64
|
+
async def generic_exception_handler(
|
|
65
|
+
request: Request, exc: Exception
|
|
66
|
+
) -> JSONResponse:
|
|
67
|
+
"""Handle all unhandled exceptions by logging and returning 500."""
|
|
68
|
+
logger.error(
|
|
69
|
+
f"Unhandled exception in {request.method} {request.url}: {exc}",
|
|
70
|
+
exc_info=True,
|
|
71
|
+
)
|
|
72
|
+
return JSONResponse(
|
|
73
|
+
status_code=500, content={"detail": "Internal server error"}
|
|
74
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from threading import Lock
|
|
3
|
+
from typing import Generic, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
K = TypeVar("K")
|
|
6
|
+
V = TypeVar("V")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ThreadSafeLRUCache(Generic[K, V]):
|
|
10
|
+
def __init__(self, max_size: int = 1000):
|
|
11
|
+
self._max_size = max_size
|
|
12
|
+
self._lock = Lock()
|
|
13
|
+
self._cache: OrderedDict[K, V] = OrderedDict()
|
|
14
|
+
|
|
15
|
+
def get(self, key: K) -> V | None:
|
|
16
|
+
with self._lock:
|
|
17
|
+
value = self._cache.get(key)
|
|
18
|
+
if value is not None:
|
|
19
|
+
self._cache.move_to_end(key) # Mark as recently used
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
def set(self, key: K, value: V) -> None:
|
|
23
|
+
with self._lock:
|
|
24
|
+
self._cache[key] = value
|
|
25
|
+
self._cache.move_to_end(key)
|
|
26
|
+
if len(self._cache) > self._max_size:
|
|
27
|
+
self._cache.popitem(last=False) # Remove LRU
|
|
28
|
+
|
|
29
|
+
def delete(self, key: K) -> Optional[V]:
|
|
30
|
+
with self._lock:
|
|
31
|
+
return self._cache.pop(key, None)
|
|
32
|
+
|
|
33
|
+
def keys(self) -> list[K]:
|
|
34
|
+
with self._lock:
|
|
35
|
+
return list(self._cache.keys())
|
|
36
|
+
|
|
37
|
+
def clear(self) -> None:
|
|
38
|
+
with self._lock:
|
|
39
|
+
self._cache.clear()
|
|
40
|
+
|
|
41
|
+
def __contains__(self, key: K) -> bool:
|
|
42
|
+
with self._lock:
|
|
43
|
+
return key in self._cache
|