mdb-engine 0.6.0__py3-none-any.whl → 0.7.1__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/__init__.py +7 -13
- mdb_engine/auth/__init__.py +9 -0
- mdb_engine/auth/csrf.py +493 -144
- mdb_engine/auth/provider.py +10 -0
- mdb_engine/auth/shared_users.py +41 -0
- mdb_engine/auth/users.py +2 -1
- mdb_engine/auth/websocket_tickets.py +307 -0
- mdb_engine/cli/main.py +1 -1
- mdb_engine/core/app_registration.py +10 -0
- mdb_engine/core/engine.py +687 -38
- mdb_engine/core/manifest.py +14 -0
- mdb_engine/core/ray_integration.py +4 -4
- mdb_engine/core/service_initialization.py +63 -7
- mdb_engine/core/types.py +1 -0
- mdb_engine/database/connection.py +6 -3
- mdb_engine/database/scoped_wrapper.py +3 -3
- mdb_engine/indexes/manager.py +3 -3
- mdb_engine/observability/health.py +7 -7
- mdb_engine/routing/README.md +9 -2
- mdb_engine/routing/websockets.py +453 -74
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/METADATA +128 -4
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/RECORD +26 -25
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/WHEEL +0 -0
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.6.0.dist-info → mdb_engine-0.7.1.dist-info}/top_level.txt +0 -0
mdb_engine/core/manifest.py
CHANGED
|
@@ -1373,6 +1373,20 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1373
1373
|
"alive (default: 30, min: 5, max: 300)"
|
|
1374
1374
|
),
|
|
1375
1375
|
},
|
|
1376
|
+
"ticket_ttl_seconds": {
|
|
1377
|
+
"type": "integer",
|
|
1378
|
+
"minimum": 1,
|
|
1379
|
+
"maximum": 60,
|
|
1380
|
+
"default": 10,
|
|
1381
|
+
"description": (
|
|
1382
|
+
"WebSocket ticket time-to-live in seconds (default: 10). "
|
|
1383
|
+
"Tickets are single-use authentication tokens for "
|
|
1384
|
+
"WebSocket connections. "
|
|
1385
|
+
"Shorter TTL (3-5s) provides better security but requires "
|
|
1386
|
+
"faster connection. "
|
|
1387
|
+
"Longer TTL (10-30s) is more forgiving for slow networks."
|
|
1388
|
+
),
|
|
1389
|
+
},
|
|
1376
1390
|
},
|
|
1377
1391
|
"required": ["path"],
|
|
1378
1392
|
"additionalProperties": False,
|
|
@@ -360,10 +360,10 @@ def ray_actor_decorator(
|
|
|
360
360
|
# Convert to Ray remote class
|
|
361
361
|
ray_cls = ray.remote(cls)
|
|
362
362
|
|
|
363
|
-
# Store metadata on the class
|
|
364
|
-
ray_cls._app_slug = actor_app_slug
|
|
365
|
-
ray_cls._namespace = actor_namespace
|
|
366
|
-
ray_cls._isolated = isolated
|
|
363
|
+
# Store metadata on the class (intentional dynamic attribute setting for Ray)
|
|
364
|
+
ray_cls._app_slug = actor_app_slug # noqa: SLF001
|
|
365
|
+
ray_cls._namespace = actor_namespace # noqa: SLF001
|
|
366
|
+
ray_cls._isolated = isolated # noqa: SLF001
|
|
367
367
|
|
|
368
368
|
# Add spawn class method
|
|
369
369
|
@classmethod
|
|
@@ -26,6 +26,11 @@ from ..observability import get_logger as get_contextual_logger
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
contextual_logger = get_contextual_logger(__name__)
|
|
28
28
|
|
|
29
|
+
try:
|
|
30
|
+
from openai import OpenAIError
|
|
31
|
+
except ImportError:
|
|
32
|
+
OpenAIError = RuntimeError
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
class ServiceInitializer:
|
|
31
36
|
"""
|
|
@@ -54,7 +59,9 @@ class ServiceInitializer:
|
|
|
54
59
|
self._memory_services: dict[str, Any] = {}
|
|
55
60
|
self._websocket_configs: dict[str, dict[str, Any]] = {}
|
|
56
61
|
|
|
57
|
-
async def initialize_memory_service(
|
|
62
|
+
async def initialize_memory_service(
|
|
63
|
+
self, slug: str, memory_config: dict[str, Any] | None
|
|
64
|
+
) -> None:
|
|
58
65
|
"""
|
|
59
66
|
Initialize Mem0 memory service for an app.
|
|
60
67
|
|
|
@@ -63,8 +70,17 @@ class ServiceInitializer:
|
|
|
63
70
|
|
|
64
71
|
Args:
|
|
65
72
|
slug: App slug
|
|
66
|
-
memory_config: Memory configuration from manifest (already validated)
|
|
73
|
+
memory_config: Memory configuration from manifest (already validated).
|
|
74
|
+
Can be None or empty dict to skip initialization.
|
|
67
75
|
"""
|
|
76
|
+
# Handle None or empty config
|
|
77
|
+
if not memory_config:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Check if memory is enabled (must be checked before import)
|
|
81
|
+
if not memory_config.get("enabled", False):
|
|
82
|
+
return
|
|
83
|
+
|
|
68
84
|
# Try to import Memory service (optional dependency)
|
|
69
85
|
try:
|
|
70
86
|
from ..memory import Mem0MemoryService, Mem0MemoryServiceError
|
|
@@ -145,12 +161,41 @@ class ServiceInitializer:
|
|
|
145
161
|
extra={"app_slug": slug, "error": str(e)},
|
|
146
162
|
exc_info=True,
|
|
147
163
|
)
|
|
148
|
-
except
|
|
149
|
-
contextual_logger.
|
|
150
|
-
f"
|
|
164
|
+
except OpenAIError as e:
|
|
165
|
+
contextual_logger.warning(
|
|
166
|
+
f"Memory service initialization skipped for app '{slug}': "
|
|
167
|
+
f"OpenAI API error. {e}",
|
|
151
168
|
extra={"app_slug": slug, "error": str(e)},
|
|
152
|
-
exc_info=True,
|
|
153
169
|
)
|
|
170
|
+
except (
|
|
171
|
+
ImportError,
|
|
172
|
+
AttributeError,
|
|
173
|
+
TypeError,
|
|
174
|
+
ValueError,
|
|
175
|
+
RuntimeError,
|
|
176
|
+
ConnectionError,
|
|
177
|
+
OSError,
|
|
178
|
+
) as e:
|
|
179
|
+
error_msg = str(e).lower()
|
|
180
|
+
error_type = type(e).__name__
|
|
181
|
+
is_api_key_error = (
|
|
182
|
+
"api_key" in error_msg
|
|
183
|
+
or "api key" in error_msg
|
|
184
|
+
or "openai" in error_type.lower()
|
|
185
|
+
or "openai" in error_msg
|
|
186
|
+
)
|
|
187
|
+
if is_api_key_error:
|
|
188
|
+
contextual_logger.warning(
|
|
189
|
+
f"Memory service initialization skipped for app '{slug}': "
|
|
190
|
+
f"Missing API key or configuration. {e}",
|
|
191
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
contextual_logger.error(
|
|
195
|
+
f"Error initializing memory service for app '{slug}': {e}",
|
|
196
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
197
|
+
exc_info=True,
|
|
198
|
+
)
|
|
154
199
|
|
|
155
200
|
async def register_websockets(self, slug: str, websockets_config: dict[str, Any]) -> None:
|
|
156
201
|
"""
|
|
@@ -330,7 +375,18 @@ class ServiceInitializer:
|
|
|
330
375
|
extra={"app_slug": slug},
|
|
331
376
|
)
|
|
332
377
|
return None
|
|
333
|
-
|
|
378
|
+
return service
|
|
379
|
+
|
|
380
|
+
# Service not found - check if it should be initialized but wasn't
|
|
381
|
+
# This can happen in multi-app context if initialization was missed
|
|
382
|
+
# Note: We can't do async initialization here, so we just log a warning
|
|
383
|
+
# The explicit initialization in create_multi_app should handle this
|
|
384
|
+
contextual_logger.debug(
|
|
385
|
+
f"Memory service not found for '{slug}' - "
|
|
386
|
+
f"it may not be initialized yet or memory is disabled",
|
|
387
|
+
extra={"app_slug": slug},
|
|
388
|
+
)
|
|
389
|
+
return None
|
|
334
390
|
except (KeyError, AttributeError, TypeError) as e:
|
|
335
391
|
contextual_logger.error(
|
|
336
392
|
f"Error retrieving memory service for '{slug}': {e}",
|
mdb_engine/core/types.py
CHANGED
|
@@ -91,7 +91,8 @@ def get_shared_mongo_client(
|
|
|
91
91
|
# Verify client is still connected
|
|
92
92
|
try:
|
|
93
93
|
# Non-blocking check - if client was closed, it will be None or invalid
|
|
94
|
-
|
|
94
|
+
# Accessing MongoDB client's internal _topology attribute to check connection status
|
|
95
|
+
if hasattr(_shared_client, "_topology") and _shared_client._topology is not None: # noqa: SLF001
|
|
95
96
|
return _shared_client
|
|
96
97
|
except (AttributeError, RuntimeError):
|
|
97
98
|
# Client was closed or invalid, reset and recreate
|
|
@@ -104,7 +105,8 @@ def get_shared_mongo_client(
|
|
|
104
105
|
# Double-check pattern: another thread may have initialized while we waited
|
|
105
106
|
if _shared_client is not None:
|
|
106
107
|
try:
|
|
107
|
-
|
|
108
|
+
# Accessing MongoDB client's internal _topology attribute to check connection status
|
|
109
|
+
if hasattr(_shared_client, "_topology") and _shared_client._topology is not None: # noqa: SLF001
|
|
108
110
|
return _shared_client
|
|
109
111
|
except (AttributeError, RuntimeError):
|
|
110
112
|
# Client was closed or invalid, reset and recreate
|
|
@@ -234,7 +236,8 @@ async def get_pool_metrics(
|
|
|
234
236
|
for registered_client in _registered_clients:
|
|
235
237
|
try:
|
|
236
238
|
# Verify client is still valid
|
|
237
|
-
|
|
239
|
+
# Accessing MongoDB client's internal _topology attribute to check connection status
|
|
240
|
+
if hasattr(registered_client, "_topology") and registered_client._topology is not None: # noqa: SLF001
|
|
238
241
|
return await _get_client_pool_metrics(registered_client)
|
|
239
242
|
except (AttributeError, RuntimeError):
|
|
240
243
|
# Type 2: Recoverable - if this client is invalid, try next one
|
|
@@ -291,7 +291,7 @@ class AsyncAtlasIndexManager:
|
|
|
291
291
|
"""
|
|
292
292
|
# Unwrap _SecureCollectionProxy if present to get the real collection
|
|
293
293
|
if isinstance(real_collection, _SecureCollectionProxy):
|
|
294
|
-
real_collection = real_collection._collection
|
|
294
|
+
real_collection = real_collection._collection # noqa: SLF001
|
|
295
295
|
if not isinstance(real_collection, AsyncIOMotorCollection):
|
|
296
296
|
raise TypeError(f"Expected AsyncIOMotorCollection, got {type(real_collection)}")
|
|
297
297
|
self._collection = real_collection
|
|
@@ -1297,7 +1297,7 @@ class ScopedCollectionWrapper:
|
|
|
1297
1297
|
try:
|
|
1298
1298
|
# Verify token if needed (lazy verification for async contexts)
|
|
1299
1299
|
if self._parent_wrapper:
|
|
1300
|
-
await self._parent_wrapper._verify_token_if_needed()
|
|
1300
|
+
await self._parent_wrapper._verify_token_if_needed() # noqa: SLF001
|
|
1301
1301
|
|
|
1302
1302
|
# Validate document size before insert
|
|
1303
1303
|
self._resource_limiter.validate_document_size(document)
|
|
@@ -1390,7 +1390,7 @@ class ScopedCollectionWrapper:
|
|
|
1390
1390
|
try:
|
|
1391
1391
|
# Verify token if needed (lazy verification for async contexts)
|
|
1392
1392
|
if self._parent_wrapper:
|
|
1393
|
-
await self._parent_wrapper._verify_token_if_needed()
|
|
1393
|
+
await self._parent_wrapper._verify_token_if_needed() # noqa: SLF001
|
|
1394
1394
|
|
|
1395
1395
|
# Validate query filter for security
|
|
1396
1396
|
self._query_validator.validate_filter(filter)
|
mdb_engine/indexes/manager.py
CHANGED
|
@@ -441,7 +441,7 @@ async def _handle_search_index(
|
|
|
441
441
|
logger.info(f"{log_prefix} Search index '{index_name}' definition matches.")
|
|
442
442
|
if not existing_index.get("queryable") and existing_index.get("status") != "FAILED":
|
|
443
443
|
logger.info(f"{log_prefix} Index '{index_name}' not queryable yet; waiting.")
|
|
444
|
-
await index_manager._wait_for_search_index_ready(
|
|
444
|
+
await index_manager._wait_for_search_index_ready( # noqa: SLF001
|
|
445
445
|
index_name, index_manager.DEFAULT_SEARCH_TIMEOUT
|
|
446
446
|
)
|
|
447
447
|
logger.info(f"{log_prefix} Index '{index_name}' now ready.")
|
|
@@ -580,7 +580,7 @@ async def _handle_hybrid_index(
|
|
|
580
580
|
f"{log_prefix} Vector index '{vector_index_name}' "
|
|
581
581
|
f"not queryable yet; waiting."
|
|
582
582
|
)
|
|
583
|
-
await index_manager._wait_for_search_index_ready(
|
|
583
|
+
await index_manager._wait_for_search_index_ready( # noqa: SLF001
|
|
584
584
|
vector_index_name, index_manager.DEFAULT_SEARCH_TIMEOUT
|
|
585
585
|
)
|
|
586
586
|
logger.info(f"{log_prefix} Vector index '{vector_index_name}' now ready.")
|
|
@@ -634,7 +634,7 @@ async def _handle_hybrid_index(
|
|
|
634
634
|
logger.info(
|
|
635
635
|
f"{log_prefix} Text index '{text_index_name}' " f"not queryable yet; waiting."
|
|
636
636
|
)
|
|
637
|
-
await index_manager._wait_for_search_index_ready(
|
|
637
|
+
await index_manager._wait_for_search_index_ready( # noqa: SLF001
|
|
638
638
|
text_index_name, index_manager.DEFAULT_SEARCH_TIMEOUT
|
|
639
639
|
)
|
|
640
640
|
logger.info(f"{log_prefix} Text index '{text_index_name}' now ready.")
|
|
@@ -184,15 +184,15 @@ async def check_engine_health(engine: Any | None) -> HealthCheckResult:
|
|
|
184
184
|
message="MongoDBEngine not initialized",
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
if not engine.initialized:
|
|
188
|
+
return HealthCheckResult(
|
|
189
|
+
name="engine",
|
|
190
|
+
status=HealthStatus.UNHEALTHY,
|
|
191
|
+
message="MongoDBEngine not initialized",
|
|
192
|
+
)
|
|
193
193
|
|
|
194
194
|
# Check registered apps
|
|
195
|
-
app_count = len(engine.
|
|
195
|
+
app_count = len(engine.apps)
|
|
196
196
|
|
|
197
197
|
return HealthCheckResult(
|
|
198
198
|
name="engine",
|
mdb_engine/routing/README.md
CHANGED
|
@@ -133,8 +133,10 @@ register_message_handler("my_app", "realtime", handle_client_message)
|
|
|
133
133
|
|
|
134
134
|
### Authentication
|
|
135
135
|
|
|
136
|
-
-
|
|
137
|
-
-
|
|
136
|
+
- Uses ticket-based authentication (secure-by-default)
|
|
137
|
+
- Tickets are short-lived (10 seconds), single-use, and in-memory
|
|
138
|
+
- Exchange JWT for ticket via `/auth/ticket` endpoint
|
|
139
|
+
- Include ticket as query parameter (`?ticket=...`) or header (`X-WebSocket-Ticket`)
|
|
138
140
|
- Respects app's `auth_policy` configuration
|
|
139
141
|
- Can be overridden per endpoint
|
|
140
142
|
|
|
@@ -434,6 +436,11 @@ async def on_document_created(document):
|
|
|
434
436
|
- **Simplicity**: Just declare in manifest.json, register handlers in code
|
|
435
437
|
- **Flexibility**: Full two-way communication (broadcast + listen)
|
|
436
438
|
- **Automatic**: Routes registered automatically during app registration
|
|
439
|
+
- **FastAPI Integration**: Uses FastAPI's `APIRouter` for WebSocket registration, ensuring:
|
|
440
|
+
- ✅ Full FastAPI feature support (dependency injection, OpenAPI docs, request/response models)
|
|
441
|
+
- ✅ Consistent behavior across single-app and multi-app modes
|
|
442
|
+
- ✅ Best practices compliance with FastAPI's recommended patterns
|
|
443
|
+
- ✅ Better maintainability through FastAPI abstractions
|
|
437
444
|
|
|
438
445
|
## Message Flow
|
|
439
446
|
|