mdb-engine 0.5.1__py3-none-any.whl → 0.7.0__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 +13 -9
- mdb_engine/auth/__init__.py +18 -0
- mdb_engine/auth/csrf.py +651 -69
- mdb_engine/auth/provider.py +10 -0
- mdb_engine/auth/shared_users.py +73 -2
- mdb_engine/auth/users.py +2 -1
- mdb_engine/auth/utils.py +31 -6
- mdb_engine/auth/websocket_sessions.py +433 -0
- mdb_engine/auth/websocket_tickets.py +307 -0
- mdb_engine/core/app_registration.py +10 -0
- mdb_engine/core/engine.py +656 -21
- mdb_engine/core/manifest.py +26 -0
- mdb_engine/core/ray_integration.py +4 -4
- mdb_engine/core/types.py +2 -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 +479 -56
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/METADATA +128 -4
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/RECORD +26 -24
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.5.1.dist-info → mdb_engine-0.7.0.dist-info}/top_level.txt +0 -0
mdb_engine/core/manifest.py
CHANGED
|
@@ -1338,6 +1338,18 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1338
1338
|
"auth is required (default: false)"
|
|
1339
1339
|
),
|
|
1340
1340
|
},
|
|
1341
|
+
"csrf_required": {
|
|
1342
|
+
"type": "boolean",
|
|
1343
|
+
"default": True,
|
|
1344
|
+
"description": (
|
|
1345
|
+
"Require CSRF validation for WebSocket connections "
|
|
1346
|
+
"(default: true - security by default). "
|
|
1347
|
+
"When true, uses encrypted session keys stored in "
|
|
1348
|
+
"private collection for CSRF protection. "
|
|
1349
|
+
"Set to false to use Origin validation + "
|
|
1350
|
+
"SameSite cookies only."
|
|
1351
|
+
),
|
|
1352
|
+
},
|
|
1341
1353
|
},
|
|
1342
1354
|
"additionalProperties": False,
|
|
1343
1355
|
"description": (
|
|
@@ -1361,6 +1373,20 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1361
1373
|
"alive (default: 30, min: 5, max: 300)"
|
|
1362
1374
|
),
|
|
1363
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
|
+
},
|
|
1364
1390
|
},
|
|
1365
1391
|
"required": ["path"],
|
|
1366
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
|
mdb_engine/core/types.py
CHANGED
|
@@ -243,6 +243,7 @@ class WebSocketAuthDict(TypedDict, total=False):
|
|
|
243
243
|
|
|
244
244
|
required: bool
|
|
245
245
|
allow_anonymous: bool
|
|
246
|
+
csrf_required: bool # Whether CSRF cookie is required (default: False)
|
|
246
247
|
|
|
247
248
|
|
|
248
249
|
class WebSocketEndpointDict(TypedDict, total=False):
|
|
@@ -252,6 +253,7 @@ class WebSocketEndpointDict(TypedDict, total=False):
|
|
|
252
253
|
auth: WebSocketAuthDict
|
|
253
254
|
description: str
|
|
254
255
|
ping_interval: int
|
|
256
|
+
ticket_ttl_seconds: int
|
|
255
257
|
|
|
256
258
|
|
|
257
259
|
class WebSocketsDict(TypedDict):
|
|
@@ -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
|
|