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.
Files changed (75) hide show
  1. mdb_engine/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,179 @@
1
+ """
2
+ Initial Data Seeding
3
+
4
+ Provides utilities for seeding initial data into collections based on manifest configuration.
5
+
6
+ This module is part of MDB_ENGINE - MongoDB Engine.
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime
11
+ from typing import Any, Dict, List
12
+
13
+ from pymongo.errors import (ConnectionFailure, OperationFailure,
14
+ ServerSelectionTimeoutError)
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ async def seed_initial_data(
20
+ db, app_slug: str, initial_data: Dict[str, List[Dict[str, Any]]]
21
+ ) -> Dict[str, int]:
22
+ """
23
+ Seed initial data into collections.
24
+
25
+ This function:
26
+ 1. Checks if each collection is empty
27
+ 2. Only seeds if collection is empty (idempotent)
28
+ 3. Tracks seeded collections in metadata collection
29
+ 4. Handles datetime conversion for seed data
30
+
31
+ Args:
32
+ db: Database wrapper (ScopedMongoWrapper or AppDB)
33
+ app_slug: App slug identifier
34
+ initial_data: Dictionary mapping collection names to arrays of documents
35
+
36
+ Returns:
37
+ Dictionary mapping collection names to number of documents inserted
38
+ """
39
+ results = {}
40
+
41
+ # Check metadata collection to see if we've already seeded
42
+ # Use a non-underscore name to avoid ScopedMongoWrapper attribute access restrictions
43
+ metadata_collection_name = "app_seeding_metadata"
44
+ # Use getattr to access collection (works with ScopedMongoWrapper and AppDB)
45
+ metadata_collection = getattr(db, metadata_collection_name)
46
+
47
+ # Get existing seeding metadata
48
+ seeding_metadata = await metadata_collection.find_one({"app_slug": app_slug})
49
+ seeded_collections = (
50
+ set(seeding_metadata.get("seeded_collections", []))
51
+ if seeding_metadata
52
+ else set()
53
+ )
54
+
55
+ for collection_name, documents in initial_data.items():
56
+ try:
57
+ # Check if already seeded
58
+ if collection_name in seeded_collections:
59
+ logger.debug(
60
+ f"Collection '{collection_name}' already seeded for {app_slug}, skipping"
61
+ )
62
+ results[collection_name] = 0
63
+ continue
64
+
65
+ # Get collection
66
+ collection = getattr(db, collection_name)
67
+
68
+ # Check if collection is empty (idempotent check)
69
+ count = await collection.count_documents({})
70
+ if count > 0:
71
+ logger.info(
72
+ f"Collection '{collection_name}' is not empty "
73
+ f"({count} documents) for {app_slug}, "
74
+ f"skipping seed to avoid duplicates"
75
+ )
76
+ # Mark as seeded even though we didn't insert (collection already has data)
77
+ seeded_collections.add(collection_name)
78
+ results[collection_name] = 0
79
+ continue
80
+
81
+ # Prepare documents for insertion
82
+ # Convert datetime strings to datetime objects if needed
83
+ prepared_docs = []
84
+ for doc in documents:
85
+ prepared_doc = doc.copy()
86
+
87
+ # Convert datetime strings to datetime objects
88
+ for key, value in prepared_doc.items():
89
+ if isinstance(value, str):
90
+ # Try to parse ISO format datetime strings
91
+ try:
92
+ # 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
+ ):
96
+ # Try parsing as ISO format
97
+ from dateutil.parser import parse as parse_date
98
+
99
+ prepared_doc[key] = parse_date(value)
100
+ except (ValueError, ImportError):
101
+ # Not a datetime string or dateutil not available, keep as string
102
+ pass
103
+ elif isinstance(value, dict) and "$date" in value:
104
+ # MongoDB extended JSON format
105
+ try:
106
+ from dateutil.parser import parse as parse_date
107
+
108
+ prepared_doc[key] = parse_date(value["$date"])
109
+ except (ValueError, ImportError):
110
+ pass
111
+
112
+ # 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
+ ):
117
+ prepared_doc["created_at"] = datetime.utcnow()
118
+
119
+ prepared_docs.append(prepared_doc)
120
+
121
+ # Insert documents
122
+ if prepared_docs:
123
+ result = await collection.insert_many(prepared_docs)
124
+ inserted_count = len(result.inserted_ids)
125
+ results[collection_name] = inserted_count
126
+
127
+ # Mark as seeded
128
+ seeded_collections.add(collection_name)
129
+
130
+ logger.info(
131
+ f"✅ Seeded {inserted_count} document(s) into collection "
132
+ f"'{collection_name}' for {app_slug}"
133
+ )
134
+ else:
135
+ results[collection_name] = 0
136
+ logger.warning(
137
+ f"No documents to seed for collection '{collection_name}' in {app_slug}"
138
+ )
139
+
140
+ except (
141
+ OperationFailure,
142
+ ConnectionFailure,
143
+ ServerSelectionTimeoutError,
144
+ Exception,
145
+ ) as e:
146
+ # 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
+ )
150
+ results[collection_name] = 0
151
+
152
+ # Update seeding metadata
153
+ try:
154
+ if seeding_metadata:
155
+ await metadata_collection.update_one(
156
+ {"app_slug": app_slug},
157
+ {"$set": {"seeded_collections": list(seeded_collections)}},
158
+ )
159
+ else:
160
+ await metadata_collection.insert_one(
161
+ {
162
+ "app_slug": app_slug,
163
+ "seeded_collections": list(seeded_collections),
164
+ "created_at": datetime.utcnow(),
165
+ }
166
+ )
167
+ except (
168
+ OperationFailure,
169
+ ConnectionFailure,
170
+ ServerSelectionTimeoutError,
171
+ ValueError,
172
+ TypeError,
173
+ KeyError,
174
+ ) as e:
175
+ logger.warning(
176
+ f"Failed to update seeding metadata for {app_slug}: {e}", exc_info=True
177
+ )
178
+
179
+ return results
@@ -0,0 +1,355 @@
1
+ """
2
+ Service initialization for MongoDB Engine.
3
+
4
+ This module handles initialization of optional services:
5
+ - Memory service (Mem0)
6
+ - WebSocket endpoints
7
+ - Observability (health checks, metrics, logging)
8
+ - Data seeding
9
+
10
+ This module is part of MDB_ENGINE - MongoDB Engine.
11
+ """
12
+
13
+ import logging
14
+ from typing import Any, Callable, Dict, List, Optional
15
+
16
+ from pymongo.errors import (ConnectionFailure, OperationFailure,
17
+ ServerSelectionTimeoutError)
18
+
19
+ from ..database import ScopedMongoWrapper
20
+ from ..observability import get_logger as get_contextual_logger
21
+
22
+ logger = logging.getLogger(__name__)
23
+ contextual_logger = get_contextual_logger(__name__)
24
+
25
+
26
+ class ServiceInitializer:
27
+ """
28
+ Manages initialization of optional services for apps.
29
+
30
+ Handles memory service, WebSocket endpoints, observability, and data seeding.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ mongo_uri: str,
36
+ db_name: str,
37
+ get_scoped_db_fn: Callable[[str], ScopedMongoWrapper],
38
+ ) -> None:
39
+ """
40
+ Initialize the service initializer.
41
+
42
+ Args:
43
+ mongo_uri: MongoDB connection URI
44
+ db_name: Database name
45
+ get_scoped_db_fn: Function to get scoped database wrapper
46
+ """
47
+ self.mongo_uri = mongo_uri
48
+ self.db_name = db_name
49
+ self.get_scoped_db_fn = get_scoped_db_fn
50
+ self._memory_services: Dict[str, Any] = {}
51
+ self._websocket_configs: Dict[str, Dict[str, Any]] = {}
52
+
53
+ async def initialize_memory_service(
54
+ self, slug: str, memory_config: Dict[str, Any]
55
+ ) -> None:
56
+ """
57
+ Initialize Mem0 memory service for an app.
58
+
59
+ Memory support is OPTIONAL - only processes if dependencies are available.
60
+ mem0 handles embeddings and LLM via environment variables (.env).
61
+
62
+ Args:
63
+ slug: App slug
64
+ memory_config: Memory configuration from manifest (already validated)
65
+ """
66
+ # Try to import Memory service (optional dependency)
67
+ try:
68
+ from ..memory import Mem0MemoryService, Mem0MemoryServiceError
69
+ except ImportError as e:
70
+ contextual_logger.warning(
71
+ f"Memory configuration found for app '{slug}' but "
72
+ f"dependencies are not available: {e}. "
73
+ f"Memory support will be disabled for this app. Install with: "
74
+ f"pip install mem0ai"
75
+ )
76
+ return
77
+
78
+ contextual_logger.info(
79
+ f"Initializing Mem0 memory service for app '{slug}'",
80
+ extra={
81
+ "app_slug": slug,
82
+ "collection_name": memory_config.get(
83
+ "collection_name", f"{slug}_memories"
84
+ ),
85
+ "enable_graph": memory_config.get("enable_graph", False),
86
+ "embedding_model_dims": memory_config.get("embedding_model_dims", 1536),
87
+ "infer": memory_config.get("infer", True),
88
+ },
89
+ )
90
+
91
+ try:
92
+ # Extract memory config (exclude 'enabled')
93
+ service_config = {
94
+ k: v
95
+ for k, v in memory_config.items()
96
+ if k != "enabled"
97
+ and k
98
+ in [
99
+ "collection_name",
100
+ "embedding_model_dims",
101
+ "enable_graph",
102
+ "infer",
103
+ "async_mode",
104
+ "embedding_model",
105
+ "chat_model",
106
+ "temperature",
107
+ ]
108
+ }
109
+
110
+ # Set default collection name if not provided
111
+ if "collection_name" not in service_config:
112
+ service_config["collection_name"] = f"{slug}_memories"
113
+ else:
114
+ # Ensure collection name is prefixed with app slug
115
+ collection_name = service_config["collection_name"]
116
+ if not collection_name.startswith(f"{slug}_"):
117
+ service_config["collection_name"] = f"{slug}_{collection_name}"
118
+ contextual_logger.info(
119
+ f"Prefixed memory collection name: "
120
+ f"'{collection_name}' -> "
121
+ f"'{service_config['collection_name']}'",
122
+ extra={
123
+ "app_slug": slug,
124
+ "original": collection_name,
125
+ "prefixed": service_config["collection_name"],
126
+ },
127
+ )
128
+
129
+ # Create Memory service with MongoDB integration
130
+ memory_service = Mem0MemoryService(
131
+ mongo_uri=self.mongo_uri,
132
+ db_name=self.db_name,
133
+ app_slug=slug,
134
+ config=service_config,
135
+ )
136
+ self._memory_services[slug] = memory_service
137
+
138
+ contextual_logger.info(
139
+ f"Mem0 memory service initialized for app '{slug}'",
140
+ extra={"app_slug": slug},
141
+ )
142
+ except Mem0MemoryServiceError as e:
143
+ contextual_logger.error(
144
+ f"Failed to initialize memory service for app '{slug}': {e}",
145
+ extra={"app_slug": slug, "error": str(e)},
146
+ exc_info=True,
147
+ )
148
+ except (ImportError, AttributeError, TypeError, ValueError) as e:
149
+ contextual_logger.error(
150
+ f"Error initializing memory service for app '{slug}': {e}",
151
+ extra={"app_slug": slug, "error": str(e)},
152
+ exc_info=True,
153
+ )
154
+
155
+ async def register_websockets(
156
+ self, slug: str, websockets_config: Dict[str, Any]
157
+ ) -> None:
158
+ """
159
+ Register WebSocket endpoints for an app.
160
+
161
+ WebSocket support is OPTIONAL - only processes if dependencies are available.
162
+
163
+ Args:
164
+ slug: App slug
165
+ websockets_config: WebSocket configuration from manifest
166
+ """
167
+ # Try to import WebSocket support (optional dependency)
168
+ try:
169
+ from ..routing.websockets import get_websocket_manager
170
+ except ImportError as e:
171
+ contextual_logger.warning(
172
+ f"WebSocket configuration found for app '{slug}' but "
173
+ f"dependencies are not available: {e}. "
174
+ f"WebSocket support will be disabled for this app. "
175
+ f"Install FastAPI with WebSocket support."
176
+ )
177
+ return
178
+
179
+ contextual_logger.info(
180
+ f"Registering WebSocket endpoints for app '{slug}'",
181
+ extra={"app_slug": slug, "endpoint_count": len(websockets_config)},
182
+ )
183
+
184
+ # Store WebSocket configuration for later route registration
185
+ self._websocket_configs[slug] = websockets_config
186
+
187
+ # Pre-initialize WebSocket managers
188
+ for endpoint_name, endpoint_config in websockets_config.items():
189
+ path = endpoint_config.get("path", f"/{endpoint_name}")
190
+ try:
191
+ await get_websocket_manager(slug)
192
+ except (ImportError, AttributeError, RuntimeError) as e:
193
+ contextual_logger.warning(
194
+ f"Could not initialize WebSocket manager for {slug}: {e}"
195
+ )
196
+ continue
197
+ contextual_logger.debug(
198
+ f"Configured WebSocket endpoint '{endpoint_name}' at path '{path}'",
199
+ extra={"app_slug": slug, "endpoint": endpoint_name, "path": path},
200
+ )
201
+
202
+ async def seed_initial_data(
203
+ self, slug: str, initial_data: Dict[str, List[Dict[str, Any]]]
204
+ ) -> None:
205
+ """
206
+ Seed initial data into collections for an app.
207
+
208
+ Args:
209
+ slug: App slug
210
+ initial_data: Dictionary mapping collection names to arrays of documents
211
+ """
212
+ try:
213
+ from .seeding import seed_initial_data
214
+
215
+ db = self.get_scoped_db_fn(slug)
216
+ results = await seed_initial_data(db, slug, initial_data)
217
+
218
+ total_inserted = sum(results.values())
219
+ if total_inserted > 0:
220
+ contextual_logger.info(
221
+ f"Seeded initial data for app '{slug}'",
222
+ extra={
223
+ "app_slug": slug,
224
+ "collections_seeded": len(
225
+ [c for c, count in results.items() if count > 0]
226
+ ),
227
+ "total_documents": total_inserted,
228
+ },
229
+ )
230
+ else:
231
+ contextual_logger.debug(
232
+ f"No initial data seeded for app '{slug}' "
233
+ f"(collections already had data or were empty)",
234
+ extra={"app_slug": slug},
235
+ )
236
+ except (
237
+ OperationFailure,
238
+ ConnectionFailure,
239
+ ServerSelectionTimeoutError,
240
+ ValueError,
241
+ TypeError,
242
+ ) as e:
243
+ contextual_logger.error(
244
+ f"Failed to seed initial data for app '{slug}': {e}",
245
+ extra={"app_slug": slug, "error": str(e)},
246
+ exc_info=True,
247
+ )
248
+
249
+ async def setup_observability(
250
+ self, slug: str, manifest: Dict[str, Any], observability_config: Dict[str, Any]
251
+ ) -> None:
252
+ """
253
+ Set up observability features (health checks, metrics, logging) from manifest.
254
+
255
+ Args:
256
+ slug: App slug
257
+ manifest: Full manifest dictionary
258
+ observability_config: Observability configuration from manifest
259
+ """
260
+ try:
261
+ # Set up health checks
262
+ health_config = observability_config.get("health_checks", {})
263
+ if health_config.get("enabled", True):
264
+ endpoint = health_config.get("endpoint", "/health")
265
+ contextual_logger.info(
266
+ f"Health checks configured for {slug}",
267
+ extra={
268
+ "endpoint": endpoint,
269
+ "interval_seconds": health_config.get("interval_seconds", 30),
270
+ },
271
+ )
272
+
273
+ # Set up metrics
274
+ metrics_config = observability_config.get("metrics", {})
275
+ if metrics_config.get("enabled", True):
276
+ contextual_logger.info(
277
+ f"Metrics collection configured for {slug}",
278
+ extra={
279
+ "operation_metrics": metrics_config.get(
280
+ "collect_operation_metrics", True
281
+ ),
282
+ "performance_metrics": metrics_config.get(
283
+ "collect_performance_metrics", True
284
+ ),
285
+ "custom_metrics": metrics_config.get("custom_metrics", []),
286
+ },
287
+ )
288
+
289
+ # Set up logging
290
+ logging_config = observability_config.get("logging", {})
291
+ if logging_config:
292
+ log_level = logging_config.get("level", "INFO")
293
+ log_format = logging_config.get("format", "json")
294
+ contextual_logger.info(
295
+ f"Logging configured for {slug}",
296
+ extra={
297
+ "level": log_level,
298
+ "format": log_format,
299
+ "include_request_id": logging_config.get(
300
+ "include_request_id", True
301
+ ),
302
+ },
303
+ )
304
+
305
+ except (ImportError, AttributeError, TypeError, ValueError, KeyError) as e:
306
+ contextual_logger.warning(
307
+ f"Could not set up observability for {slug}: {e}", exc_info=True
308
+ )
309
+
310
+ def get_websocket_config(self, slug: str) -> Optional[Dict[str, Any]]:
311
+ """
312
+ Get WebSocket configuration for an app.
313
+
314
+ Args:
315
+ slug: App slug
316
+
317
+ Returns:
318
+ WebSocket configuration dict or None if not configured
319
+ """
320
+ return self._websocket_configs.get(slug)
321
+
322
+ def get_memory_service(self, slug: str) -> Optional[Any]:
323
+ """
324
+ Get Mem0 memory service for an app.
325
+
326
+ Args:
327
+ slug: App slug
328
+
329
+ Returns:
330
+ Mem0MemoryService instance if memory is enabled for this app, None otherwise
331
+ """
332
+ try:
333
+ service = self._memory_services.get(slug)
334
+ if service is not None:
335
+ # Quick health check - ensure it has the memory attribute
336
+ if not hasattr(service, "memory"):
337
+ contextual_logger.warning(
338
+ f"Memory service for '{slug}' is missing 'memory' "
339
+ f"attribute, returning None",
340
+ extra={"app_slug": slug},
341
+ )
342
+ return None
343
+ return service
344
+ except (KeyError, AttributeError, TypeError) as e:
345
+ contextual_logger.error(
346
+ f"Error retrieving memory service for '{slug}': {e}",
347
+ exc_info=True,
348
+ extra={"app_slug": slug, "error": str(e)},
349
+ )
350
+ return None
351
+
352
+ def clear_services(self) -> None:
353
+ """Clear all service state."""
354
+ self._memory_services.clear()
355
+ self._websocket_configs.clear()