mdb-engine 0.1.6__py3-none-any.whl → 0.2.0__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.
- mdb_engine/__init__.py +104 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +648 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +264 -69
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +47 -50
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +53 -80
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +18 -38
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +1057 -93
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +459 -150
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +37 -154
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +25 -60
- mdb_engine-0.2.0.dist-info/METADATA +313 -0
- mdb_engine-0.2.0.dist-info/RECORD +96 -0
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Dependencies for MDB Engine
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
1. RequestContext - All-in-one request-scoped dependency
|
|
6
|
+
2. Individual dependencies for fine-grained control
|
|
7
|
+
3. DI container integration
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from fastapi import Depends
|
|
11
|
+
from mdb_engine.dependencies import RequestContext
|
|
12
|
+
|
|
13
|
+
@app.get("/users/{user_id}")
|
|
14
|
+
async def get_user(user_id: str, ctx: RequestContext = Depends()):
|
|
15
|
+
user = await ctx.uow.users.get(user_id)
|
|
16
|
+
return user
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, TypeVar, Union
|
|
22
|
+
|
|
23
|
+
from fastapi import HTTPException, Request
|
|
24
|
+
|
|
25
|
+
from .di import Container
|
|
26
|
+
from .repositories import UnitOfWork
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from openai import AzureOpenAI, OpenAI
|
|
30
|
+
|
|
31
|
+
from .auth.provider import AuthorizationProvider
|
|
32
|
+
from .core.engine import MongoDBEngine
|
|
33
|
+
from .database.scoped_wrapper import ScopedMongoWrapper
|
|
34
|
+
from .embeddings.service import EmbeddingService
|
|
35
|
+
from .memory.service import Mem0MemoryService
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Core Engine Dependencies
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def get_engine(request: Request) -> "MongoDBEngine":
|
|
48
|
+
"""Get the MongoDBEngine instance from app state."""
|
|
49
|
+
engine = getattr(request.app.state, "engine", None)
|
|
50
|
+
if not engine:
|
|
51
|
+
raise HTTPException(503, "Engine not initialized")
|
|
52
|
+
if not engine.initialized:
|
|
53
|
+
raise HTTPException(503, "Engine not fully initialized")
|
|
54
|
+
return engine
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def get_app_slug(request: Request) -> str:
|
|
58
|
+
"""Get the current app's slug."""
|
|
59
|
+
slug = getattr(request.app.state, "app_slug", None)
|
|
60
|
+
if not slug:
|
|
61
|
+
raise HTTPException(503, "App slug not configured")
|
|
62
|
+
return slug
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def get_app_config(request: Request) -> Dict[str, Any]:
|
|
66
|
+
"""Get the app's manifest configuration."""
|
|
67
|
+
manifest = getattr(request.app.state, "manifest", None)
|
|
68
|
+
if manifest is None:
|
|
69
|
+
engine = getattr(request.app.state, "engine", None)
|
|
70
|
+
slug = getattr(request.app.state, "app_slug", None)
|
|
71
|
+
if engine and slug:
|
|
72
|
+
manifest = engine.get_app(slug)
|
|
73
|
+
if manifest is None:
|
|
74
|
+
raise HTTPException(503, "App configuration not available")
|
|
75
|
+
return manifest
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# Database Dependencies
|
|
80
|
+
# =============================================================================
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def get_scoped_db(request: Request) -> "ScopedMongoWrapper":
|
|
84
|
+
"""Get a scoped database wrapper for the current app."""
|
|
85
|
+
engine = await get_engine(request)
|
|
86
|
+
slug = await get_app_slug(request)
|
|
87
|
+
return engine.get_scoped_db(slug)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def get_unit_of_work(request: Request) -> UnitOfWork:
|
|
91
|
+
"""Get a request-scoped UnitOfWork."""
|
|
92
|
+
db = await get_scoped_db(request)
|
|
93
|
+
return UnitOfWork(db)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# AI/ML Service Dependencies
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def get_embedding_service(request: Request) -> "EmbeddingService":
|
|
102
|
+
"""Get the EmbeddingService for text embeddings."""
|
|
103
|
+
engine = await get_engine(request)
|
|
104
|
+
slug = await get_app_slug(request)
|
|
105
|
+
|
|
106
|
+
app_config = engine.get_app(slug)
|
|
107
|
+
if not app_config:
|
|
108
|
+
raise HTTPException(503, f"App configuration not found for '{slug}'")
|
|
109
|
+
|
|
110
|
+
embedding_config = app_config.get("embedding_config", {})
|
|
111
|
+
if not embedding_config.get("enabled", True):
|
|
112
|
+
raise HTTPException(503, "Embedding service is disabled")
|
|
113
|
+
|
|
114
|
+
from .embeddings.service import EmbeddingServiceError
|
|
115
|
+
from .embeddings.service import get_embedding_service as create_service
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return create_service(config=embedding_config)
|
|
119
|
+
except (EmbeddingServiceError, ValueError, RuntimeError, ImportError, AttributeError) as e:
|
|
120
|
+
raise HTTPException(503, f"Failed to initialize embedding service: {e}") from e
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def get_memory_service(request: Request) -> Optional["Mem0MemoryService"]:
|
|
124
|
+
"""Get the Mem0 memory service if configured."""
|
|
125
|
+
engine = getattr(request.app.state, "engine", None)
|
|
126
|
+
slug = getattr(request.app.state, "app_slug", None)
|
|
127
|
+
if not engine or not slug:
|
|
128
|
+
return None
|
|
129
|
+
return engine.get_memory_service(slug)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def get_llm_client(request: Request) -> Union["AzureOpenAI", "OpenAI"]:
|
|
133
|
+
"""Get an OpenAI/AzureOpenAI client."""
|
|
134
|
+
azure_key = os.getenv("AZURE_OPENAI_API_KEY")
|
|
135
|
+
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
136
|
+
|
|
137
|
+
if azure_key and azure_endpoint:
|
|
138
|
+
from openai import AzureOpenAI
|
|
139
|
+
|
|
140
|
+
return AzureOpenAI(
|
|
141
|
+
api_key=azure_key,
|
|
142
|
+
azure_endpoint=azure_endpoint,
|
|
143
|
+
api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01"),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
openai_key = os.getenv("OPENAI_API_KEY")
|
|
147
|
+
if openai_key:
|
|
148
|
+
from openai import OpenAI
|
|
149
|
+
|
|
150
|
+
return OpenAI(api_key=openai_key)
|
|
151
|
+
|
|
152
|
+
raise HTTPException(503, "No LLM API key configured")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_llm_model_name() -> str:
|
|
156
|
+
"""Get the configured LLM model/deployment name."""
|
|
157
|
+
if os.getenv("AZURE_OPENAI_API_KEY"):
|
|
158
|
+
return os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o")
|
|
159
|
+
return os.getenv("OPENAI_MODEL", "gpt-4o")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# Auth Dependencies
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def get_authz_provider(request: Request) -> Optional["AuthorizationProvider"]:
|
|
168
|
+
"""Get the authorization provider if configured."""
|
|
169
|
+
return getattr(request.app.state, "authz_provider", None)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def get_current_user(request: Request) -> Optional[Dict[str, Any]]:
|
|
173
|
+
"""Get the current authenticated user."""
|
|
174
|
+
return getattr(request.state, "user", None)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def get_user_roles(request: Request) -> List[str]:
|
|
178
|
+
"""Get the current user's roles."""
|
|
179
|
+
return getattr(request.state, "user_roles", [])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def require_user():
|
|
183
|
+
"""Dependency that requires authentication."""
|
|
184
|
+
|
|
185
|
+
async def _require_user(request: Request) -> Dict[str, Any]:
|
|
186
|
+
user = await get_current_user(request)
|
|
187
|
+
if not user:
|
|
188
|
+
raise HTTPException(401, "Authentication required")
|
|
189
|
+
return user
|
|
190
|
+
|
|
191
|
+
return _require_user
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def require_role(*roles: str):
|
|
195
|
+
"""Dependency that requires specific roles."""
|
|
196
|
+
|
|
197
|
+
async def _require_role(request: Request) -> Dict[str, Any]:
|
|
198
|
+
user = await get_current_user(request)
|
|
199
|
+
if not user:
|
|
200
|
+
raise HTTPException(401, "Authentication required")
|
|
201
|
+
user_roles = set(await get_user_roles(request))
|
|
202
|
+
if not any(role in user_roles for role in roles):
|
|
203
|
+
raise HTTPException(403, f"Required role: {' or '.join(roles)}")
|
|
204
|
+
return user
|
|
205
|
+
|
|
206
|
+
return _require_role
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# =============================================================================
|
|
210
|
+
# RequestContext - All-in-One Dependency (Regular class, not dataclass!)
|
|
211
|
+
# =============================================================================
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class RequestContext:
|
|
215
|
+
"""
|
|
216
|
+
All-in-one request context with lazy-loaded dependencies.
|
|
217
|
+
|
|
218
|
+
This is NOT a dataclass to avoid FastAPI trying to analyze
|
|
219
|
+
fields as Pydantic types.
|
|
220
|
+
|
|
221
|
+
Usage:
|
|
222
|
+
@app.post("/documents")
|
|
223
|
+
async def create_doc(data: DocCreate, ctx: RequestContext = Depends()):
|
|
224
|
+
doc_id = await ctx.uow.documents.add(doc)
|
|
225
|
+
return {"id": doc_id}
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self, request: Request):
|
|
229
|
+
self.request = request
|
|
230
|
+
self._uow = None
|
|
231
|
+
self._engine = None
|
|
232
|
+
self._db = None
|
|
233
|
+
self._slug = None
|
|
234
|
+
self._config = None
|
|
235
|
+
self._embedding_service = None
|
|
236
|
+
self._memory = None
|
|
237
|
+
self._llm = None
|
|
238
|
+
self._user = None
|
|
239
|
+
self._authz = None
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def engine(self):
|
|
243
|
+
"""Get the MongoDBEngine instance."""
|
|
244
|
+
if self._engine is None:
|
|
245
|
+
engine = getattr(self.request.app.state, "engine", None)
|
|
246
|
+
if not engine or not engine.initialized:
|
|
247
|
+
raise HTTPException(503, "Engine not initialized")
|
|
248
|
+
self._engine = engine
|
|
249
|
+
return self._engine
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def slug(self) -> str:
|
|
253
|
+
"""Get the current app's slug."""
|
|
254
|
+
if self._slug is None:
|
|
255
|
+
self._slug = getattr(self.request.app.state, "app_slug", None)
|
|
256
|
+
if not self._slug:
|
|
257
|
+
raise HTTPException(503, "App slug not configured")
|
|
258
|
+
return self._slug
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def db(self):
|
|
262
|
+
"""Get the scoped database wrapper."""
|
|
263
|
+
if self._db is None:
|
|
264
|
+
self._db = self.engine.get_scoped_db(self.slug)
|
|
265
|
+
return self._db
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def uow(self) -> UnitOfWork:
|
|
269
|
+
"""Get the Unit of Work for repository access."""
|
|
270
|
+
if self._uow is None:
|
|
271
|
+
self._uow = UnitOfWork(self.db)
|
|
272
|
+
return self._uow
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def config(self) -> Dict[str, Any]:
|
|
276
|
+
"""Get the app's manifest configuration."""
|
|
277
|
+
if self._config is None:
|
|
278
|
+
self._config = getattr(self.request.app.state, "manifest", None)
|
|
279
|
+
if self._config is None:
|
|
280
|
+
self._config = self.engine.get_app(self.slug) or {}
|
|
281
|
+
return self._config
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def embedding_service(self):
|
|
285
|
+
"""Get the embedding service (None if not configured)."""
|
|
286
|
+
if self._embedding_service is None:
|
|
287
|
+
embedding_config = self.config.get("embedding_config", {})
|
|
288
|
+
if embedding_config.get("enabled", True):
|
|
289
|
+
try:
|
|
290
|
+
from .embeddings.service import (
|
|
291
|
+
EmbeddingServiceError,
|
|
292
|
+
get_embedding_service,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
self._embedding_service = get_embedding_service(config=embedding_config)
|
|
296
|
+
except (EmbeddingServiceError, ValueError, RuntimeError, ImportError):
|
|
297
|
+
pass
|
|
298
|
+
return self._embedding_service
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def memory(self):
|
|
302
|
+
"""Get the memory service (None if not configured)."""
|
|
303
|
+
if self._memory is None:
|
|
304
|
+
self._memory = self.engine.get_memory_service(self.slug)
|
|
305
|
+
return self._memory
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def llm(self):
|
|
309
|
+
"""Get the LLM client (None if not configured)."""
|
|
310
|
+
if self._llm is None:
|
|
311
|
+
azure_key = os.getenv("AZURE_OPENAI_API_KEY")
|
|
312
|
+
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
313
|
+
|
|
314
|
+
if azure_key and azure_endpoint:
|
|
315
|
+
from openai import AzureOpenAI
|
|
316
|
+
|
|
317
|
+
self._llm = AzureOpenAI(
|
|
318
|
+
api_key=azure_key,
|
|
319
|
+
azure_endpoint=azure_endpoint,
|
|
320
|
+
api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01"),
|
|
321
|
+
)
|
|
322
|
+
elif os.getenv("OPENAI_API_KEY"):
|
|
323
|
+
from openai import OpenAI
|
|
324
|
+
|
|
325
|
+
self._llm = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
|
326
|
+
return self._llm
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def llm_model(self) -> str:
|
|
330
|
+
"""Get the LLM model/deployment name."""
|
|
331
|
+
return get_llm_model_name()
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def user(self) -> Optional[Dict[str, Any]]:
|
|
335
|
+
"""Get the current authenticated user."""
|
|
336
|
+
if self._user is None:
|
|
337
|
+
self._user = getattr(self.request.state, "user", None)
|
|
338
|
+
return self._user
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def user_roles(self) -> List[str]:
|
|
342
|
+
"""Get the current user's roles."""
|
|
343
|
+
return getattr(self.request.state, "user_roles", [])
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def authz(self):
|
|
347
|
+
"""Get the authorization provider."""
|
|
348
|
+
if self._authz is None:
|
|
349
|
+
self._authz = getattr(self.request.app.state, "authz_provider", None)
|
|
350
|
+
return self._authz
|
|
351
|
+
|
|
352
|
+
def require_user(self) -> Dict[str, Any]:
|
|
353
|
+
"""Require authentication, raising 401 if not authenticated."""
|
|
354
|
+
if not self.user:
|
|
355
|
+
raise HTTPException(401, "Authentication required")
|
|
356
|
+
return self.user
|
|
357
|
+
|
|
358
|
+
def require_role(self, *roles: str) -> Dict[str, Any]:
|
|
359
|
+
"""Require specific roles, raising 403 if not authorized."""
|
|
360
|
+
user = self.require_user()
|
|
361
|
+
user_roles = set(self.user_roles)
|
|
362
|
+
if not any(role in user_roles for role in roles):
|
|
363
|
+
roles_str = " or ".join(roles)
|
|
364
|
+
raise HTTPException(403, f"Required role: {roles_str}")
|
|
365
|
+
return user
|
|
366
|
+
|
|
367
|
+
async def check_permission(
|
|
368
|
+
self, resource: str, action: str, subject: Optional[str] = None
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Check if current user has permission for an action."""
|
|
371
|
+
if not self.authz:
|
|
372
|
+
return True
|
|
373
|
+
if subject is None:
|
|
374
|
+
user = self.user
|
|
375
|
+
subject = user.get("email", "anonymous") if user else "anonymous"
|
|
376
|
+
return await self.authz.check(subject, resource, action)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def _get_request_context(request: Request) -> RequestContext:
|
|
380
|
+
"""Create a RequestContext for the current request."""
|
|
381
|
+
return RequestContext(request=request)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# Make RequestContext usable with Depends()
|
|
385
|
+
RequestContext.__call__ = staticmethod(_get_request_context)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# =============================================================================
|
|
389
|
+
# DI Container Integration
|
|
390
|
+
# =============================================================================
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def inject(service_type: Type[T]) -> Callable[..., T]:
|
|
394
|
+
"""Create a dependency that resolves a service from the DI container."""
|
|
395
|
+
|
|
396
|
+
async def _resolve(request: Request) -> T:
|
|
397
|
+
container = getattr(request.app.state, "container", None)
|
|
398
|
+
if container is None:
|
|
399
|
+
container = Container.get_global()
|
|
400
|
+
return container.resolve(service_type)
|
|
401
|
+
|
|
402
|
+
return _resolve
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
Inject = inject
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
__all__ = [
|
|
409
|
+
"get_engine",
|
|
410
|
+
"get_app_slug",
|
|
411
|
+
"get_app_config",
|
|
412
|
+
"get_scoped_db",
|
|
413
|
+
"get_unit_of_work",
|
|
414
|
+
"get_embedding_service",
|
|
415
|
+
"get_memory_service",
|
|
416
|
+
"get_llm_client",
|
|
417
|
+
"get_llm_model_name",
|
|
418
|
+
"get_authz_provider",
|
|
419
|
+
"get_current_user",
|
|
420
|
+
"get_user_roles",
|
|
421
|
+
"require_user",
|
|
422
|
+
"require_role",
|
|
423
|
+
"RequestContext",
|
|
424
|
+
"inject",
|
|
425
|
+
"Inject",
|
|
426
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MDB Engine Dependency Injection Module
|
|
3
|
+
|
|
4
|
+
Enterprise-grade DI container with proper service lifetimes:
|
|
5
|
+
- SINGLETON: One instance per application lifetime
|
|
6
|
+
- REQUEST: One instance per HTTP request
|
|
7
|
+
- TRANSIENT: New instance on every injection
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from mdb_engine.di import Container, Scope, inject
|
|
11
|
+
|
|
12
|
+
# Register services
|
|
13
|
+
container = Container()
|
|
14
|
+
container.register(DatabaseService, scope=Scope.SINGLETON)
|
|
15
|
+
container.register(UserService, scope=Scope.REQUEST)
|
|
16
|
+
|
|
17
|
+
# In FastAPI routes
|
|
18
|
+
@app.get("/users")
|
|
19
|
+
async def get_users(user_svc: UserService = inject(UserService)):
|
|
20
|
+
return await user_svc.list_all()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .container import Container
|
|
24
|
+
from .providers import FactoryProvider, Provider, SingletonProvider
|
|
25
|
+
from .scopes import Scope, ScopeManager
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Container",
|
|
29
|
+
"Scope",
|
|
30
|
+
"ScopeManager",
|
|
31
|
+
"Provider",
|
|
32
|
+
"FactoryProvider",
|
|
33
|
+
"SingletonProvider",
|
|
34
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency Injection Container
|
|
3
|
+
|
|
4
|
+
A lightweight, FastAPI-native DI container with proper service lifetimes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, TypeVar
|
|
9
|
+
|
|
10
|
+
from .scopes import Scope
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .providers import Provider
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Container:
|
|
21
|
+
"""
|
|
22
|
+
Dependency Injection Container with proper service lifetimes.
|
|
23
|
+
|
|
24
|
+
Supports three scopes:
|
|
25
|
+
- SINGLETON: One instance for app lifetime
|
|
26
|
+
- REQUEST: One instance per HTTP request
|
|
27
|
+
- TRANSIENT: New instance on every resolve
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
container = Container()
|
|
31
|
+
|
|
32
|
+
# Register with auto-detection
|
|
33
|
+
container.register(UserService) # Singleton by default
|
|
34
|
+
container.register(RequestContext, scope=Scope.REQUEST)
|
|
35
|
+
|
|
36
|
+
# Register with factory
|
|
37
|
+
container.register_factory(
|
|
38
|
+
Database,
|
|
39
|
+
lambda c: Database(c.resolve(Config).db_url),
|
|
40
|
+
scope=Scope.SINGLETON
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Register instance directly
|
|
44
|
+
container.register_instance(Config, config_instance)
|
|
45
|
+
|
|
46
|
+
# Resolve
|
|
47
|
+
user_svc = container.resolve(UserService)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_global_instance: Optional["Container"] = None
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self._providers: Dict[type, "Provider"] = {}
|
|
54
|
+
self._instances: Dict[type, Any] = {} # For register_instance
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_global(cls) -> "Container":
|
|
58
|
+
"""Get the global container instance."""
|
|
59
|
+
if cls._global_instance is None:
|
|
60
|
+
cls._global_instance = Container()
|
|
61
|
+
return cls._global_instance
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def set_global(cls, container: "Container") -> None:
|
|
65
|
+
"""Set the global container instance."""
|
|
66
|
+
cls._global_instance = container
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def reset_global(cls) -> None:
|
|
70
|
+
"""Reset the global container (useful for testing)."""
|
|
71
|
+
cls._global_instance = None
|
|
72
|
+
|
|
73
|
+
def register(
|
|
74
|
+
self,
|
|
75
|
+
service_type: Type[T],
|
|
76
|
+
implementation: Optional[Type[T]] = None,
|
|
77
|
+
scope: Scope = Scope.SINGLETON,
|
|
78
|
+
) -> "Container":
|
|
79
|
+
"""
|
|
80
|
+
Register a service type with the container.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
service_type: The type to register (interface or concrete class)
|
|
84
|
+
implementation: Optional implementation class (defaults to service_type)
|
|
85
|
+
scope: Service lifetime scope
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Self for chaining
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
container.register(UserService)
|
|
92
|
+
container.register(IRepository, MongoRepository, Scope.REQUEST)
|
|
93
|
+
"""
|
|
94
|
+
from .providers import RequestProvider, SingletonProvider, TransientProvider
|
|
95
|
+
|
|
96
|
+
impl = implementation or service_type
|
|
97
|
+
|
|
98
|
+
if scope == Scope.SINGLETON:
|
|
99
|
+
self._providers[service_type] = SingletonProvider(service_type, impl)
|
|
100
|
+
elif scope == Scope.REQUEST:
|
|
101
|
+
self._providers[service_type] = RequestProvider(service_type, impl)
|
|
102
|
+
else:
|
|
103
|
+
self._providers[service_type] = TransientProvider(service_type, impl)
|
|
104
|
+
|
|
105
|
+
logger.debug(f"Registered {service_type.__name__} as {scope.value}")
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def register_factory(
|
|
109
|
+
self,
|
|
110
|
+
service_type: Type[T],
|
|
111
|
+
factory: Callable[["Container"], T],
|
|
112
|
+
scope: Scope = Scope.SINGLETON,
|
|
113
|
+
) -> "Container":
|
|
114
|
+
"""
|
|
115
|
+
Register a service with a custom factory function.
|
|
116
|
+
|
|
117
|
+
The factory receives the container and can resolve dependencies manually.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
service_type: The type to register
|
|
121
|
+
factory: Factory function (container) -> instance
|
|
122
|
+
scope: Service lifetime scope
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Self for chaining
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
container.register_factory(
|
|
129
|
+
Database,
|
|
130
|
+
lambda c: Database(c.resolve(Config).connection_string),
|
|
131
|
+
Scope.SINGLETON
|
|
132
|
+
)
|
|
133
|
+
"""
|
|
134
|
+
from .providers import FactoryProvider
|
|
135
|
+
|
|
136
|
+
self._providers[service_type] = FactoryProvider(service_type, factory, scope)
|
|
137
|
+
logger.debug(f"Registered factory for {service_type.__name__} as {scope.value}")
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def register_instance(self, service_type: Type[T], instance: T) -> "Container":
|
|
141
|
+
"""
|
|
142
|
+
Register an existing instance as a singleton.
|
|
143
|
+
|
|
144
|
+
Useful for configuration objects or externally created instances.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
service_type: The type to register
|
|
148
|
+
instance: The instance to use
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Self for chaining
|
|
152
|
+
"""
|
|
153
|
+
self._instances[service_type] = instance
|
|
154
|
+
logger.debug(f"Registered instance for {service_type.__name__}")
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def resolve(self, service_type: Type[T]) -> T:
|
|
158
|
+
"""
|
|
159
|
+
Resolve a service instance.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
service_type: The type to resolve
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Service instance
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
KeyError: If service is not registered
|
|
169
|
+
"""
|
|
170
|
+
# Check direct instances first
|
|
171
|
+
if service_type in self._instances:
|
|
172
|
+
return self._instances[service_type]
|
|
173
|
+
|
|
174
|
+
# Check providers
|
|
175
|
+
if service_type not in self._providers:
|
|
176
|
+
raise KeyError(
|
|
177
|
+
f"Service {service_type.__name__} is not registered. "
|
|
178
|
+
f"Call container.register({service_type.__name__}) first."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return self._providers[service_type].get(self)
|
|
182
|
+
|
|
183
|
+
def try_resolve(self, service_type: Type[T]) -> Optional[T]:
|
|
184
|
+
"""
|
|
185
|
+
Try to resolve a service, returning None if not registered.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
service_type: The type to resolve
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Service instance or None
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
return self.resolve(service_type)
|
|
195
|
+
except KeyError:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def is_registered(self, service_type: type) -> bool:
|
|
199
|
+
"""Check if a service type is registered."""
|
|
200
|
+
return service_type in self._providers or service_type in self._instances
|
|
201
|
+
|
|
202
|
+
def reset(self) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Reset all registrations and cached instances.
|
|
205
|
+
|
|
206
|
+
Useful for testing.
|
|
207
|
+
"""
|
|
208
|
+
for provider in self._providers.values():
|
|
209
|
+
if hasattr(provider, "reset"):
|
|
210
|
+
provider.reset()
|
|
211
|
+
self._providers.clear()
|
|
212
|
+
self._instances.clear()
|
|
213
|
+
logger.debug("Container reset")
|
|
214
|
+
|
|
215
|
+
def __contains__(self, service_type: type) -> bool:
|
|
216
|
+
"""Support 'in' operator for checking registration."""
|
|
217
|
+
return self.is_registered(service_type)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# FastAPI integration helpers
|
|
221
|
+
def inject(service_type: Type[T]) -> T:
|
|
222
|
+
"""
|
|
223
|
+
FastAPI dependency that resolves a service from the global container.
|
|
224
|
+
|
|
225
|
+
Usage:
|
|
226
|
+
@app.get("/users")
|
|
227
|
+
async def get_users(user_svc: UserService = Depends(inject(UserService))):
|
|
228
|
+
return await user_svc.list_all()
|
|
229
|
+
|
|
230
|
+
Or with the shorthand:
|
|
231
|
+
@app.get("/users")
|
|
232
|
+
async def get_users(user_svc: UserService = Inject(UserService)):
|
|
233
|
+
return await user_svc.list_all()
|
|
234
|
+
"""
|
|
235
|
+
from fastapi import Request
|
|
236
|
+
|
|
237
|
+
async def _dependency(request: Request) -> T:
|
|
238
|
+
# Get container from app state or use global
|
|
239
|
+
container = getattr(request.app.state, "container", None)
|
|
240
|
+
if container is None:
|
|
241
|
+
container = Container.get_global()
|
|
242
|
+
return container.resolve(service_type)
|
|
243
|
+
|
|
244
|
+
return _dependency
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Alias for cleaner syntax
|
|
248
|
+
Inject = inject
|