mdb-engine 0.1.6__py3-none-any.whl → 0.2.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.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ Unit of Work Pattern
3
+
4
+ Manages repository access and provides a clean interface for data operations.
5
+ The UnitOfWork acts as a factory for repositories and manages their lifecycle.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Generic, Optional, Type, TypeVar
10
+
11
+ from .base import Entity, Repository
12
+ from .mongo import MongoRepository
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ T = TypeVar("T", bound=Entity)
17
+
18
+
19
+ class UnitOfWork:
20
+ """
21
+ Unit of Work for managing repository access.
22
+
23
+ Provides a clean interface for accessing repositories through
24
+ attribute access (e.g., uow.users, uow.orders).
25
+
26
+ The UnitOfWork is request-scoped - one instance per HTTP request.
27
+ Repositories are created lazily and cached for the duration of the request.
28
+
29
+ Usage:
30
+ # In a route handler
31
+ @app.get("/users/{user_id}")
32
+ async def get_user(user_id: str, ctx: RequestContext = Depends()):
33
+ # Access repository through UnitOfWork
34
+ user = await ctx.uow.users.get(user_id)
35
+ return user
36
+
37
+ # With explicit repository method
38
+ @app.get("/orders")
39
+ async def list_orders(ctx: RequestContext = Depends()):
40
+ repo = ctx.uow.repository("orders", Order)
41
+ return await repo.find({"status": "pending"})
42
+
43
+ Repository Naming Convention:
44
+ - Attribute access uses collection name: uow.users -> users collection
45
+ - The entity class defaults to Entity if not registered
46
+ - Register entity classes for type-safe repositories
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ db: Any, # ScopedMongoWrapper - avoid import cycle
52
+ entity_registry: Optional[Dict[str, Type[Entity]]] = None,
53
+ ):
54
+ """
55
+ Initialize the Unit of Work.
56
+
57
+ Args:
58
+ db: ScopedMongoWrapper for database access
59
+ entity_registry: Optional mapping of collection names to entity classes
60
+ """
61
+ self._db = db
62
+ self._repositories: Dict[str, Repository] = {}
63
+ self._entity_registry: Dict[str, Type[Entity]] = entity_registry or {}
64
+
65
+ def register_entity(self, collection_name: str, entity_class: Type[Entity]) -> None:
66
+ """
67
+ Register an entity class for a collection.
68
+
69
+ This enables type-safe repository access.
70
+
71
+ Args:
72
+ collection_name: Name of the collection
73
+ entity_class: Entity subclass for this collection
74
+ """
75
+ self._entity_registry[collection_name] = entity_class
76
+
77
+ def repository(
78
+ self,
79
+ name: str,
80
+ entity_class: Optional[Type[T]] = None,
81
+ ) -> Repository[T]:
82
+ """
83
+ Get or create a repository for a collection.
84
+
85
+ Args:
86
+ name: Collection name
87
+ entity_class: Optional entity class override
88
+
89
+ Returns:
90
+ Repository instance for the collection
91
+ """
92
+ if name in self._repositories:
93
+ return self._repositories[name]
94
+
95
+ # Determine entity class
96
+ if entity_class is None:
97
+ entity_class = self._entity_registry.get(name, Entity)
98
+
99
+ # Get collection from db wrapper
100
+ collection = getattr(self._db, name)
101
+
102
+ # Create repository
103
+ repo = MongoRepository(collection, entity_class)
104
+ self._repositories[name] = repo
105
+
106
+ logger.debug(f"Created repository for '{name}' with entity {entity_class.__name__}")
107
+ return repo
108
+
109
+ def __getattr__(self, name: str) -> Repository:
110
+ """
111
+ Access repositories via attribute syntax.
112
+
113
+ Example:
114
+ uow.users # Returns Repository for 'users' collection
115
+ uow.orders # Returns Repository for 'orders' collection
116
+ """
117
+ # Prevent recursion on private attributes
118
+ if name.startswith("_"):
119
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
120
+
121
+ return self.repository(name)
122
+
123
+ @property
124
+ def db(self) -> Any:
125
+ """
126
+ Direct access to the underlying ScopedMongoWrapper.
127
+
128
+ Use this for operations not covered by the Repository interface,
129
+ like complex aggregations or raw queries.
130
+
131
+ Example:
132
+ # Complex aggregation
133
+ pipeline = [{"$match": {...}}, {"$group": {...}}]
134
+ results = await ctx.uow.db.users.aggregate(pipeline).to_list(None)
135
+ """
136
+ return self._db
137
+
138
+ def dispose(self) -> None:
139
+ """
140
+ Dispose of the UnitOfWork and clear cached repositories.
141
+
142
+ Called automatically at the end of a request scope.
143
+ """
144
+ self._repositories.clear()
145
+ logger.debug("UnitOfWork disposed")
146
+
147
+
148
+ class TypedUnitOfWork(UnitOfWork, Generic[T]):
149
+ """
150
+ Generic typed UnitOfWork for better IDE support.
151
+
152
+ This is a convenience class that provides type hints for specific
153
+ repository types.
154
+
155
+ Usage:
156
+ class MyUnitOfWork(TypedUnitOfWork):
157
+ @property
158
+ def users(self) -> Repository[User]:
159
+ return self.repository("users", User)
160
+
161
+ @property
162
+ def orders(self) -> Repository[Order]:
163
+ return self.repository("orders", Order)
164
+ """
165
+
166
+ pass
@@ -286,7 +286,7 @@ async def safe_message_handler(websocket, message):
286
286
  "type": "action_result",
287
287
  "result": result
288
288
  })
289
- except Exception as e:
289
+ except (ValueError, TypeError, KeyError) as e:
290
290
  logger.error(f"Error handling message: {e}")
291
291
  # Send error to client
292
292
  manager = await get_websocket_manager("my_app")
@@ -23,9 +23,7 @@ def _check_websockets_available():
23
23
  try:
24
24
  from fastapi import WebSocket
25
25
 
26
- _websockets_module = __import__(
27
- ".websockets", fromlist=[""], package=__name__
28
- )
26
+ _websockets_module = __import__(".websockets", fromlist=[""], package=__name__)
29
27
  _websockets_available = True
30
28
  except (ImportError, AttributeError):
31
29
  _websockets_available = False
@@ -72,9 +72,7 @@ class WebSocketConnectionManager:
72
72
  app_slug: App slug for scoping connections (ensures isolation)
73
73
  """
74
74
  self.app_slug = app_slug
75
- self.active_connections: List[WebSocketConnection] = (
76
- []
77
- ) # List of connection metadata
75
+ self.active_connections: List[WebSocketConnection] = [] # List of connection metadata
78
76
  self._lock = asyncio.Lock()
79
77
  logger.debug(f"Initialized WebSocket manager for app: {app_slug}")
80
78
 
@@ -97,10 +95,7 @@ class WebSocketConnectionManager:
97
95
  """
98
96
  # Note: websocket should already be accepted by the endpoint handler
99
97
  # This is just for tracking - don't accept again
100
- if (
101
- hasattr(websocket, "client_state")
102
- and websocket.client_state.name != "CONNECTED"
103
- ):
98
+ if hasattr(websocket, "client_state") and websocket.client_state.name != "CONNECTED":
104
99
  await websocket.accept()
105
100
  connection = WebSocketConnection(
106
101
  websocket=websocket,
@@ -130,9 +125,7 @@ class WebSocketConnectionManager:
130
125
  async def _disconnect():
131
126
  async with self._lock:
132
127
  self.active_connections = [
133
- conn
134
- for conn in self.active_connections
135
- if conn.websocket is not websocket
128
+ conn for conn in self.active_connections if conn.websocket is not websocket
136
129
  ]
137
130
  logger.info(
138
131
  f"WebSocket disconnected for app '{self.app_slug}'. "
@@ -141,9 +134,7 @@ class WebSocketConnectionManager:
141
134
 
142
135
  asyncio.create_task(_disconnect())
143
136
 
144
- async def broadcast(
145
- self, message: Dict[str, Any], filter_by_user: Optional[str] = None
146
- ) -> int:
137
+ async def broadcast(self, message: Dict[str, Any], filter_by_user: Optional[str] = None) -> int:
147
138
  """
148
139
  Broadcast a message to all connected clients for this app.
149
140
 
@@ -276,9 +267,7 @@ _manager_lock = asyncio.Lock()
276
267
 
277
268
  # Global registry of message handlers per app (for listening to client messages)
278
269
  # Note: Registration happens synchronously during app startup, so no lock needed
279
- _message_handlers: Dict[
280
- str, Dict[str, Callable[[Any, Dict[str, Any]], Awaitable[None]]]
281
- ] = {}
270
+ _message_handlers: Dict[str, Dict[str, Callable[[Any, Dict[str, Any]], Awaitable[None]]]] = {}
282
271
 
283
272
 
284
273
  async def get_websocket_manager(app_slug: str) -> WebSocketConnectionManager:
@@ -367,22 +356,16 @@ async def authenticate_websocket(
367
356
  else:
368
357
  logger.info(f"WebSocket query_params is empty for app '{app_slug}'")
369
358
  else:
370
- logger.warning(
371
- f"WebSocket has no query_params attribute for app '{app_slug}'"
372
- )
359
+ logger.warning(f"WebSocket has no query_params attribute for app '{app_slug}'")
373
360
 
374
361
  # If no token in query, try to get from cookies (if available)
375
362
  # Check both ws_token (non-httponly, for JS access) and token (httponly)
376
363
  if not token:
377
364
  if hasattr(websocket, "cookies"):
378
- cookie_token = websocket.cookies.get(
379
- "ws_token"
380
- ) or websocket.cookies.get("token")
365
+ cookie_token = websocket.cookies.get("ws_token") or websocket.cookies.get("token")
381
366
  if cookie_token:
382
367
  token = cookie_token
383
- logger.debug(
384
- f"WebSocket token found in cookies for app '{app_slug}'"
385
- )
368
+ logger.debug(f"WebSocket token found in cookies for app '{app_slug}'")
386
369
  else:
387
370
  logger.debug(f"WebSocket has no cookies attribute for app '{app_slug}'")
388
371
 
@@ -404,6 +387,8 @@ async def authenticate_websocket(
404
387
  return None, None # Signal auth failure
405
388
  return None, None
406
389
 
390
+ import jwt
391
+
407
392
  from ..auth.dependencies import SECRET_KEY
408
393
  from ..auth.jwt import decode_jwt_token
409
394
 
@@ -412,22 +397,16 @@ async def authenticate_websocket(
412
397
  user_id = payload.get("sub") or payload.get("user_id")
413
398
  user_email = payload.get("email")
414
399
 
415
- logger.info(
416
- f"WebSocket authenticated successfully for app '{app_slug}': {user_email}"
417
- )
400
+ logger.info(f"WebSocket authenticated successfully for app '{app_slug}': {user_email}")
418
401
  return user_id, user_email
419
- except Exception as decode_error:
420
- logger.error(
421
- f"JWT decode error for app '{app_slug}': {decode_error}", exc_info=True
422
- )
402
+ except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as decode_error:
403
+ logger.error(f"JWT decode error for app '{app_slug}': {decode_error}", exc_info=True)
423
404
  raise
424
405
 
425
406
  except WebSocketDisconnect:
426
407
  raise
427
408
  except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
428
- logger.error(
429
- f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True
430
- )
409
+ logger.error(f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True)
431
410
  if require_auth:
432
411
  # Don't close before accepting - return error info instead
433
412
  return None, None # Signal auth failure
@@ -469,9 +448,7 @@ def register_message_handler(
469
448
  if app_slug not in _message_handlers:
470
449
  _message_handlers[app_slug] = {}
471
450
  _message_handlers[app_slug][endpoint_name] = handler
472
- logger.info(
473
- f"Registered message handler for app '{app_slug}', endpoint '{endpoint_name}'"
474
- )
451
+ logger.info(f"Registered message handler for app '{app_slug}', endpoint '{endpoint_name}'")
475
452
 
476
453
 
477
454
  def get_message_handler(
@@ -497,7 +474,7 @@ async def _accept_websocket_connection(websocket: Any, app_slug: str) -> None:
497
474
  await websocket.accept()
498
475
  print(f"✅ [WEBSOCKET ACCEPTED] App: '{app_slug}'")
499
476
  logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
500
- except Exception as accept_error:
477
+ except (RuntimeError, ConnectionError, OSError) as accept_error:
501
478
  print(f"❌ [WEBSOCKET ACCEPT FAILED] App: '{app_slug}', Error: {accept_error}")
502
479
  logger.error(
503
480
  f"❌ Failed to accept WebSocket for app '{app_slug}': {accept_error}",
@@ -511,9 +488,7 @@ async def _authenticate_websocket_connection(
511
488
  ) -> tuple:
512
489
  """Authenticate WebSocket connection and return (user_id, user_email)."""
513
490
  try:
514
- user_id, user_email = await authenticate_websocket(
515
- websocket, app_slug, require_auth
516
- )
491
+ user_id, user_email = await authenticate_websocket(websocket, app_slug, require_auth)
517
492
 
518
493
  if require_auth and not user_id:
519
494
  logger.warning(
@@ -522,9 +497,7 @@ async def _authenticate_websocket_connection(
522
497
  try:
523
498
  await websocket.close(code=1008, reason="Authentication required")
524
499
  except (WebSocketDisconnect, RuntimeError, OSError) as e:
525
- logger.debug(
526
- f"WebSocket already closed during auth failure cleanup: {e}"
527
- )
500
+ logger.debug(f"WebSocket already closed during auth failure cleanup: {e}")
528
501
  raise WebSocketDisconnect(code=1008)
529
502
 
530
503
  return user_id, user_email
@@ -547,12 +520,10 @@ async def _authenticate_websocket_connection(
547
520
  exc_info=True,
548
521
  )
549
522
  try:
550
- await websocket.close(
551
- code=1011, reason="Internal server error during authentication"
552
- )
553
- except (WebSocketDisconnect, RuntimeError, OSError) as e:
554
- logger.debug(f"WebSocket already closed during auth error cleanup: {e}")
555
- raise WebSocketDisconnect(code=1011)
523
+ await websocket.close(code=1011, reason="Internal server error during authentication")
524
+ except (WebSocketDisconnect, RuntimeError, OSError) as close_error:
525
+ logger.debug(f"WebSocket already closed during auth error cleanup: {close_error}")
526
+ raise WebSocketDisconnect(code=1011) from None
556
527
 
557
528
 
558
529
  async def _handle_websocket_message(
@@ -656,9 +627,7 @@ def create_websocket_endpoint(
656
627
  file=sys.stderr,
657
628
  flush=True,
658
629
  )
659
- print(
660
- f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}", flush=True
661
- )
630
+ print(f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}", flush=True)
662
631
  logger.info(f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}")
663
632
  connection = None
664
633
  try:
@@ -693,9 +662,7 @@ def create_websocket_endpoint(
693
662
  )
694
663
 
695
664
  # Connect with metadata (websocket already accepted)
696
- connection = await manager.connect(
697
- websocket, user_id=user_id, user_email=user_email
698
- )
665
+ connection = await manager.connect(websocket, user_id=user_id, user_email=user_email)
699
666
 
700
667
  # Send initial connection confirmation
701
668
  await manager.send_to_connection(
@@ -765,9 +732,7 @@ def create_websocket_endpoint(
765
732
  TypeError,
766
733
  AttributeError,
767
734
  ) as e:
768
- logger.error(
769
- f"WebSocket connection error for app '{app_slug}': {e}", exc_info=True
770
- )
735
+ logger.error(f"WebSocket connection error for app '{app_slug}': {e}", exc_info=True)
771
736
  finally:
772
737
  if connection:
773
738
  manager.disconnect(websocket)