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,296 @@
1
+ """
2
+ Health check utilities for MDB_ENGINE.
3
+
4
+ Provides health check functions for monitoring system status.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from typing import Any, Callable, Dict, List, Optional
13
+
14
+ from pymongo.errors import (ConnectionFailure, OperationFailure,
15
+ ServerSelectionTimeoutError)
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class HealthStatus(str, Enum):
21
+ """Health status enumeration."""
22
+
23
+ HEALTHY = "healthy"
24
+ DEGRADED = "degraded"
25
+ UNHEALTHY = "unhealthy"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ @dataclass
30
+ class HealthCheckResult:
31
+ """Result of a health check."""
32
+
33
+ name: str
34
+ status: HealthStatus
35
+ message: str
36
+ details: Optional[Dict[str, Any]] = None
37
+ timestamp: datetime = field(default_factory=datetime.now)
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ """Convert to dictionary."""
41
+ return {
42
+ "name": self.name,
43
+ "status": self.status.value,
44
+ "message": self.message,
45
+ "details": self.details,
46
+ "timestamp": self.timestamp.isoformat(),
47
+ }
48
+
49
+
50
+ class HealthChecker:
51
+ """
52
+ Health checker for MDB_ENGINE components.
53
+ """
54
+
55
+ def __init__(self):
56
+ """Initialize the health checker."""
57
+ self._checks: List[callable] = []
58
+
59
+ def register_check(self, check_func: Callable) -> None:
60
+ """
61
+ Register a health check function.
62
+
63
+ Args:
64
+ check_func: Async function that returns HealthCheckResult
65
+ """
66
+ self._checks.append(check_func)
67
+
68
+ async def check_all(self) -> Dict[str, Any]:
69
+ """
70
+ Run all registered health checks.
71
+
72
+ Returns:
73
+ Dictionary with overall status and individual check results
74
+ """
75
+ results: List[HealthCheckResult] = []
76
+
77
+ for check_func in self._checks:
78
+ try:
79
+ result = await check_func()
80
+ results.append(result)
81
+ except (
82
+ RuntimeError,
83
+ ValueError,
84
+ TypeError,
85
+ AttributeError,
86
+ ConnectionError,
87
+ OSError,
88
+ ) as e:
89
+ logger.error(
90
+ f"Health check {check_func.__name__} failed: {e}", exc_info=True
91
+ )
92
+ results.append(
93
+ HealthCheckResult(
94
+ name=check_func.__name__,
95
+ status=HealthStatus.UNKNOWN,
96
+ message=f"Check failed: {str(e)}",
97
+ )
98
+ )
99
+
100
+ # Determine overall status
101
+ statuses = [r.status for r in results]
102
+ if HealthStatus.UNHEALTHY in statuses:
103
+ overall_status = HealthStatus.UNHEALTHY
104
+ elif HealthStatus.DEGRADED in statuses:
105
+ overall_status = HealthStatus.DEGRADED
106
+ elif all(s == HealthStatus.HEALTHY for s in statuses):
107
+ overall_status = HealthStatus.HEALTHY
108
+ else:
109
+ overall_status = HealthStatus.UNKNOWN
110
+
111
+ return {
112
+ "status": overall_status.value,
113
+ "timestamp": datetime.now().isoformat(),
114
+ "checks": [r.to_dict() for r in results],
115
+ }
116
+
117
+
118
+ async def check_mongodb_health(
119
+ mongo_client: Optional[Any], timeout_seconds: float = 5.0
120
+ ) -> HealthCheckResult:
121
+ """
122
+ Check MongoDB connection health.
123
+
124
+ Args:
125
+ mongo_client: MongoDB client instance
126
+ timeout_seconds: Timeout for health check
127
+
128
+ Returns:
129
+ HealthCheckResult
130
+ """
131
+ if mongo_client is None:
132
+ return HealthCheckResult(
133
+ name="mongodb",
134
+ status=HealthStatus.UNHEALTHY,
135
+ message="MongoDB client not initialized",
136
+ )
137
+
138
+ try:
139
+ # Try to ping MongoDB with timeout
140
+ await asyncio.wait_for(
141
+ mongo_client.admin.command("ping"), timeout=timeout_seconds
142
+ )
143
+
144
+ return HealthCheckResult(
145
+ name="mongodb",
146
+ status=HealthStatus.HEALTHY,
147
+ message="MongoDB connection is healthy",
148
+ details={"timeout_seconds": timeout_seconds},
149
+ )
150
+ except asyncio.TimeoutError:
151
+ return HealthCheckResult(
152
+ name="mongodb",
153
+ status=HealthStatus.UNHEALTHY,
154
+ message=f"MongoDB ping timed out after {timeout_seconds}s",
155
+ )
156
+ except (
157
+ ConnectionFailure,
158
+ OperationFailure,
159
+ ServerSelectionTimeoutError,
160
+ AttributeError,
161
+ TypeError,
162
+ ) as e:
163
+ return HealthCheckResult(
164
+ name="mongodb",
165
+ status=HealthStatus.UNHEALTHY,
166
+ message=f"MongoDB health check failed: {str(e)}",
167
+ )
168
+
169
+
170
+ async def check_engine_health(engine: Optional[Any]) -> HealthCheckResult:
171
+ """
172
+ Check MongoDB Engine health.
173
+
174
+ Args:
175
+ engine: MongoDBEngine instance
176
+
177
+ Returns:
178
+ HealthCheckResult
179
+ """
180
+ if engine is None:
181
+ return HealthCheckResult(
182
+ name="engine",
183
+ status=HealthStatus.UNHEALTHY,
184
+ message="MongoDBEngine not initialized",
185
+ )
186
+
187
+ if not engine._initialized:
188
+ return HealthCheckResult(
189
+ name="engine",
190
+ status=HealthStatus.UNHEALTHY,
191
+ message="MongoDBEngine not initialized",
192
+ )
193
+
194
+ # Check registered apps
195
+ app_count = len(engine._apps)
196
+
197
+ return HealthCheckResult(
198
+ name="engine",
199
+ status=HealthStatus.HEALTHY,
200
+ message="MongoDBEngine is healthy",
201
+ details={
202
+ "initialized": True,
203
+ "app_count": app_count,
204
+ },
205
+ )
206
+
207
+
208
+ async def check_pool_health(
209
+ get_pool_metrics_func: Optional[Callable[[], Any]] = None
210
+ ) -> HealthCheckResult:
211
+ """
212
+ Check connection pool health.
213
+
214
+ Args:
215
+ get_pool_metrics_func: Function to get pool metrics
216
+
217
+ Returns:
218
+ HealthCheckResult
219
+ """
220
+ if get_pool_metrics_func is None:
221
+ return HealthCheckResult(
222
+ name="connection_pool",
223
+ status=HealthStatus.UNKNOWN,
224
+ message="Pool metrics function not available",
225
+ )
226
+
227
+ try:
228
+ # Call the function to get metrics (handles both sync and async)
229
+ if asyncio.iscoroutinefunction(get_pool_metrics_func):
230
+ metrics = await get_pool_metrics_func()
231
+ else:
232
+ metrics = get_pool_metrics_func()
233
+
234
+ status_value = metrics.get("status")
235
+ if status_value != "connected":
236
+ # "no_client" means shared client not initialized, but engine might use its own client
237
+ # This is not a critical failure, just means pool metrics aren't available
238
+ if status_value == "no_client":
239
+ return HealthCheckResult(
240
+ name="connection_pool",
241
+ status=HealthStatus.UNKNOWN,
242
+ message="Pool metrics not available (engine using dedicated client)",
243
+ details=metrics,
244
+ )
245
+ # "error" means we couldn't get metrics, but the client might still be working
246
+ # This is not a critical failure - the engine and MongoDB checks already
247
+ # verify connectivity
248
+ if status_value == "error":
249
+ return HealthCheckResult(
250
+ name="connection_pool",
251
+ status=HealthStatus.UNKNOWN,
252
+ message=f"Pool metrics unavailable: {metrics.get('error', 'Unknown error')}",
253
+ details=metrics,
254
+ )
255
+ # Other non-connected statuses are still unhealthy
256
+ return HealthCheckResult(
257
+ name="connection_pool",
258
+ status=HealthStatus.UNHEALTHY,
259
+ message=f"Pool status: {status_value}",
260
+ details=metrics,
261
+ )
262
+
263
+ # Client is connected - check usage if available
264
+ usage_percent = metrics.get("pool_usage_percent")
265
+
266
+ # If we have usage data, check it
267
+ if usage_percent is not None:
268
+ if usage_percent > 90:
269
+ status = HealthStatus.UNHEALTHY
270
+ message = f"Connection pool usage is critical: {usage_percent:.1f}%"
271
+ elif usage_percent > 80:
272
+ status = HealthStatus.DEGRADED
273
+ message = f"Connection pool usage is high: {usage_percent:.1f}%"
274
+ else:
275
+ status = HealthStatus.HEALTHY
276
+ message = f"Connection pool is healthy: {usage_percent:.1f}% usage"
277
+ else:
278
+ # No usage data available, but client is connected
279
+ # This is fine - detailed metrics might not be available but client works
280
+ status = HealthStatus.HEALTHY
281
+ message = metrics.get(
282
+ "note", "Connection pool is operational (detailed metrics unavailable)"
283
+ )
284
+
285
+ return HealthCheckResult(
286
+ name="connection_pool",
287
+ status=status,
288
+ message=message,
289
+ details=metrics,
290
+ )
291
+ except (AttributeError, TypeError, ValueError, KeyError, RuntimeError) as e:
292
+ return HealthCheckResult(
293
+ name="connection_pool",
294
+ status=HealthStatus.UNKNOWN,
295
+ message=f"Failed to check pool health: {str(e)}",
296
+ )
@@ -0,0 +1,161 @@
1
+ """
2
+ Enhanced logging utilities for MDB_ENGINE.
3
+
4
+ Provides structured logging with correlation IDs and context.
5
+ """
6
+
7
+ import contextvars
8
+ import logging
9
+ import uuid
10
+ from datetime import datetime
11
+ from typing import Any, Dict, Optional
12
+
13
+ # Context variable for correlation ID
14
+ _correlation_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
15
+ "correlation_id", default=None
16
+ )
17
+
18
+ # Context variable for app context
19
+ _app_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
20
+ "app_context", default=None
21
+ )
22
+
23
+
24
+ def get_correlation_id() -> Optional[str]:
25
+ """Get the current correlation ID from context."""
26
+ return _correlation_id.get()
27
+
28
+
29
+ def set_correlation_id(correlation_id: Optional[str] = None) -> str:
30
+ """
31
+ Set a correlation ID in the current context.
32
+
33
+ Args:
34
+ correlation_id: Optional correlation ID (generates new one if None)
35
+
36
+ Returns:
37
+ The correlation ID that was set
38
+ """
39
+ if correlation_id is None:
40
+ correlation_id = str(uuid.uuid4())
41
+ _correlation_id.set(correlation_id)
42
+ return correlation_id
43
+
44
+
45
+ def clear_correlation_id() -> None:
46
+ """Clear the correlation ID from context."""
47
+ _correlation_id.set(None)
48
+
49
+
50
+ def set_app_context(app_slug: Optional[str] = None, **kwargs: Any) -> None:
51
+ """
52
+ Set app context for logging.
53
+
54
+ Args:
55
+ app_slug: App slug
56
+ **kwargs: Additional context (collection_name, user_id, etc.)
57
+ """
58
+ context = {"app_slug": app_slug, **kwargs}
59
+ _app_context.set(context)
60
+
61
+
62
+ def clear_app_context() -> None:
63
+ """Clear app context."""
64
+ _app_context.set(None)
65
+
66
+
67
+ def get_logging_context() -> Dict[str, Any]:
68
+ """
69
+ Get current logging context (correlation ID and app context).
70
+
71
+ Returns:
72
+ Dictionary with context information
73
+ """
74
+ context: Dict[str, Any] = {
75
+ "timestamp": datetime.now().isoformat(),
76
+ }
77
+
78
+ correlation_id = get_correlation_id()
79
+ if correlation_id:
80
+ context["correlation_id"] = correlation_id
81
+
82
+ app_context = _app_context.get()
83
+ if app_context:
84
+ context.update(app_context)
85
+
86
+ return context
87
+
88
+
89
+ class ContextualLoggerAdapter(logging.LoggerAdapter):
90
+ """
91
+ Logger adapter that automatically adds context to log records.
92
+ """
93
+
94
+ def process(self, msg: str, kwargs: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
95
+ """Add context to log records."""
96
+ # Get base context
97
+ context = get_logging_context()
98
+
99
+ # Merge with any extra context provided
100
+ extra = kwargs.get("extra", {})
101
+ if extra:
102
+ context.update(extra)
103
+
104
+ kwargs["extra"] = context
105
+ return msg, kwargs
106
+
107
+
108
+ def get_logger(name: str) -> ContextualLoggerAdapter:
109
+ """
110
+ Get a contextual logger that automatically adds correlation ID and context.
111
+
112
+ Args:
113
+ name: Logger name (typically __name__)
114
+
115
+ Returns:
116
+ ContextualLoggerAdapter instance
117
+ """
118
+ base_logger = logging.getLogger(name)
119
+ return ContextualLoggerAdapter(base_logger, {})
120
+
121
+
122
+ def log_operation(
123
+ logger: logging.Logger,
124
+ operation: str,
125
+ level: int = logging.INFO,
126
+ success: bool = True,
127
+ duration_ms: Optional[float] = None,
128
+ **context: Any,
129
+ ) -> None:
130
+ """
131
+ Log an operation with structured context.
132
+
133
+ Args:
134
+ logger: Logger instance
135
+ operation: Operation name
136
+ level: Log level
137
+ success: Whether operation succeeded
138
+ duration_ms: Operation duration in milliseconds
139
+ **context: Additional context
140
+ """
141
+ log_context = get_logging_context()
142
+ log_context.update(
143
+ {
144
+ "operation": operation,
145
+ "success": success,
146
+ }
147
+ )
148
+
149
+ if duration_ms is not None:
150
+ log_context["duration_ms"] = round(duration_ms, 2)
151
+
152
+ if context:
153
+ log_context.update(context)
154
+
155
+ message = f"Operation: {operation}"
156
+ if not success:
157
+ message = f"Operation failed: {operation}"
158
+ if duration_ms is not None:
159
+ message += f" (duration: {duration_ms:.2f}ms)"
160
+
161
+ logger.log(level, message, extra=log_context)