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.
@@ -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(self, slug: str, memory_config: dict[str, Any]) -> None:
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 (ImportError, AttributeError, TypeError, ValueError) as e:
149
- contextual_logger.error(
150
- f"Error initializing memory service for app '{slug}': {e}",
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
- return service
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
@@ -253,6 +253,7 @@ class WebSocketEndpointDict(TypedDict, total=False):
253
253
  auth: WebSocketAuthDict
254
254
  description: str
255
255
  ping_interval: int
256
+ ticket_ttl_seconds: int
256
257
 
257
258
 
258
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
- if hasattr(_shared_client, "_topology") and _shared_client._topology is not None:
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
- if hasattr(_shared_client, "_topology") and _shared_client._topology is not None:
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
- if hasattr(registered_client, "_topology") and registered_client._topology is not None:
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)
@@ -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
- if not engine._initialized:
188
- return HealthCheckResult(
189
- name="engine",
190
- status=HealthStatus.UNHEALTHY,
191
- message="MongoDBEngine not initialized",
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._apps)
195
+ app_count = len(engine.apps)
196
196
 
197
197
  return HealthCheckResult(
198
198
  name="engine",
@@ -133,8 +133,10 @@ register_message_handler("my_app", "realtime", handle_client_message)
133
133
 
134
134
  ### Authentication
135
135
 
136
- - Integrates with mdb_engine's JWT authentication system
137
- - Supports token via query parameter or cookie
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