mdb-engine 0.1.6__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 (75) hide show
  1. mdb_engine/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,749 @@
1
+ """
2
+ Engine
3
+
4
+ The core orchestration engine for MDB_ENGINE that manages:
5
+ - Database connections
6
+ - Experiment registration
7
+ - Authentication/authorization
8
+ - Index management
9
+ - Resource lifecycle
10
+
11
+ This module is part of MDB_ENGINE - MongoDB Engine.
12
+ """
13
+
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
17
+
18
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
19
+
20
+ if TYPE_CHECKING:
21
+ from ..auth import AuthorizationProvider
22
+ from .types import ManifestDict
23
+
24
+ # Import engine components
25
+ from ..constants import DEFAULT_MAX_POOL_SIZE, DEFAULT_MIN_POOL_SIZE
26
+ from ..database import ScopedMongoWrapper
27
+ from ..observability import (HealthChecker, check_engine_health,
28
+ check_mongodb_health, check_pool_health)
29
+ from ..observability import get_logger as get_contextual_logger
30
+ from .app_registration import AppRegistrationManager
31
+ from .connection import ConnectionManager
32
+ from .index_management import IndexManager
33
+ from .manifest import ManifestParser, ManifestValidator
34
+ from .service_initialization import ServiceInitializer
35
+
36
+ logger = logging.getLogger(__name__)
37
+ # Use contextual logger for better observability
38
+ contextual_logger = get_contextual_logger(__name__)
39
+
40
+
41
+ class MongoDBEngine:
42
+ """
43
+ The MongoDB Engine for managing multi-app applications.
44
+
45
+ This class orchestrates all engine components including:
46
+ - Database connections and scoping
47
+ - Manifest validation and parsing
48
+ - App registration
49
+ - Index management
50
+ - Authentication/authorization setup
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ mongo_uri: str,
56
+ db_name: str,
57
+ manifests_dir: Optional[Path] = None,
58
+ authz_provider: Optional["AuthorizationProvider"] = None,
59
+ max_pool_size: int = DEFAULT_MAX_POOL_SIZE,
60
+ min_pool_size: int = DEFAULT_MIN_POOL_SIZE,
61
+ ) -> None:
62
+ """
63
+ Initialize the MongoDB Engine.
64
+
65
+ Args:
66
+ mongo_uri: MongoDB connection URI
67
+ db_name: Database name
68
+ manifests_dir: Path to manifests directory (optional)
69
+ authz_provider: Authorization provider instance (optional, can be set later)
70
+ max_pool_size: Maximum MongoDB connection pool size
71
+ min_pool_size: Minimum MongoDB connection pool size
72
+ """
73
+ self.mongo_uri = mongo_uri
74
+ self.db_name = db_name
75
+ self.manifests_dir = manifests_dir
76
+ self.authz_provider = authz_provider
77
+ self.max_pool_size = max_pool_size
78
+ self.min_pool_size = min_pool_size
79
+
80
+ # Initialize component managers
81
+ self._connection_manager = ConnectionManager(
82
+ mongo_uri=mongo_uri,
83
+ db_name=db_name,
84
+ max_pool_size=max_pool_size,
85
+ min_pool_size=min_pool_size,
86
+ )
87
+
88
+ # Validators
89
+ self.manifest_validator = ManifestValidator()
90
+ self.manifest_parser = ManifestParser()
91
+
92
+ # Initialize managers (will be set up after connection is established)
93
+ self._app_registration_manager: Optional[AppRegistrationManager] = None
94
+ self._index_manager: Optional[IndexManager] = None
95
+ self._service_initializer: Optional[ServiceInitializer] = None
96
+
97
+ async def initialize(self) -> None:
98
+ """
99
+ Initialize the MongoDB Engine.
100
+
101
+ This method:
102
+ 1. Connects to MongoDB
103
+ 2. Validates the connection
104
+ 3. Sets up initial state
105
+
106
+ Raises:
107
+ InitializationError: If initialization fails (subclass of RuntimeError
108
+ for backward compatibility)
109
+ RuntimeError: If initialization fails (for backward compatibility)
110
+ """
111
+ # Initialize connection
112
+ await self._connection_manager.initialize()
113
+
114
+ # Set up component managers
115
+ self._app_registration_manager = AppRegistrationManager(
116
+ mongo_db=self._connection_manager.mongo_db,
117
+ manifest_validator=self.manifest_validator,
118
+ manifest_parser=self.manifest_parser,
119
+ )
120
+
121
+ self._index_manager = IndexManager(mongo_db=self._connection_manager.mongo_db)
122
+
123
+ self._service_initializer = ServiceInitializer(
124
+ mongo_uri=self.mongo_uri,
125
+ db_name=self.db_name,
126
+ get_scoped_db_fn=self.get_scoped_db,
127
+ )
128
+
129
+ @property
130
+ def mongo_client(self) -> AsyncIOMotorClient:
131
+ """
132
+ Get the MongoDB client.
133
+
134
+ Returns:
135
+ AsyncIOMotorClient instance
136
+
137
+ Raises:
138
+ RuntimeError: If engine is not initialized
139
+ """
140
+ return self._connection_manager.mongo_client
141
+
142
+ @property
143
+ def mongo_db(self) -> AsyncIOMotorDatabase:
144
+ """
145
+ Get the MongoDB database.
146
+
147
+ Returns:
148
+ AsyncIOMotorDatabase instance
149
+
150
+ Raises:
151
+ RuntimeError: If engine is not initialized
152
+ """
153
+ return self._connection_manager.mongo_db
154
+
155
+ @property
156
+ def _initialized(self) -> bool:
157
+ """Check if engine is initialized."""
158
+ return self._connection_manager.initialized
159
+
160
+ def get_scoped_db(
161
+ self,
162
+ app_slug: str,
163
+ read_scopes: Optional[List[str]] = None,
164
+ write_scope: Optional[str] = None,
165
+ auto_index: bool = True,
166
+ ) -> ScopedMongoWrapper:
167
+ """
168
+ Get a scoped database wrapper for an app.
169
+
170
+ The scoped database wrapper automatically filters queries by app_id
171
+ to ensure data isolation between apps. All read operations are
172
+ scoped to the specified read_scopes, and all write operations are
173
+ tagged with the write_scope.
174
+
175
+ Args:
176
+ app_slug: App slug (used as default for both read and write scopes)
177
+ read_scopes: List of app slugs to read from. If None, defaults to
178
+ [app_slug]. Allows cross-app data access when needed.
179
+ write_scope: App slug to write to. If None, defaults to app_slug.
180
+ All documents inserted through this wrapper will have this as their
181
+ app_id.
182
+ auto_index: Whether to enable automatic index creation based on query
183
+ patterns. Defaults to True. Set to False to disable automatic indexing.
184
+
185
+ Returns:
186
+ ScopedMongoWrapper instance configured with the specified scopes.
187
+
188
+ Raises:
189
+ RuntimeError: If engine is not initialized.
190
+
191
+ Example:
192
+ >>> db = engine.get_scoped_db("my_app")
193
+ >>> # All queries are automatically scoped to "my_app"
194
+ >>> doc = await db.my_collection.find_one({"name": "test"})
195
+ """
196
+ if not self._initialized:
197
+ raise RuntimeError(
198
+ "MongoDBEngine not initialized. Call initialize() first."
199
+ )
200
+
201
+ if read_scopes is None:
202
+ read_scopes = [app_slug]
203
+ if write_scope is None:
204
+ write_scope = app_slug
205
+
206
+ return ScopedMongoWrapper(
207
+ real_db=self._connection_manager.mongo_db,
208
+ read_scopes=read_scopes,
209
+ write_scope=write_scope,
210
+ auto_index=auto_index,
211
+ )
212
+
213
+ async def validate_manifest(
214
+ self, manifest: "ManifestDict"
215
+ ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
216
+ """
217
+ Validate a manifest against the schema.
218
+
219
+ Args:
220
+ manifest: Manifest dictionary to validate. Must be a valid
221
+ dictionary containing experiment configuration.
222
+
223
+ Returns:
224
+ Tuple of (is_valid, error_message, error_paths):
225
+ - is_valid: True if manifest is valid, False otherwise
226
+ - error_message: Human-readable error message if invalid, None if valid
227
+ - error_paths: List of JSON paths with validation errors, None if valid
228
+ """
229
+ if not self._app_registration_manager:
230
+ raise RuntimeError(
231
+ "MongoDBEngine not initialized. Call initialize() first."
232
+ )
233
+ return await self._app_registration_manager.validate_manifest(manifest)
234
+
235
+ async def load_manifest(self, path: Path) -> "ManifestDict":
236
+ """
237
+ Load and validate a manifest from a file.
238
+
239
+ Args:
240
+ path: Path to manifest.json file
241
+
242
+ Returns:
243
+ Validated manifest dictionary
244
+
245
+ Raises:
246
+ FileNotFoundError: If file doesn't exist
247
+ ValueError: If validation fails
248
+ """
249
+ if not self._app_registration_manager:
250
+ raise RuntimeError(
251
+ "MongoDBEngine not initialized. Call initialize() first."
252
+ )
253
+ return await self._app_registration_manager.load_manifest(path)
254
+
255
+ async def register_app(
256
+ self, manifest: "ManifestDict", create_indexes: bool = True
257
+ ) -> bool:
258
+ """
259
+ Register an app from its manifest.
260
+
261
+ This method validates the manifest, stores the app configuration,
262
+ and optionally creates managed indexes defined in the manifest.
263
+
264
+ Args:
265
+ manifest: Validated manifest dictionary containing app
266
+ configuration. Must include 'slug' field.
267
+ create_indexes: Whether to create managed indexes defined in
268
+ the manifest. Defaults to True.
269
+
270
+ Returns:
271
+ True if registration successful, False otherwise.
272
+ Returns False if manifest validation fails or slug is missing.
273
+
274
+ Raises:
275
+ RuntimeError: If engine is not initialized.
276
+ """
277
+ if not self._app_registration_manager:
278
+ raise RuntimeError(
279
+ "MongoDBEngine not initialized. Call initialize() first."
280
+ )
281
+
282
+ # Create callbacks for service initialization
283
+ async def create_indexes_callback(slug: str, manifest: "ManifestDict") -> None:
284
+ if self._index_manager and create_indexes:
285
+ await self._index_manager.create_app_indexes(slug, manifest)
286
+
287
+ async def seed_data_callback(slug: str, initial_data: Dict[str, Any]) -> None:
288
+ if self._service_initializer:
289
+ await self._service_initializer.seed_initial_data(slug, initial_data)
290
+
291
+ async def initialize_memory_callback(
292
+ slug: str, memory_config: Dict[str, Any]
293
+ ) -> None:
294
+ if self._service_initializer:
295
+ await self._service_initializer.initialize_memory_service(
296
+ slug, memory_config
297
+ )
298
+
299
+ async def register_websockets_callback(
300
+ slug: str, websockets_config: Dict[str, Any]
301
+ ) -> None:
302
+ if self._service_initializer:
303
+ await self._service_initializer.register_websockets(
304
+ slug, websockets_config
305
+ )
306
+
307
+ async def setup_observability_callback(
308
+ slug: str,
309
+ manifest: "ManifestDict",
310
+ observability_config: Dict[str, Any],
311
+ ) -> None:
312
+ if self._service_initializer:
313
+ await self._service_initializer.setup_observability(
314
+ slug, manifest, observability_config
315
+ )
316
+
317
+ return await self._app_registration_manager.register_app(
318
+ manifest=manifest,
319
+ create_indexes_callback=create_indexes_callback if create_indexes else None,
320
+ seed_data_callback=seed_data_callback,
321
+ initialize_memory_callback=initialize_memory_callback,
322
+ register_websockets_callback=register_websockets_callback,
323
+ setup_observability_callback=setup_observability_callback,
324
+ )
325
+
326
+ def get_websocket_config(self, slug: str) -> Optional[Dict[str, Any]]:
327
+ """
328
+ Get WebSocket configuration for an app.
329
+
330
+ Args:
331
+ slug: App slug
332
+
333
+ Returns:
334
+ WebSocket configuration dict or None if not configured
335
+ """
336
+ if self._service_initializer:
337
+ return self._service_initializer.get_websocket_config(slug)
338
+ return None
339
+
340
+ def register_websocket_routes(self, app: Any, slug: str) -> None:
341
+ """
342
+ Register WebSocket routes with a FastAPI app.
343
+
344
+ WebSocket support is OPTIONAL - only enabled if:
345
+ 1. App defines "websockets" in manifest.json
346
+ 2. WebSocket dependencies are available
347
+
348
+ This should be called after the FastAPI app is created to actually
349
+ mount the WebSocket endpoints.
350
+
351
+ Args:
352
+ app: FastAPI application instance
353
+ slug: App slug
354
+ """
355
+ # Check if WebSockets are configured for this app
356
+ websockets_config = self.get_websocket_config(slug)
357
+ if not websockets_config:
358
+ contextual_logger.debug(
359
+ f"No WebSocket configuration found for app '{slug}' - WebSocket support disabled"
360
+ )
361
+ return
362
+
363
+ # Try to import WebSocket support (optional dependency)
364
+ try:
365
+ from ..routing.websockets import create_websocket_endpoint
366
+ except ImportError as e:
367
+ contextual_logger.warning(
368
+ f"WebSocket support requested for app '{slug}' but "
369
+ f"dependencies are not available: {e}. "
370
+ f"WebSocket routes will not be registered. "
371
+ f"Install FastAPI with WebSocket support."
372
+ )
373
+ return
374
+
375
+ for endpoint_name, endpoint_config in websockets_config.items():
376
+ path = endpoint_config.get("path", f"/{endpoint_name}")
377
+
378
+ # Handle auth configuration - use app's auth_policy as default
379
+ # Support both new nested format and old top-level format for backward compatibility
380
+ auth_config = endpoint_config.get("auth", {})
381
+ if isinstance(auth_config, dict) and "required" in auth_config:
382
+ require_auth = auth_config.get("required", True)
383
+ elif "require_auth" in endpoint_config:
384
+ # Backward compatibility: if "require_auth" is at top level
385
+ require_auth = endpoint_config.get("require_auth", True)
386
+ else:
387
+ # Default: use app's auth_policy if available, otherwise require auth
388
+ app_config = self.get_app(slug)
389
+ if app_config and "auth_policy" in app_config:
390
+ require_auth = app_config["auth_policy"].get("required", True)
391
+ else:
392
+ require_auth = True # Secure default
393
+
394
+ ping_interval = endpoint_config.get("ping_interval", 30)
395
+
396
+ # Create the endpoint handler with app isolation
397
+ # Note: Apps can register message handlers later using register_message_handler()
398
+ try:
399
+ handler = create_websocket_endpoint(
400
+ app_slug=slug,
401
+ path=path,
402
+ endpoint_name=endpoint_name, # Pass endpoint name for handler lookup
403
+ handler=None, # Handlers registered via register_message_handler()
404
+ require_auth=require_auth,
405
+ ping_interval=ping_interval,
406
+ )
407
+ print(
408
+ f"✅ Created WebSocket handler for '{path}' "
409
+ f"(type: {type(handler).__name__}, "
410
+ f"callable: {callable(handler)})"
411
+ )
412
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
413
+ print(f"❌ Failed to create WebSocket handler for '{path}': {e}")
414
+ import traceback
415
+
416
+ traceback.print_exc()
417
+ raise
418
+
419
+ # Register with FastAPI - automatically scoped to this app
420
+ try:
421
+ # FastAPI WebSocket registration - use APIRouter approach (most reliable)
422
+ from fastapi import APIRouter
423
+
424
+ # Create a router for this WebSocket route
425
+ ws_router = APIRouter()
426
+ ws_router.websocket(path)(handler)
427
+
428
+ # Include the router in the app
429
+ app.include_router(ws_router)
430
+
431
+ print(
432
+ f"✅ Registered WebSocket route '{path}' for app '{slug}' using APIRouter"
433
+ )
434
+ print(
435
+ f" Handler type: {type(handler).__name__}, Callable: {callable(handler)}"
436
+ )
437
+ print(
438
+ f" Route name: {slug}_{endpoint_name}, Auth required: {require_auth}"
439
+ )
440
+ print(f" Route path: {path}, Full route count: {len(app.routes)}")
441
+ contextual_logger.info(
442
+ f"✅ Registered WebSocket route '{path}' for app '{slug}' "
443
+ f"(auth: {require_auth})",
444
+ extra={
445
+ "app_slug": slug,
446
+ "path": path,
447
+ "endpoint": endpoint_name,
448
+ "require_auth": require_auth,
449
+ },
450
+ )
451
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
452
+ contextual_logger.error(
453
+ f"❌ Failed to register WebSocket route '{path}' for app '{slug}': {e}",
454
+ exc_info=True,
455
+ extra={
456
+ "app_slug": slug,
457
+ "path": path,
458
+ "endpoint": endpoint_name,
459
+ "error": str(e),
460
+ },
461
+ )
462
+ print(
463
+ f"❌ Failed to register WebSocket route '{path}' for app '{slug}': {e}"
464
+ )
465
+ import traceback
466
+
467
+ traceback.print_exc()
468
+ raise
469
+
470
+ async def reload_apps(self) -> int:
471
+ """
472
+ Reload all active apps from the database.
473
+
474
+ This method fetches all apps with status "active" from the
475
+ apps_config collection and registers them. Existing
476
+ app registrations are cleared before reloading.
477
+
478
+ Returns:
479
+ Number of apps successfully registered.
480
+ Returns 0 if an error occurs during reload.
481
+
482
+ Raises:
483
+ RuntimeError: If engine is not initialized.
484
+ """
485
+ if not self._app_registration_manager:
486
+ raise RuntimeError(
487
+ "MongoDBEngine not initialized. Call initialize() first."
488
+ )
489
+
490
+ return await self._app_registration_manager.reload_apps(
491
+ register_app_callback=self.register_app
492
+ )
493
+
494
+ def get_app(self, slug: str) -> Optional["ManifestDict"]:
495
+ """
496
+ Get app configuration by slug.
497
+
498
+ Args:
499
+ slug: App slug
500
+
501
+ Returns:
502
+ App manifest dict or None if not found
503
+ """
504
+ if not self._app_registration_manager:
505
+ raise RuntimeError(
506
+ "MongoDBEngine not initialized. Call initialize() first."
507
+ )
508
+ return self._app_registration_manager.get_app(slug)
509
+
510
+ async def get_manifest(self, slug: str) -> Optional["ManifestDict"]:
511
+ """
512
+ Get app manifest by slug (async alias for get_app).
513
+
514
+ Args:
515
+ slug: App slug
516
+
517
+ Returns:
518
+ App manifest dict or None if not found
519
+ """
520
+ if not self._app_registration_manager:
521
+ raise RuntimeError(
522
+ "MongoDBEngine not initialized. Call initialize() first."
523
+ )
524
+ return await self._app_registration_manager.get_manifest(slug)
525
+
526
+ def get_database(self) -> AsyncIOMotorDatabase:
527
+ """
528
+ Get the MongoDB database instance.
529
+
530
+ Returns:
531
+ AsyncIOMotorDatabase instance
532
+ """
533
+ return self.mongo_db
534
+
535
+ def get_memory_service(self, slug: str) -> Optional[Any]:
536
+ """
537
+ Get Mem0 memory service for an app.
538
+
539
+ Args:
540
+ slug: App slug
541
+
542
+ Returns:
543
+ Mem0MemoryService instance if memory is enabled for this app, None otherwise
544
+
545
+ Example:
546
+ ```python
547
+ memory_service = engine.get_memory_service("my_app")
548
+ if memory_service:
549
+ memories = memory_service.add(
550
+ messages=[{"role": "user", "content": "I love sci-fi movies"}],
551
+ user_id="alice"
552
+ )
553
+ ```
554
+ """
555
+ if self._service_initializer:
556
+ return self._service_initializer.get_memory_service(slug)
557
+ return None
558
+
559
+ @property
560
+ def _apps(self) -> Dict[str, Any]:
561
+ """
562
+ Get the apps dictionary (for backward compatibility with tests).
563
+
564
+ Returns:
565
+ Dictionary of registered apps
566
+
567
+ Raises:
568
+ RuntimeError: If engine is not initialized
569
+ """
570
+ if not self._app_registration_manager:
571
+ raise RuntimeError(
572
+ "MongoDBEngine not initialized. Call initialize() first."
573
+ )
574
+ return self._app_registration_manager._apps
575
+
576
+ def list_apps(self) -> List[str]:
577
+ """
578
+ List all registered app slugs.
579
+
580
+ Returns:
581
+ List of app slugs
582
+ """
583
+ if not self._app_registration_manager:
584
+ raise RuntimeError(
585
+ "MongoDBEngine not initialized. Call initialize() first."
586
+ )
587
+ return self._app_registration_manager.list_apps()
588
+
589
+ async def shutdown(self) -> None:
590
+ """
591
+ Shutdown the MongoDB Engine and clean up resources.
592
+
593
+ This method:
594
+ 1. Closes MongoDB connections
595
+ 2. Clears app registrations
596
+ 3. Resets initialization state
597
+
598
+ This method is idempotent - it's safe to call multiple times.
599
+ """
600
+ if self._service_initializer:
601
+ self._service_initializer.clear_services()
602
+
603
+ if self._app_registration_manager:
604
+ self._app_registration_manager.clear_apps()
605
+
606
+ await self._connection_manager.shutdown()
607
+
608
+ def __enter__(self) -> "MongoDBEngine":
609
+ """
610
+ Context manager entry (synchronous).
611
+
612
+ Note: This is synchronous and does not initialize the engine.
613
+ For async initialization, use async context manager (async with).
614
+
615
+ Returns:
616
+ MongoDBEngine instance
617
+ """
618
+ return self
619
+
620
+ def __exit__(
621
+ self,
622
+ exc_type: Optional[type[BaseException]],
623
+ exc_val: Optional[BaseException],
624
+ exc_tb: Optional[Any],
625
+ ) -> None:
626
+ """
627
+ Context manager exit (synchronous).
628
+
629
+ Note: This is synchronous, so we can't await shutdown.
630
+ Users should call await shutdown() explicitly or use async context manager.
631
+
632
+ Args:
633
+ exc_type: Exception type (if any)
634
+ exc_val: Exception value (if any)
635
+ exc_tb: Exception traceback (if any)
636
+ """
637
+ # Note: This is synchronous, so we can't await shutdown
638
+ # Users should call await shutdown() explicitly
639
+ pass
640
+
641
+ async def __aenter__(self) -> "MongoDBEngine":
642
+ """
643
+ Async context manager entry.
644
+
645
+ Automatically initializes the engine when entering the context.
646
+
647
+ Returns:
648
+ Initialized MongoDBEngine instance
649
+ """
650
+ await self.initialize()
651
+ return self
652
+
653
+ async def __aexit__(
654
+ self,
655
+ exc_type: Optional[type[BaseException]],
656
+ exc_val: Optional[BaseException],
657
+ exc_tb: Optional[Any],
658
+ ) -> None:
659
+ """
660
+ Async context manager exit.
661
+
662
+ Automatically shuts down the engine when exiting the context.
663
+
664
+ Args:
665
+ exc_type: Exception type (if any)
666
+ exc_val: Exception value (if any)
667
+ exc_tb: Exception traceback (if any)
668
+ """
669
+ await self.shutdown()
670
+
671
+ async def get_health_status(self) -> Dict[str, Any]:
672
+ """
673
+ Get health status of the MongoDB Engine.
674
+
675
+ Returns:
676
+ Dictionary with health status and component checks
677
+ """
678
+ health_checker = HealthChecker()
679
+
680
+ # Register health checks
681
+ health_checker.register_check(lambda: check_engine_health(self))
682
+ health_checker.register_check(
683
+ lambda: check_mongodb_health(self._connection_manager.mongo_client)
684
+ )
685
+
686
+ # Add pool health check if available (but don't fail overall health if it's just a warning)
687
+ try:
688
+ from ..database.connection import get_pool_metrics
689
+
690
+ async def pool_check_wrapper():
691
+ # Pass MongoDBEngine's client and pool config to get_pool_metrics
692
+ # for accurate monitoring
693
+ # This follows MongoDB best practice: monitor the actual client
694
+ # being used
695
+ async def get_metrics():
696
+ metrics = await get_pool_metrics(
697
+ self._connection_manager.mongo_client
698
+ )
699
+ # Add MongoDBEngine's pool configuration if not already in metrics
700
+ if metrics.get("status") == "connected":
701
+ if (
702
+ "max_pool_size" not in metrics
703
+ or metrics.get("max_pool_size") is None
704
+ ):
705
+ metrics["max_pool_size"] = self.max_pool_size
706
+ if (
707
+ "min_pool_size" not in metrics
708
+ or metrics.get("min_pool_size") is None
709
+ ):
710
+ metrics["min_pool_size"] = self.min_pool_size
711
+ return metrics
712
+
713
+ result = await check_pool_health(get_metrics)
714
+ # Only treat pool issues as unhealthy if usage is critical (>90%)
715
+ # Otherwise treat as degraded or healthy
716
+ if result.status.value == "unhealthy":
717
+ # Check if it's a critical pool usage issue
718
+ details = result.details or {}
719
+ usage = details.get("pool_usage_percent", 0)
720
+ if usage <= 90 and details.get("status") == "connected":
721
+ # Not critical, downgrade to degraded
722
+ from ..observability.health import (HealthCheckResult,
723
+ HealthStatus)
724
+
725
+ return HealthCheckResult(
726
+ name=result.name,
727
+ status=HealthStatus.DEGRADED,
728
+ message=result.message,
729
+ details=result.details,
730
+ )
731
+ return result
732
+
733
+ health_checker.register_check(pool_check_wrapper)
734
+ except ImportError:
735
+ pass
736
+
737
+ return await health_checker.check_all()
738
+
739
+ def get_metrics(self) -> Dict[str, Any]:
740
+ """
741
+ Get metrics for the MongoDB Engine.
742
+
743
+ Returns:
744
+ Dictionary with operation metrics
745
+ """
746
+ from ..observability import get_metrics_collector
747
+
748
+ collector = get_metrics_collector()
749
+ return collector.get_summary()