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,42 @@
1
+ """
2
+ Core MongoDB Engine components.
3
+
4
+ This module contains the main MongoDBEngine class and core
5
+ orchestration logic for managing apps.
6
+ """
7
+
8
+ from .engine import MongoDBEngine
9
+ from .manifest import ( # Classes; Constants; Functions (for backward compatibility); Schemas
10
+ CURRENT_SCHEMA_VERSION, DEFAULT_SCHEMA_VERSION, MANIFEST_SCHEMA,
11
+ MANIFEST_SCHEMA_V1, MANIFEST_SCHEMA_V2, SCHEMA_REGISTRY, ManifestParser,
12
+ ManifestValidator, clear_validation_cache, get_schema_for_version,
13
+ get_schema_version, migrate_manifest, validate_developer_id,
14
+ validate_index_definition, validate_managed_indexes, validate_manifest,
15
+ validate_manifest_with_db, validate_manifests_parallel)
16
+
17
+ __all__ = [
18
+ # MongoDB Engine
19
+ "MongoDBEngine",
20
+ # Classes
21
+ "ManifestValidator",
22
+ "ManifestParser",
23
+ # Constants
24
+ "CURRENT_SCHEMA_VERSION",
25
+ "DEFAULT_SCHEMA_VERSION",
26
+ # Functions
27
+ "validate_manifest",
28
+ "validate_manifest_with_db",
29
+ "validate_managed_indexes",
30
+ "validate_index_definition",
31
+ "validate_developer_id",
32
+ "get_schema_version",
33
+ "migrate_manifest",
34
+ "get_schema_for_version",
35
+ "clear_validation_cache",
36
+ "validate_manifests_parallel",
37
+ # Schemas
38
+ "MANIFEST_SCHEMA_V1",
39
+ "MANIFEST_SCHEMA_V2",
40
+ "MANIFEST_SCHEMA",
41
+ "SCHEMA_REGISTRY",
42
+ ]
@@ -0,0 +1,392 @@
1
+ """
2
+ App registration management for MongoDB Engine.
3
+
4
+ This module handles app registration, manifest validation, persistence,
5
+ and app state management.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ import time
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
15
+
16
+ from motor.motor_asyncio import AsyncIOMotorDatabase
17
+ from pymongo.errors import (ConnectionFailure, InvalidOperation,
18
+ OperationFailure, ServerSelectionTimeoutError)
19
+
20
+ from ..observability import clear_app_context
21
+ from ..observability import get_logger as get_contextual_logger
22
+ from ..observability import record_operation, set_app_context
23
+ from .manifest import ManifestParser, ManifestValidator
24
+
25
+ if TYPE_CHECKING:
26
+ from .types import ManifestDict
27
+
28
+ logger = logging.getLogger(__name__)
29
+ contextual_logger = get_contextual_logger(__name__)
30
+
31
+
32
+ class AppRegistrationManager:
33
+ """
34
+ Manages app registration and manifest handling.
35
+
36
+ Handles manifest validation, app state management, and persistence.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ mongo_db: AsyncIOMotorDatabase,
42
+ manifest_validator: ManifestValidator,
43
+ manifest_parser: ManifestParser,
44
+ ) -> None:
45
+ """
46
+ Initialize the app registration manager.
47
+
48
+ Args:
49
+ mongo_db: MongoDB database instance
50
+ manifest_validator: Manifest validator instance
51
+ manifest_parser: Manifest parser instance
52
+ """
53
+ self._mongo_db = mongo_db
54
+ self.manifest_validator = manifest_validator
55
+ self.manifest_parser = manifest_parser
56
+ self._apps: Dict[str, Dict[str, Any]] = {}
57
+
58
+ async def validate_manifest(
59
+ self, manifest: "ManifestDict"
60
+ ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
61
+ """
62
+ Validate a manifest against the schema.
63
+
64
+ Args:
65
+ manifest: Manifest dictionary to validate
66
+
67
+ Returns:
68
+ Tuple of (is_valid, error_message, error_paths)
69
+ """
70
+ start_time = time.time()
71
+ slug = manifest.get("slug", "unknown")
72
+
73
+ try:
74
+ result = self.manifest_validator.validate(manifest)
75
+ duration_ms = (time.time() - start_time) * 1000
76
+ is_valid = result[0]
77
+ record_operation(
78
+ "app_registration.validate_manifest",
79
+ duration_ms,
80
+ success=is_valid,
81
+ experiment_slug=slug,
82
+ )
83
+ return result
84
+ except Exception:
85
+ duration_ms = (time.time() - start_time) * 1000
86
+ record_operation(
87
+ "app_registration.validate_manifest",
88
+ duration_ms,
89
+ success=False,
90
+ experiment_slug=slug,
91
+ )
92
+ raise
93
+
94
+ async def load_manifest(self, path: Path) -> "ManifestDict":
95
+ """
96
+ Load and validate a manifest from a file.
97
+
98
+ Args:
99
+ path: Path to manifest.json file
100
+
101
+ Returns:
102
+ Validated manifest dictionary
103
+
104
+ Raises:
105
+ FileNotFoundError: If file doesn't exist
106
+ ValueError: If validation fails
107
+ """
108
+ return await self.manifest_parser.load_from_file(path, validate=True)
109
+
110
+ async def register_app(
111
+ self,
112
+ manifest: "ManifestDict",
113
+ create_indexes_callback: Optional[Callable[[str, "ManifestDict"], Any]] = None,
114
+ seed_data_callback: Optional[
115
+ Callable[[str, Dict[str, List[Dict[str, Any]]]], Any]
116
+ ] = None,
117
+ initialize_memory_callback: Optional[
118
+ Callable[[str, Dict[str, Any]], Any]
119
+ ] = None,
120
+ register_websockets_callback: Optional[
121
+ Callable[[str, Dict[str, Any]], Any]
122
+ ] = None,
123
+ setup_observability_callback: Optional[
124
+ Callable[[str, "ManifestDict", Dict[str, Any]], Any]
125
+ ] = None,
126
+ ) -> bool:
127
+ """
128
+ Register an app from its manifest.
129
+
130
+ This method validates the manifest, stores the app configuration,
131
+ and optionally creates managed indexes defined in the manifest.
132
+
133
+ Args:
134
+ manifest: Validated manifest dictionary containing app configuration
135
+ create_indexes_callback: Optional callback to create indexes
136
+ seed_data_callback: Optional callback to seed initial data
137
+ initialize_memory_callback: Optional callback to initialize memory service
138
+ register_websockets_callback: Optional callback to register WebSockets
139
+ setup_observability_callback: Optional callback to setup observability
140
+
141
+ Returns:
142
+ True if registration successful, False otherwise
143
+ """
144
+ start_time = time.time()
145
+
146
+ slug: Optional[str] = manifest.get("slug")
147
+ if not slug:
148
+ contextual_logger.error(
149
+ "Cannot register app: missing 'slug' in manifest",
150
+ extra={"operation": "register_app"},
151
+ )
152
+ return False
153
+
154
+ # Set app context for logging
155
+ set_app_context(app_slug=slug)
156
+
157
+ try:
158
+ # Normalize manifest: convert Python tuples to lists for JSON schema compatibility
159
+ from .manifest import _convert_tuples_to_lists
160
+
161
+ normalized_manifest = _convert_tuples_to_lists(manifest)
162
+
163
+ # Validate manifest
164
+ is_valid, error, paths = await self.validate_manifest(normalized_manifest)
165
+ if not is_valid:
166
+ duration_ms = (time.time() - start_time) * 1000
167
+ logger.error(
168
+ f"[{slug}] ❌ Manifest validation FAILED: {error}. "
169
+ f"Error paths: {paths}. "
170
+ f"This blocks app registration and index creation!"
171
+ )
172
+ record_operation(
173
+ "app_registration.register_app",
174
+ duration_ms,
175
+ success=False,
176
+ experiment_slug=slug,
177
+ )
178
+ contextual_logger.error(
179
+ "App registration blocked: Manifest validation failed",
180
+ extra={
181
+ "experiment_slug": slug,
182
+ "validation_error": error,
183
+ "error_paths": paths,
184
+ "duration_ms": round(duration_ms, 2),
185
+ },
186
+ )
187
+ return False
188
+
189
+ # Use normalized manifest for rest of registration
190
+ manifest = normalized_manifest
191
+
192
+ # Store app config in memory
193
+ self._apps[slug] = manifest
194
+
195
+ # Persist app config to MongoDB
196
+ try:
197
+ await self._mongo_db.apps_config.replace_one(
198
+ {"slug": slug},
199
+ manifest,
200
+ upsert=True,
201
+ )
202
+ except (
203
+ ConnectionFailure,
204
+ ServerSelectionTimeoutError,
205
+ OperationFailure,
206
+ ) as e:
207
+ logger.warning(
208
+ f"Failed to persist app '{slug}' to MongoDB: {e}",
209
+ exc_info=True,
210
+ )
211
+ # Continue even if persistence fails - app is still registered in memory
212
+ except InvalidOperation as e:
213
+ logger.debug(
214
+ f"Cannot persist app '{slug}': MongoDB client is closed: {e}"
215
+ )
216
+ # Continue - app is still registered in memory
217
+
218
+ # Invalidate auth config cache for this app
219
+ try:
220
+ from ..auth.integration import invalidate_auth_config_cache
221
+
222
+ invalidate_auth_config_cache(slug)
223
+ except (AttributeError, ImportError, RuntimeError) as e:
224
+ logger.debug(f"Could not invalidate auth config cache for {slug}: {e}")
225
+
226
+ # Build list of callbacks to run in parallel
227
+ callback_tasks = []
228
+
229
+ # Create indexes if requested
230
+ if create_indexes_callback and "managed_indexes" in manifest:
231
+ logger.info(
232
+ f"[{slug}] Creating managed indexes " f"(has_managed_indexes=True)"
233
+ )
234
+ callback_tasks.append(create_indexes_callback(slug, manifest))
235
+
236
+ # Seed initial data if configured
237
+ if seed_data_callback and "initial_data" in manifest:
238
+ callback_tasks.append(
239
+ seed_data_callback(slug, manifest["initial_data"])
240
+ )
241
+
242
+ # Initialize Memory service if configured
243
+ memory_config = manifest.get("memory_config")
244
+ if (
245
+ initialize_memory_callback
246
+ and memory_config
247
+ and memory_config.get("enabled", False)
248
+ ):
249
+ callback_tasks.append(initialize_memory_callback(slug, memory_config))
250
+
251
+ # Register WebSocket endpoints if configured
252
+ websockets_config = manifest.get("websockets")
253
+ if register_websockets_callback and websockets_config:
254
+ callback_tasks.append(
255
+ register_websockets_callback(slug, websockets_config)
256
+ )
257
+
258
+ # Set up observability (health checks, metrics, logging)
259
+ observability_config = manifest.get("observability", {})
260
+ if setup_observability_callback and observability_config:
261
+ callback_tasks.append(
262
+ setup_observability_callback(slug, manifest, observability_config)
263
+ )
264
+
265
+ # Run all callbacks in parallel
266
+ if callback_tasks:
267
+ results = await asyncio.gather(*callback_tasks, return_exceptions=True)
268
+ # Log any exceptions but don't fail registration
269
+ for i, result in enumerate(results):
270
+ if isinstance(result, Exception):
271
+ callback_names = [
272
+ "create_indexes",
273
+ "seed_data",
274
+ "initialize_memory",
275
+ "register_websockets",
276
+ "setup_observability",
277
+ ]
278
+ callback_name = (
279
+ callback_names[i] if i < len(callback_names) else "unknown"
280
+ )
281
+ logger.warning(
282
+ f"[{slug}] Callback '{callback_name}' failed during "
283
+ f"app registration: {result}",
284
+ exc_info=result,
285
+ )
286
+
287
+ duration_ms = (time.time() - start_time) * 1000
288
+ record_operation(
289
+ "app_registration.register_app",
290
+ duration_ms,
291
+ success=True,
292
+ app_slug=slug,
293
+ )
294
+ contextual_logger.info(
295
+ "App registered successfully",
296
+ extra={
297
+ "app_slug": slug,
298
+ "memory_enabled": bool(
299
+ memory_config and memory_config.get("enabled", False)
300
+ ),
301
+ "websockets_configured": bool(websockets_config),
302
+ "duration_ms": round(duration_ms, 2),
303
+ },
304
+ )
305
+ return True
306
+ finally:
307
+ clear_app_context()
308
+
309
+ async def reload_apps(
310
+ self,
311
+ register_app_callback: Any,
312
+ ) -> int:
313
+ """
314
+ Reload all active apps from the database.
315
+
316
+ Args:
317
+ register_app_callback: Callback to register each app
318
+
319
+ Returns:
320
+ Number of apps successfully registered
321
+ """
322
+ logger.info("Reloading active apps from database...")
323
+
324
+ try:
325
+ # Fetch active apps
326
+ active_cfgs = (
327
+ await self._mongo_db.apps_config.find({"status": "active"})
328
+ .limit(500)
329
+ .to_list(None)
330
+ )
331
+
332
+ logger.info(f"Found {len(active_cfgs)} active app(s).")
333
+
334
+ # Clear existing registrations
335
+ self._apps.clear()
336
+
337
+ # Register each app
338
+ registered_count = 0
339
+ for cfg in active_cfgs:
340
+ success = await register_app_callback(cfg, create_indexes=True)
341
+ if success:
342
+ registered_count += 1
343
+
344
+ logger.info(f"✔️ App reload complete. {registered_count} app(s) registered.")
345
+ return registered_count
346
+ except (
347
+ OperationFailure,
348
+ ConnectionFailure,
349
+ ServerSelectionTimeoutError,
350
+ ValueError,
351
+ TypeError,
352
+ KeyError,
353
+ ) as e:
354
+ logger.error(f"❌ Error reloading apps: {e}", exc_info=True)
355
+ return 0
356
+
357
+ def get_app(self, slug: str) -> Optional["ManifestDict"]:
358
+ """
359
+ Get app configuration by slug.
360
+
361
+ Args:
362
+ slug: App slug
363
+
364
+ Returns:
365
+ App manifest dict or None if not found
366
+ """
367
+ return self._apps.get(slug)
368
+
369
+ async def get_manifest(self, slug: str) -> Optional["ManifestDict"]:
370
+ """
371
+ Get app manifest by slug (async alias for get_app).
372
+
373
+ Args:
374
+ slug: App slug
375
+
376
+ Returns:
377
+ App manifest dict or None if not found
378
+ """
379
+ return self._apps.get(slug)
380
+
381
+ def list_apps(self) -> List[str]:
382
+ """
383
+ List all registered app slugs.
384
+
385
+ Returns:
386
+ List of app slugs
387
+ """
388
+ return list(self._apps.keys())
389
+
390
+ def clear_apps(self) -> None:
391
+ """Clear all registered apps."""
392
+ self._apps.clear()
@@ -0,0 +1,243 @@
1
+ """
2
+ Connection management for MongoDB Engine.
3
+
4
+ This module handles MongoDB connection initialization, shutdown, and
5
+ connection pool configuration.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+ """
9
+
10
+ import logging
11
+ import time
12
+ from typing import Optional
13
+
14
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
15
+ from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
16
+
17
+ from ..constants import (DEFAULT_MAX_IDLE_TIME_MS, DEFAULT_MAX_POOL_SIZE,
18
+ DEFAULT_MIN_POOL_SIZE,
19
+ DEFAULT_SERVER_SELECTION_TIMEOUT_MS)
20
+ from ..exceptions import InitializationError
21
+ from ..observability import get_logger as get_contextual_logger
22
+ from ..observability import record_operation
23
+
24
+ logger = logging.getLogger(__name__)
25
+ contextual_logger = get_contextual_logger(__name__)
26
+
27
+
28
+ class ConnectionManager:
29
+ """
30
+ Manages MongoDB connection lifecycle and configuration.
31
+
32
+ Handles connection initialization, validation, and shutdown.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ mongo_uri: str,
38
+ db_name: str,
39
+ max_pool_size: int = DEFAULT_MAX_POOL_SIZE,
40
+ min_pool_size: int = DEFAULT_MIN_POOL_SIZE,
41
+ ) -> None:
42
+ """
43
+ Initialize the connection manager.
44
+
45
+ Args:
46
+ mongo_uri: MongoDB connection URI
47
+ db_name: Database name
48
+ max_pool_size: Maximum MongoDB connection pool size
49
+ min_pool_size: Minimum MongoDB connection pool size
50
+ """
51
+ self.mongo_uri = mongo_uri
52
+ self.db_name = db_name
53
+ self.max_pool_size = max_pool_size
54
+ self.min_pool_size = min_pool_size
55
+
56
+ # Connection state
57
+ self._mongo_client: Optional[AsyncIOMotorClient] = None
58
+ self._mongo_db: Optional[AsyncIOMotorDatabase] = None
59
+ self._initialized: bool = False
60
+
61
+ async def initialize(self) -> None:
62
+ """
63
+ Initialize the MongoDB connection.
64
+
65
+ This method:
66
+ 1. Connects to MongoDB
67
+ 2. Validates the connection
68
+ 3. Sets up initial state
69
+
70
+ Raises:
71
+ InitializationError: If initialization fails
72
+ """
73
+ start_time = time.time()
74
+
75
+ if self._initialized:
76
+ logger.warning(
77
+ "ConnectionManager already initialized. Skipping re-initialization."
78
+ )
79
+ return
80
+
81
+ contextual_logger.info(
82
+ "Initializing MongoDB connection",
83
+ extra={
84
+ "mongo_uri": self.mongo_uri,
85
+ "db_name": self.db_name,
86
+ "max_pool_size": self.max_pool_size,
87
+ "min_pool_size": self.min_pool_size,
88
+ },
89
+ )
90
+
91
+ try:
92
+ # Connect to MongoDB
93
+ self._mongo_client = AsyncIOMotorClient(
94
+ self.mongo_uri,
95
+ serverSelectionTimeoutMS=DEFAULT_SERVER_SELECTION_TIMEOUT_MS,
96
+ appname="MDB_ENGINE",
97
+ maxPoolSize=self.max_pool_size,
98
+ minPoolSize=self.min_pool_size,
99
+ maxIdleTimeMS=DEFAULT_MAX_IDLE_TIME_MS,
100
+ retryWrites=True,
101
+ retryReads=True,
102
+ )
103
+
104
+ # Verify connection
105
+ await self._mongo_client.admin.command("ping")
106
+ self._mongo_db = self._mongo_client[self.db_name]
107
+
108
+ # Register client for pool metrics monitoring
109
+ try:
110
+ from ..database.connection import register_client_for_metrics
111
+
112
+ register_client_for_metrics(self._mongo_client)
113
+ except ImportError:
114
+ pass # Optional feature
115
+
116
+ self._initialized = True
117
+ duration_ms = (time.time() - start_time) * 1000
118
+ record_operation("connection.initialize", duration_ms, success=True)
119
+ contextual_logger.info(
120
+ "MongoDB connection initialized successfully",
121
+ extra={
122
+ "db_name": self.db_name,
123
+ "pool_size": f"{self.min_pool_size}-{self.max_pool_size}",
124
+ "duration_ms": round(duration_ms, 2),
125
+ },
126
+ )
127
+ except (ConnectionFailure, ServerSelectionTimeoutError) as e:
128
+ duration_ms = (time.time() - start_time) * 1000
129
+ record_operation("connection.initialize", duration_ms, success=False)
130
+ contextual_logger.critical(
131
+ "MongoDB connection failed",
132
+ extra={
133
+ "error_type": type(e).__name__,
134
+ "error": str(e),
135
+ "duration_ms": round(duration_ms, 2),
136
+ },
137
+ exc_info=True,
138
+ )
139
+ raise InitializationError(
140
+ f"Failed to connect to MongoDB: {e}",
141
+ mongo_uri=self.mongo_uri,
142
+ db_name=self.db_name,
143
+ context={
144
+ "error_type": type(e).__name__,
145
+ "max_pool_size": self.max_pool_size,
146
+ "min_pool_size": self.min_pool_size,
147
+ },
148
+ ) from e
149
+ except (TypeError, ValueError, AttributeError, KeyError) as e:
150
+ # Programming errors - these should not happen
151
+ duration_ms = (time.time() - start_time) * 1000
152
+ record_operation("connection.initialize", duration_ms, success=False)
153
+ contextual_logger.critical(
154
+ "ConnectionManager initialization failed",
155
+ extra={
156
+ "error_type": type(e).__name__,
157
+ "error": str(e),
158
+ "duration_ms": round(duration_ms, 2),
159
+ },
160
+ exc_info=True,
161
+ )
162
+ raise InitializationError(
163
+ f"ConnectionManager initialization failed: {e}",
164
+ mongo_uri=self.mongo_uri,
165
+ db_name=self.db_name,
166
+ context={
167
+ "error_type": type(e).__name__,
168
+ },
169
+ ) from e
170
+
171
+ async def shutdown(self) -> None:
172
+ """
173
+ Shutdown the MongoDB connection and clean up resources.
174
+
175
+ This method is idempotent - it's safe to call multiple times.
176
+ """
177
+ start_time = time.time()
178
+
179
+ if not self._initialized:
180
+ return
181
+
182
+ contextual_logger.info("Shutting down MongoDB connection...")
183
+
184
+ # Close MongoDB connection
185
+ if self._mongo_client:
186
+ self._mongo_client.close()
187
+ contextual_logger.info("MongoDB connection closed.")
188
+
189
+ self._initialized = False
190
+ self._mongo_client = None
191
+ self._mongo_db = None
192
+
193
+ duration_ms = (time.time() - start_time) * 1000
194
+ record_operation("connection.shutdown", duration_ms, success=True)
195
+ contextual_logger.info(
196
+ "MongoDB connection shutdown complete",
197
+ extra={"duration_ms": round(duration_ms, 2)},
198
+ )
199
+
200
+ @property
201
+ def mongo_client(self) -> AsyncIOMotorClient:
202
+ """
203
+ Get the MongoDB client.
204
+
205
+ Returns:
206
+ AsyncIOMotorClient instance
207
+
208
+ Raises:
209
+ RuntimeError: If connection is not initialized
210
+ """
211
+ if not self._initialized:
212
+ raise RuntimeError(
213
+ "ConnectionManager not initialized. Call initialize() first.",
214
+ )
215
+ assert (
216
+ self._mongo_client is not None
217
+ ), "MongoDB client should not be None after initialization"
218
+ return self._mongo_client
219
+
220
+ @property
221
+ def mongo_db(self) -> AsyncIOMotorDatabase:
222
+ """
223
+ Get the MongoDB database.
224
+
225
+ Returns:
226
+ AsyncIOMotorDatabase instance
227
+
228
+ Raises:
229
+ RuntimeError: If connection is not initialized
230
+ """
231
+ if not self._initialized:
232
+ raise RuntimeError(
233
+ "ConnectionManager not initialized. Call initialize() first.",
234
+ )
235
+ assert (
236
+ self._mongo_db is not None
237
+ ), "MongoDB database should not be None after initialization"
238
+ return self._mongo_db
239
+
240
+ @property
241
+ def initialized(self) -> bool:
242
+ """Check if connection is initialized."""
243
+ return self._initialized