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