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.
Files changed (93) hide show
  1. fred_core/__init__.py +166 -0
  2. fred_core/common/__init__.py +62 -0
  3. fred_core/common/config_files.py +135 -0
  4. fred_core/common/config_loader.py +47 -0
  5. fred_core/common/env.py +56 -0
  6. fred_core/common/fastapi_handlers.py +74 -0
  7. fred_core/common/lru_cache.py +43 -0
  8. fred_core/common/structures.py +203 -0
  9. fred_core/common/team_id.py +18 -0
  10. fred_core/common/utils.py +60 -0
  11. fred_core/filesystem/local_filesystem.py +240 -0
  12. fred_core/filesystem/minio_filesystem.py +382 -0
  13. fred_core/filesystem/structures.py +102 -0
  14. fred_core/kpi/__init__.py +65 -0
  15. fred_core/kpi/base_kpi_store.py +101 -0
  16. fred_core/kpi/base_kpi_writer.py +205 -0
  17. fred_core/kpi/kpi_phase_metric.py +60 -0
  18. fred_core/kpi/kpi_process.py +271 -0
  19. fred_core/kpi/kpi_reader_structures.py +117 -0
  20. fred_core/kpi/kpi_writer.py +750 -0
  21. fred_core/kpi/kpi_writer_structures.py +183 -0
  22. fred_core/kpi/log_kpi_store.py +67 -0
  23. fred_core/kpi/noop_kpi_writer.py +127 -0
  24. fred_core/kpi/opensearch_kpi_store.py +413 -0
  25. fred_core/kpi/prometheus_kpi_store.py +271 -0
  26. fred_core/logs/__init__.py +13 -0
  27. fred_core/logs/base_log_store.py +91 -0
  28. fred_core/logs/log_setup.py +292 -0
  29. fred_core/logs/log_structures.py +79 -0
  30. fred_core/logs/memory_log_store.py +126 -0
  31. fred_core/logs/null_log_store.py +36 -0
  32. fred_core/logs/opensearch_log_store.py +273 -0
  33. fred_core/model/factory.py +771 -0
  34. fred_core/model/http_clients.py +352 -0
  35. fred_core/model/models.py +26 -0
  36. fred_core/models/__init__.py +17 -0
  37. fred_core/models/base.py +29 -0
  38. fred_core/scheduler/__init__.py +27 -0
  39. fred_core/scheduler/backend.py +71 -0
  40. fred_core/scheduler/scheduler_structures.py +30 -0
  41. fred_core/scheduler/temporal_client_provider.py +63 -0
  42. fred_core/security/__init__.py +14 -0
  43. fred_core/security/authorization.py +73 -0
  44. fred_core/security/authorization_decorator.py +66 -0
  45. fred_core/security/backend_to_backend_auth.py +155 -0
  46. fred_core/security/keycloak/__init__.py +0 -0
  47. fred_core/security/keycloak/keycloack_admin_client.py +52 -0
  48. fred_core/security/models.py +95 -0
  49. fred_core/security/oidc.py +387 -0
  50. fred_core/security/outbound.py +129 -0
  51. fred_core/security/rbac.py +134 -0
  52. fred_core/security/rebac/noop_engine.py +81 -0
  53. fred_core/security/rebac/openfga_engine.py +408 -0
  54. fred_core/security/rebac/openfga_schema.py +18 -0
  55. fred_core/security/rebac/rebac_engine.py +618 -0
  56. fred_core/security/rebac/rebac_factory.py +33 -0
  57. fred_core/security/structure.py +99 -0
  58. fred_core/security/whitelist_access_control/access_control.py +100 -0
  59. fred_core/session/__init__.py +4 -0
  60. fred_core/session/session_schema.py +33 -0
  61. fred_core/session/stores/__init__.py +4 -0
  62. fred_core/session/stores/base_session_store.py +59 -0
  63. fred_core/session/stores/postgres_session_store.py +101 -0
  64. fred_core/session/stores/session_models.py +46 -0
  65. fred_core/sql/__init__.py +35 -0
  66. fred_core/sql/alembic_env.py +150 -0
  67. fred_core/sql/async_session.py +46 -0
  68. fred_core/sql/base_sql.py +499 -0
  69. fred_core/sql/mixin.py +56 -0
  70. fred_core/store/__init__.py +21 -0
  71. fred_core/store/base_content_store.py +69 -0
  72. fred_core/store/local_content_store.py +102 -0
  73. fred_core/store/minio_content_store.py +192 -0
  74. fred_core/store/opensearch_mapping_validator.py +203 -0
  75. fred_core/store/sql_store.py +198 -0
  76. fred_core/store/structures.py +22 -0
  77. fred_core/store/vector_search.py +69 -0
  78. fred_core/tests/__init__.py +13 -0
  79. fred_core/tests/common/test_config_loader.py +83 -0
  80. fred_core/tests/common/test_env.py +27 -0
  81. fred_core/tests/common/test_log_setup.py +40 -0
  82. fred_core/tests/integration/__init__.py +1 -0
  83. fred_core/tests/integration/test_rebac.py +964 -0
  84. fred_core/tests/model/test_vertex_model_garden_auth.py +177 -0
  85. fred_core/tests/scheduler/test_backend.py +24 -0
  86. fred_core/tests/security/__init__.py +13 -0
  87. fred_core/tests/security/test_authorization.py +133 -0
  88. fred_core/tests/security/test_rebac_engine_team_helpers.py +162 -0
  89. fred_core/tests/session/test_postgres_json_session_store_sqlite.py +57 -0
  90. fred_core-1.3.1.dist-info/METADATA +97 -0
  91. fred_core-1.3.1.dist-info/RECORD +93 -0
  92. fred_core-1.3.1.dist-info/WHEEL +5 -0
  93. 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
@@ -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