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,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()
|