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.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {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