mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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 +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional Ray Integration for MDB Engine.
|
|
3
|
+
|
|
4
|
+
Provides optional Ray support for distributed processing with app isolation.
|
|
5
|
+
This module gracefully degrades if Ray is not installed.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# Check if Ray is available
|
|
9
|
+
from mdb_engine.core.ray_integration import RAY_AVAILABLE
|
|
10
|
+
|
|
11
|
+
if RAY_AVAILABLE:
|
|
12
|
+
from mdb_engine.core.ray_integration import (
|
|
13
|
+
AppRayActor,
|
|
14
|
+
get_ray_actor_handle,
|
|
15
|
+
ray_actor_decorator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Or use the helper that handles availability
|
|
19
|
+
actor = await get_ray_actor_handle("my_app")
|
|
20
|
+
if actor:
|
|
21
|
+
result = await actor.process.remote(data)
|
|
22
|
+
|
|
23
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# --- Ray Availability Check ---
|
|
37
|
+
# This flag allows code to check Ray availability without try/except
|
|
38
|
+
RAY_AVAILABLE = False
|
|
39
|
+
ray = None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import ray as _ray
|
|
43
|
+
|
|
44
|
+
ray = _ray
|
|
45
|
+
RAY_AVAILABLE = True
|
|
46
|
+
logger.debug("Ray is available")
|
|
47
|
+
except ImportError:
|
|
48
|
+
logger.debug("Ray not installed - Ray features will be disabled")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Type variable for decorator
|
|
52
|
+
T = TypeVar("T")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AppRayActor:
|
|
56
|
+
"""
|
|
57
|
+
Base Ray actor class for app-specific isolated environments.
|
|
58
|
+
|
|
59
|
+
Each app can have its own Ray actor with:
|
|
60
|
+
- Isolated MongoDB connection
|
|
61
|
+
- App-specific configuration
|
|
62
|
+
- Isolated state
|
|
63
|
+
|
|
64
|
+
Subclass this to create app-specific actors:
|
|
65
|
+
|
|
66
|
+
@ray.remote
|
|
67
|
+
class MyAppActor(AppRayActor):
|
|
68
|
+
async def process(self, data):
|
|
69
|
+
db = await self.get_app_db()
|
|
70
|
+
# Process with isolated DB access
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
app_slug: Application identifier
|
|
75
|
+
mongo_uri: MongoDB connection URI
|
|
76
|
+
db_name: Database name
|
|
77
|
+
use_in_memory_fallback: Whether to use in-memory DB if MongoDB unavailable
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
app_slug: str,
|
|
83
|
+
mongo_uri: str,
|
|
84
|
+
db_name: str,
|
|
85
|
+
use_in_memory_fallback: bool = False,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Initialize app-specific Ray actor.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
app_slug: Application identifier
|
|
92
|
+
mongo_uri: MongoDB connection URI
|
|
93
|
+
db_name: Database name
|
|
94
|
+
use_in_memory_fallback: Use in-memory DB if MongoDB unavailable
|
|
95
|
+
"""
|
|
96
|
+
self.app_slug = app_slug
|
|
97
|
+
self.mongo_uri = mongo_uri
|
|
98
|
+
self.db_name = db_name
|
|
99
|
+
self.use_in_memory_fallback = use_in_memory_fallback
|
|
100
|
+
|
|
101
|
+
# Lazy initialization for engine
|
|
102
|
+
self._engine = None
|
|
103
|
+
self._initialized = False
|
|
104
|
+
|
|
105
|
+
logger.info(f"AppRayActor created for '{app_slug}' " f"(fallback={use_in_memory_fallback})")
|
|
106
|
+
|
|
107
|
+
async def _ensure_initialized(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Ensure the actor is initialized with database connection.
|
|
110
|
+
|
|
111
|
+
Called lazily on first database access.
|
|
112
|
+
"""
|
|
113
|
+
if self._initialized:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
from .engine import MongoDBEngine
|
|
118
|
+
|
|
119
|
+
self._engine = MongoDBEngine(
|
|
120
|
+
mongo_uri=self.mongo_uri,
|
|
121
|
+
db_name=self.db_name,
|
|
122
|
+
)
|
|
123
|
+
await self._engine.initialize()
|
|
124
|
+
self._initialized = True
|
|
125
|
+
logger.info(f"AppRayActor engine initialized for '{self.app_slug}'")
|
|
126
|
+
|
|
127
|
+
except (ConnectionError, ValueError, RuntimeError, OSError) as e:
|
|
128
|
+
logger.exception(f"Error initializing engine in Ray actor for '{self.app_slug}': {e}")
|
|
129
|
+
if self.use_in_memory_fallback:
|
|
130
|
+
logger.warning(f"Using in-memory fallback for '{self.app_slug}'")
|
|
131
|
+
self._engine = None
|
|
132
|
+
self._initialized = True
|
|
133
|
+
else:
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
async def get_app_db(self, app_token: str | None = None) -> Any:
|
|
137
|
+
"""
|
|
138
|
+
Get scoped database for this app.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
app_token: Optional app token for authentication
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ScopedMongoWrapper for this app
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
RuntimeError: If engine not available and no fallback
|
|
148
|
+
"""
|
|
149
|
+
await self._ensure_initialized()
|
|
150
|
+
|
|
151
|
+
if not self._engine:
|
|
152
|
+
if self.use_in_memory_fallback:
|
|
153
|
+
raise RuntimeError(f"In-memory fallback not yet implemented for '{self.app_slug}'")
|
|
154
|
+
raise RuntimeError(f"Engine not available for '{self.app_slug}'")
|
|
155
|
+
|
|
156
|
+
# Try to get app token from environment if not provided
|
|
157
|
+
if not app_token:
|
|
158
|
+
env_var_name = f"{self.app_slug.upper()}_SECRET"
|
|
159
|
+
app_token = os.getenv(env_var_name)
|
|
160
|
+
|
|
161
|
+
return self._engine.get_scoped_db(self.app_slug, app_token=app_token)
|
|
162
|
+
|
|
163
|
+
def get_app_slug(self) -> str:
|
|
164
|
+
"""Get the app slug."""
|
|
165
|
+
return self.app_slug
|
|
166
|
+
|
|
167
|
+
async def health_check(self) -> dict:
|
|
168
|
+
"""
|
|
169
|
+
Health check for the actor.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict with health status information
|
|
173
|
+
"""
|
|
174
|
+
await self._ensure_initialized()
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"app_slug": self.app_slug,
|
|
178
|
+
"initialized": self._initialized,
|
|
179
|
+
"engine_available": self._engine is not None,
|
|
180
|
+
"status": "healthy" if self._initialized else "initializing",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async def shutdown(self) -> None:
|
|
184
|
+
"""Shutdown the actor and release resources."""
|
|
185
|
+
if self._engine:
|
|
186
|
+
await self._engine.shutdown()
|
|
187
|
+
self._engine = None
|
|
188
|
+
self._initialized = False
|
|
189
|
+
logger.info(f"AppRayActor shutdown for '{self.app_slug}'")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def get_ray_actor_handle(
|
|
193
|
+
app_slug: str,
|
|
194
|
+
namespace: str = "modular_labs",
|
|
195
|
+
mongo_uri: str | None = None,
|
|
196
|
+
db_name: str | None = None,
|
|
197
|
+
create_if_missing: bool = True,
|
|
198
|
+
actor_class: type | None = None,
|
|
199
|
+
) -> Any | None:
|
|
200
|
+
"""
|
|
201
|
+
Get or create a Ray actor handle for an app.
|
|
202
|
+
|
|
203
|
+
This function:
|
|
204
|
+
1. Checks if Ray is available
|
|
205
|
+
2. Initializes Ray if not already done
|
|
206
|
+
3. Tries to get existing actor by name
|
|
207
|
+
4. Creates new actor if missing and create_if_missing=True
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
app_slug: Application identifier
|
|
211
|
+
namespace: Ray namespace (default: "modular_labs")
|
|
212
|
+
mongo_uri: MongoDB URI (default: from MONGODB_URI env var)
|
|
213
|
+
db_name: Database name (default: from MONGODB_DB env var)
|
|
214
|
+
create_if_missing: Create actor if it doesn't exist (default: True)
|
|
215
|
+
actor_class: Custom actor class to use (default: AppRayActor)
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Ray actor handle, or None if Ray unavailable or actor not found
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
# Get or create actor
|
|
222
|
+
actor = await get_ray_actor_handle("my_app")
|
|
223
|
+
if actor:
|
|
224
|
+
result = await actor.process.remote(data)
|
|
225
|
+
|
|
226
|
+
# Get existing actor only
|
|
227
|
+
actor = await get_ray_actor_handle("my_app", create_if_missing=False)
|
|
228
|
+
"""
|
|
229
|
+
if not RAY_AVAILABLE:
|
|
230
|
+
logger.debug(f"Ray not available - cannot get actor for '{app_slug}'")
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
actor_name = f"{app_slug}-actor"
|
|
234
|
+
|
|
235
|
+
# Ensure Ray is initialized
|
|
236
|
+
if not ray.is_initialized():
|
|
237
|
+
ray_address = os.getenv("RAY_ADDRESS")
|
|
238
|
+
if ray_address:
|
|
239
|
+
try:
|
|
240
|
+
ray.init(address=ray_address, namespace=namespace)
|
|
241
|
+
logger.info(f"Ray initialized with address: {ray_address}")
|
|
242
|
+
except (RuntimeError, ConnectionError) as e:
|
|
243
|
+
logger.exception(f"Failed to initialize Ray: {e}")
|
|
244
|
+
return None
|
|
245
|
+
else:
|
|
246
|
+
try:
|
|
247
|
+
# Initialize local Ray instance
|
|
248
|
+
ray.init(namespace=namespace, ignore_reinit_error=True)
|
|
249
|
+
logger.info("Ray initialized locally")
|
|
250
|
+
except RuntimeError as e:
|
|
251
|
+
logger.exception(f"Failed to initialize local Ray: {e}")
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
# Try to get existing actor
|
|
255
|
+
try:
|
|
256
|
+
handle = ray.get_actor(actor_name, namespace=namespace)
|
|
257
|
+
logger.debug(f"Found existing Ray actor: {actor_name}")
|
|
258
|
+
return handle
|
|
259
|
+
except ValueError:
|
|
260
|
+
# Actor doesn't exist
|
|
261
|
+
if not create_if_missing:
|
|
262
|
+
logger.debug(f"Ray actor '{actor_name}' not found")
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# Create new actor
|
|
266
|
+
try:
|
|
267
|
+
# Use provided class or default to AppRayActor
|
|
268
|
+
cls = actor_class or AppRayActor
|
|
269
|
+
|
|
270
|
+
# Make it a Ray remote class if not already
|
|
271
|
+
if not hasattr(cls, "remote"):
|
|
272
|
+
cls = ray.remote(cls)
|
|
273
|
+
|
|
274
|
+
# Get connection parameters
|
|
275
|
+
uri = mongo_uri or os.getenv("MONGODB_URI", "mongodb://localhost:27017")
|
|
276
|
+
db = db_name or os.getenv("MONGODB_DB", "mdb_runtime")
|
|
277
|
+
|
|
278
|
+
handle = cls.options(
|
|
279
|
+
name=actor_name,
|
|
280
|
+
namespace=namespace,
|
|
281
|
+
lifetime="detached",
|
|
282
|
+
get_if_exists=True,
|
|
283
|
+
).remote(
|
|
284
|
+
app_slug=app_slug,
|
|
285
|
+
mongo_uri=uri,
|
|
286
|
+
db_name=db,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
logger.info(f"Created Ray actor '{actor_name}' in namespace '{namespace}'")
|
|
290
|
+
return handle
|
|
291
|
+
|
|
292
|
+
except ray.exceptions.RayError as e:
|
|
293
|
+
logger.error(f"Error creating Ray actor for '{app_slug}': {e}", exc_info=True)
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def ray_actor_decorator(
|
|
298
|
+
app_slug: str | None = None,
|
|
299
|
+
namespace: str = "modular_labs",
|
|
300
|
+
isolated: bool = True,
|
|
301
|
+
lifetime: str = "detached",
|
|
302
|
+
max_restarts: int = -1,
|
|
303
|
+
) -> Callable[[type], type]:
|
|
304
|
+
"""
|
|
305
|
+
Decorator for creating Ray actors with app isolation.
|
|
306
|
+
|
|
307
|
+
This decorator:
|
|
308
|
+
1. Converts a class to a Ray remote class
|
|
309
|
+
2. Adds app-specific namespace (if isolated=True)
|
|
310
|
+
3. Provides a convenient spawn() class method
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
app_slug: Application identifier. If None, derived from class name
|
|
314
|
+
namespace: Base Ray namespace (default: "modular_labs")
|
|
315
|
+
isolated: Use app-specific namespace for isolation (default: True)
|
|
316
|
+
lifetime: Actor lifetime ("detached" or "non_detached")
|
|
317
|
+
max_restarts: Max automatic restarts (-1 for unlimited)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Decorated class
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
@ray_actor_decorator(app_slug="my_app", isolated=True)
|
|
324
|
+
class MyAppActor(AppRayActor):
|
|
325
|
+
async def process(self, data):
|
|
326
|
+
db = await self.get_app_db()
|
|
327
|
+
return await db.items.find_one({"id": data["id"]})
|
|
328
|
+
|
|
329
|
+
# Spawn the actor
|
|
330
|
+
actor = MyAppActor.spawn()
|
|
331
|
+
result = await actor.process.remote({"id": "123"})
|
|
332
|
+
|
|
333
|
+
Note:
|
|
334
|
+
If Ray is not available, returns the class unchanged (no-op decorator).
|
|
335
|
+
"""
|
|
336
|
+
if not RAY_AVAILABLE:
|
|
337
|
+
|
|
338
|
+
def noop_decorator(cls: type) -> type:
|
|
339
|
+
logger.debug(f"Ray not available - {cls.__name__} not converted to Ray actor")
|
|
340
|
+
return cls
|
|
341
|
+
|
|
342
|
+
return noop_decorator
|
|
343
|
+
|
|
344
|
+
def decorator(cls: type) -> type:
|
|
345
|
+
# Determine app slug from class name if not provided
|
|
346
|
+
actor_app_slug = app_slug
|
|
347
|
+
if actor_app_slug is None:
|
|
348
|
+
# Convert class name to slug: MyAppActor -> my_app
|
|
349
|
+
name = cls.__name__
|
|
350
|
+
if name.endswith("Actor"):
|
|
351
|
+
name = name[:-5]
|
|
352
|
+
# Convert CamelCase to snake_case
|
|
353
|
+
import re
|
|
354
|
+
|
|
355
|
+
actor_app_slug = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
356
|
+
|
|
357
|
+
# Determine namespace
|
|
358
|
+
actor_namespace = f"{namespace}_{actor_app_slug}" if isolated else namespace
|
|
359
|
+
|
|
360
|
+
# Convert to Ray remote class
|
|
361
|
+
ray_cls = ray.remote(cls)
|
|
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
|
|
367
|
+
|
|
368
|
+
# Add spawn class method
|
|
369
|
+
@classmethod
|
|
370
|
+
def spawn(
|
|
371
|
+
cls,
|
|
372
|
+
*args,
|
|
373
|
+
mongo_uri: str | None = None,
|
|
374
|
+
db_name: str | None = None,
|
|
375
|
+
**kwargs,
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Spawn an instance of this actor.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
*args: Positional arguments for actor __init__
|
|
382
|
+
mongo_uri: MongoDB URI (default: from env)
|
|
383
|
+
db_name: Database name (default: from env)
|
|
384
|
+
**kwargs: Keyword arguments for actor __init__
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Ray actor handle
|
|
388
|
+
"""
|
|
389
|
+
actor_name = f"{actor_app_slug}-{cls.__name__.lower()}"
|
|
390
|
+
|
|
391
|
+
# Get connection parameters if not in kwargs
|
|
392
|
+
if "mongo_uri" not in kwargs:
|
|
393
|
+
kwargs["mongo_uri"] = mongo_uri or os.getenv(
|
|
394
|
+
"MONGODB_URI", "mongodb://localhost:27017"
|
|
395
|
+
)
|
|
396
|
+
if "db_name" not in kwargs:
|
|
397
|
+
kwargs["db_name"] = db_name or os.getenv("MONGODB_DB", "mdb_runtime")
|
|
398
|
+
if "app_slug" not in kwargs:
|
|
399
|
+
kwargs["app_slug"] = actor_app_slug
|
|
400
|
+
|
|
401
|
+
return cls.options(
|
|
402
|
+
name=actor_name,
|
|
403
|
+
namespace=actor_namespace,
|
|
404
|
+
lifetime=lifetime,
|
|
405
|
+
max_restarts=max_restarts,
|
|
406
|
+
get_if_exists=True,
|
|
407
|
+
).remote(*args, **kwargs)
|
|
408
|
+
|
|
409
|
+
ray_cls.spawn = spawn
|
|
410
|
+
|
|
411
|
+
logger.debug(
|
|
412
|
+
f"Created Ray actor class '{cls.__name__}' "
|
|
413
|
+
f"(app_slug={actor_app_slug}, namespace={actor_namespace})"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return ray_cls
|
|
417
|
+
|
|
418
|
+
return decorator
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# Convenience aliases
|
|
422
|
+
if RAY_AVAILABLE:
|
|
423
|
+
# Export ray module for convenience
|
|
424
|
+
get_actor = ray.get_actor
|
|
425
|
+
init_ray = ray.init
|
|
426
|
+
is_initialized = ray.is_initialized
|
|
427
|
+
else:
|
|
428
|
+
# Stub functions when Ray not available
|
|
429
|
+
def get_actor(*args, **kwargs):
|
|
430
|
+
raise RuntimeError("Ray is not available")
|
|
431
|
+
|
|
432
|
+
def init_ray(*args, **kwargs):
|
|
433
|
+
raise RuntimeError("Ray is not available")
|
|
434
|
+
|
|
435
|
+
def is_initialized():
|
|
436
|
+
return False
|
mdb_engine/core/seeding.py
CHANGED
|
@@ -8,17 +8,20 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
from datetime import datetime
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
|
-
from pymongo.errors import (
|
|
14
|
-
|
|
13
|
+
from pymongo.errors import (
|
|
14
|
+
ConnectionFailure,
|
|
15
|
+
OperationFailure,
|
|
16
|
+
ServerSelectionTimeoutError,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
async def seed_initial_data(
|
|
20
|
-
db, app_slug: str, initial_data:
|
|
21
|
-
) ->
|
|
23
|
+
db, app_slug: str, initial_data: dict[str, list[dict[str, Any]]]
|
|
24
|
+
) -> dict[str, int]:
|
|
22
25
|
"""
|
|
23
26
|
Seed initial data into collections.
|
|
24
27
|
|
|
@@ -47,9 +50,7 @@ async def seed_initial_data(
|
|
|
47
50
|
# Get existing seeding metadata
|
|
48
51
|
seeding_metadata = await metadata_collection.find_one({"app_slug": app_slug})
|
|
49
52
|
seeded_collections = (
|
|
50
|
-
set(seeding_metadata.get("seeded_collections", []))
|
|
51
|
-
if seeding_metadata
|
|
52
|
-
else set()
|
|
53
|
+
set(seeding_metadata.get("seeded_collections", [])) if seeding_metadata else set()
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
for collection_name, documents in initial_data.items():
|
|
@@ -90,9 +91,7 @@ async def seed_initial_data(
|
|
|
90
91
|
# Try to parse ISO format datetime strings
|
|
91
92
|
try:
|
|
92
93
|
# Check if it looks like a datetime string
|
|
93
|
-
if "T" in value and (
|
|
94
|
-
"Z" in value or "+" in value or "-" in value[-6:]
|
|
95
|
-
):
|
|
94
|
+
if "T" in value and ("Z" in value or "+" in value or "-" in value[-6:]):
|
|
96
95
|
# Try parsing as ISO format
|
|
97
96
|
from dateutil.parser import parse as parse_date
|
|
98
97
|
|
|
@@ -110,10 +109,7 @@ async def seed_initial_data(
|
|
|
110
109
|
pass
|
|
111
110
|
|
|
112
111
|
# Add created_at if not present and document doesn't have timestamp fields
|
|
113
|
-
if
|
|
114
|
-
"created_at" not in prepared_doc
|
|
115
|
-
and "date_created" not in prepared_doc
|
|
116
|
-
):
|
|
112
|
+
if "created_at" not in prepared_doc and "date_created" not in prepared_doc:
|
|
117
113
|
prepared_doc["created_at"] = datetime.utcnow()
|
|
118
114
|
|
|
119
115
|
prepared_docs.append(prepared_doc)
|
|
@@ -144,9 +140,7 @@ async def seed_initial_data(
|
|
|
144
140
|
Exception,
|
|
145
141
|
) as e:
|
|
146
142
|
# Type 2: Recoverable - log error and continue with other collections
|
|
147
|
-
logger.exception(
|
|
148
|
-
f"Failed to seed collection '{collection_name}' for {app_slug}: {e}"
|
|
149
|
-
)
|
|
143
|
+
logger.exception(f"Failed to seed collection '{collection_name}' for {app_slug}: {e}")
|
|
150
144
|
results[collection_name] = 0
|
|
151
145
|
|
|
152
146
|
# Update seeding metadata
|
|
@@ -172,8 +166,6 @@ async def seed_initial_data(
|
|
|
172
166
|
TypeError,
|
|
173
167
|
KeyError,
|
|
174
168
|
) as e:
|
|
175
|
-
logger.warning(
|
|
176
|
-
f"Failed to update seeding metadata for {app_slug}: {e}", exc_info=True
|
|
177
|
-
)
|
|
169
|
+
logger.warning(f"Failed to update seeding metadata for {app_slug}: {e}", exc_info=True)
|
|
178
170
|
|
|
179
171
|
return results
|
|
@@ -11,10 +11,14 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any
|
|
15
16
|
|
|
16
|
-
from pymongo.errors import (
|
|
17
|
-
|
|
17
|
+
from pymongo.errors import (
|
|
18
|
+
ConnectionFailure,
|
|
19
|
+
OperationFailure,
|
|
20
|
+
ServerSelectionTimeoutError,
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
from ..database import ScopedMongoWrapper
|
|
20
24
|
from ..observability import get_logger as get_contextual_logger
|
|
@@ -47,12 +51,10 @@ class ServiceInitializer:
|
|
|
47
51
|
self.mongo_uri = mongo_uri
|
|
48
52
|
self.db_name = db_name
|
|
49
53
|
self.get_scoped_db_fn = get_scoped_db_fn
|
|
50
|
-
self._memory_services:
|
|
51
|
-
self._websocket_configs:
|
|
54
|
+
self._memory_services: dict[str, Any] = {}
|
|
55
|
+
self._websocket_configs: dict[str, dict[str, Any]] = {}
|
|
52
56
|
|
|
53
|
-
async def initialize_memory_service(
|
|
54
|
-
self, slug: str, memory_config: Dict[str, Any]
|
|
55
|
-
) -> None:
|
|
57
|
+
async def initialize_memory_service(self, slug: str, memory_config: dict[str, Any]) -> None:
|
|
56
58
|
"""
|
|
57
59
|
Initialize Mem0 memory service for an app.
|
|
58
60
|
|
|
@@ -79,9 +81,7 @@ class ServiceInitializer:
|
|
|
79
81
|
f"Initializing Mem0 memory service for app '{slug}'",
|
|
80
82
|
extra={
|
|
81
83
|
"app_slug": slug,
|
|
82
|
-
"collection_name": memory_config.get(
|
|
83
|
-
"collection_name", f"{slug}_memories"
|
|
84
|
-
),
|
|
84
|
+
"collection_name": memory_config.get("collection_name", f"{slug}_memories"),
|
|
85
85
|
"enable_graph": memory_config.get("enable_graph", False),
|
|
86
86
|
"embedding_model_dims": memory_config.get("embedding_model_dims", 1536),
|
|
87
87
|
"infer": memory_config.get("infer", True),
|
|
@@ -152,9 +152,7 @@ class ServiceInitializer:
|
|
|
152
152
|
exc_info=True,
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
-
async def register_websockets(
|
|
156
|
-
self, slug: str, websockets_config: Dict[str, Any]
|
|
157
|
-
) -> None:
|
|
155
|
+
async def register_websockets(self, slug: str, websockets_config: dict[str, Any]) -> None:
|
|
158
156
|
"""
|
|
159
157
|
Register WebSocket endpoints for an app.
|
|
160
158
|
|
|
@@ -190,9 +188,7 @@ class ServiceInitializer:
|
|
|
190
188
|
try:
|
|
191
189
|
await get_websocket_manager(slug)
|
|
192
190
|
except (ImportError, AttributeError, RuntimeError) as e:
|
|
193
|
-
contextual_logger.warning(
|
|
194
|
-
f"Could not initialize WebSocket manager for {slug}: {e}"
|
|
195
|
-
)
|
|
191
|
+
contextual_logger.warning(f"Could not initialize WebSocket manager for {slug}: {e}")
|
|
196
192
|
continue
|
|
197
193
|
contextual_logger.debug(
|
|
198
194
|
f"Configured WebSocket endpoint '{endpoint_name}' at path '{path}'",
|
|
@@ -200,7 +196,7 @@ class ServiceInitializer:
|
|
|
200
196
|
)
|
|
201
197
|
|
|
202
198
|
async def seed_initial_data(
|
|
203
|
-
self, slug: str, initial_data:
|
|
199
|
+
self, slug: str, initial_data: dict[str, list[dict[str, Any]]]
|
|
204
200
|
) -> None:
|
|
205
201
|
"""
|
|
206
202
|
Seed initial data into collections for an app.
|
|
@@ -221,9 +217,7 @@ class ServiceInitializer:
|
|
|
221
217
|
f"Seeded initial data for app '{slug}'",
|
|
222
218
|
extra={
|
|
223
219
|
"app_slug": slug,
|
|
224
|
-
"collections_seeded": len(
|
|
225
|
-
[c for c, count in results.items() if count > 0]
|
|
226
|
-
),
|
|
220
|
+
"collections_seeded": len([c for c, count in results.items() if count > 0]),
|
|
227
221
|
"total_documents": total_inserted,
|
|
228
222
|
},
|
|
229
223
|
)
|
|
@@ -247,7 +241,7 @@ class ServiceInitializer:
|
|
|
247
241
|
)
|
|
248
242
|
|
|
249
243
|
async def setup_observability(
|
|
250
|
-
self, slug: str, manifest:
|
|
244
|
+
self, slug: str, manifest: dict[str, Any], observability_config: dict[str, Any]
|
|
251
245
|
) -> None:
|
|
252
246
|
"""
|
|
253
247
|
Set up observability features (health checks, metrics, logging) from manifest.
|
|
@@ -276,9 +270,7 @@ class ServiceInitializer:
|
|
|
276
270
|
contextual_logger.info(
|
|
277
271
|
f"Metrics collection configured for {slug}",
|
|
278
272
|
extra={
|
|
279
|
-
"operation_metrics": metrics_config.get(
|
|
280
|
-
"collect_operation_metrics", True
|
|
281
|
-
),
|
|
273
|
+
"operation_metrics": metrics_config.get("collect_operation_metrics", True),
|
|
282
274
|
"performance_metrics": metrics_config.get(
|
|
283
275
|
"collect_performance_metrics", True
|
|
284
276
|
),
|
|
@@ -296,9 +288,7 @@ class ServiceInitializer:
|
|
|
296
288
|
extra={
|
|
297
289
|
"level": log_level,
|
|
298
290
|
"format": log_format,
|
|
299
|
-
"include_request_id": logging_config.get(
|
|
300
|
-
"include_request_id", True
|
|
301
|
-
),
|
|
291
|
+
"include_request_id": logging_config.get("include_request_id", True),
|
|
302
292
|
},
|
|
303
293
|
)
|
|
304
294
|
|
|
@@ -307,7 +297,7 @@ class ServiceInitializer:
|
|
|
307
297
|
f"Could not set up observability for {slug}: {e}", exc_info=True
|
|
308
298
|
)
|
|
309
299
|
|
|
310
|
-
def get_websocket_config(self, slug: str) ->
|
|
300
|
+
def get_websocket_config(self, slug: str) -> dict[str, Any] | None:
|
|
311
301
|
"""
|
|
312
302
|
Get WebSocket configuration for an app.
|
|
313
303
|
|
|
@@ -319,7 +309,7 @@ class ServiceInitializer:
|
|
|
319
309
|
"""
|
|
320
310
|
return self._websocket_configs.get(slug)
|
|
321
311
|
|
|
322
|
-
def get_memory_service(self, slug: str) ->
|
|
312
|
+
def get_memory_service(self, slug: str) -> Any | None:
|
|
323
313
|
"""
|
|
324
314
|
Get Mem0 memory service for an app.
|
|
325
315
|
|