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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- 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
|