mdb-engine 0.1.6__py3-none-any.whl → 0.1.7__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 +38 -6
- mdb_engine/auth/README.md +534 -11
- mdb_engine/auth/__init__.py +129 -28
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/casbin_factory.py +10 -14
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +37 -45
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +30 -73
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +16 -36
- mdb_engine/auth/provider.py +17 -38
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- 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 +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +758 -95
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +424 -135
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +112 -16
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/embeddings/__init__.py +17 -9
- mdb_engine/embeddings/dependencies.py +1 -3
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +25 -60
- mdb_engine-0.1.7.dist-info/METADATA +285 -0
- mdb_engine-0.1.7.dist-info/RECORD +85 -0
- 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.1.7.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/top_level.txt +0 -0
mdb_engine/routing/websockets.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
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)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdb-engine
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: MongoDB Engine
|
|
5
|
+
Home-page: https://github.com/ranfysvalle02/mdb-engine
|
|
6
|
+
Author: Your Name
|
|
7
|
+
Author-email: Your Name <your.email@example.com>
|
|
8
|
+
License: AGPL-3.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/ranfysvalle02/mdb-engine
|
|
10
|
+
Project-URL: Documentation, https://github.com/ranfysvalle02/mdb-engine#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/ranfysvalle02/mdb-engine
|
|
12
|
+
Project-URL: Issues, https://github.com/ranfysvalle02/mdb-engine/issues
|
|
13
|
+
Keywords: mongodb,runtime,engine,database,scoping
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: motor>=3.0.0
|
|
28
|
+
Requires-Dist: pymongo>=4.0.0
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0
|
|
30
|
+
Requires-Dist: pydantic>=2.0.0
|
|
31
|
+
Requires-Dist: pyjwt>=2.8.0
|
|
32
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
33
|
+
Requires-Dist: bcrypt>=4.0.0
|
|
34
|
+
Requires-Dist: cryptography>=41.0.0
|
|
35
|
+
Requires-Dist: mem0ai>=1.0.0
|
|
36
|
+
Requires-Dist: semantic-text-splitter>=0.9.0
|
|
37
|
+
Requires-Dist: numpy<2.0.0,>=1.0.0
|
|
38
|
+
Requires-Dist: openai>=1.0.0
|
|
39
|
+
Requires-Dist: azure-identity>=1.15.0
|
|
40
|
+
Requires-Dist: click>=8.0.0
|
|
41
|
+
Provides-Extra: casbin
|
|
42
|
+
Requires-Dist: casbin>=1.0.0; extra == "casbin"
|
|
43
|
+
Requires-Dist: casbin-motor-adapter>=0.1.0; extra == "casbin"
|
|
44
|
+
Provides-Extra: oso
|
|
45
|
+
Requires-Dist: oso-cloud>=0.1.0; extra == "oso"
|
|
46
|
+
Provides-Extra: llm
|
|
47
|
+
Provides-Extra: test
|
|
48
|
+
Requires-Dist: pytest>=7.4.0; extra == "test"
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
50
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
|
|
51
|
+
Requires-Dist: pytest-mock>=3.11.0; extra == "test"
|
|
52
|
+
Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
|
|
53
|
+
Requires-Dist: testcontainers>=3.7.0; extra == "test"
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
56
|
+
Requires-Dist: semgrep>=1.50.0; extra == "dev"
|
|
57
|
+
Provides-Extra: all
|
|
58
|
+
Requires-Dist: casbin>=1.0.0; extra == "all"
|
|
59
|
+
Requires-Dist: casbin-motor-adapter>=0.1.0; extra == "all"
|
|
60
|
+
Requires-Dist: oso-cloud>=0.1.0; extra == "all"
|
|
61
|
+
Dynamic: author
|
|
62
|
+
Dynamic: home-page
|
|
63
|
+
Dynamic: license-file
|
|
64
|
+
Dynamic: requires-python
|
|
65
|
+
|
|
66
|
+
# mdb-engine
|
|
67
|
+
|
|
68
|
+
**The MongoDB Engine for Python Apps** — Auto-sandboxing, index management, and auth in one package.
|
|
69
|
+
|
|
70
|
+
[](https://pypi.org/project/mdb-engine/)
|
|
71
|
+
[](https://www.python.org/downloads/)
|
|
72
|
+
[](https://opensource.org/licenses/AGPL-3.0)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install mdb-engine
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 30-Second Quick Start
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
from mdb_engine import MongoDBEngine
|
|
89
|
+
|
|
90
|
+
# 1. Initialize the engine
|
|
91
|
+
engine = MongoDBEngine(
|
|
92
|
+
mongo_uri="mongodb://localhost:27017",
|
|
93
|
+
db_name="my_database"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 2. Create a FastAPI app with automatic lifecycle management
|
|
97
|
+
app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
|
|
98
|
+
|
|
99
|
+
# 3. Use the scoped database - all queries automatically isolated
|
|
100
|
+
@app.post("/tasks")
|
|
101
|
+
async def create_task(task: dict):
|
|
102
|
+
db = engine.get_scoped_db("my_app")
|
|
103
|
+
result = await db.tasks.insert_one(task)
|
|
104
|
+
return {"id": str(result.inserted_id)}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
That's it. Your data is automatically sandboxed, indexes are created, and cleanup is handled.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Basic Examples
|
|
112
|
+
|
|
113
|
+
### 1. Index Management
|
|
114
|
+
|
|
115
|
+
Define indexes in your `manifest.json` — they're auto-created on startup:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"schema_version": "2.0",
|
|
120
|
+
"slug": "my_app",
|
|
121
|
+
"name": "My App",
|
|
122
|
+
"status": "active",
|
|
123
|
+
"managed_indexes": {
|
|
124
|
+
"tasks": [
|
|
125
|
+
{
|
|
126
|
+
"type": "regular",
|
|
127
|
+
"keys": {"status": 1, "created_at": -1},
|
|
128
|
+
"name": "status_sort"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "regular",
|
|
132
|
+
"keys": {"priority": -1},
|
|
133
|
+
"name": "priority_idx"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"users": [
|
|
137
|
+
{
|
|
138
|
+
"type": "regular",
|
|
139
|
+
"keys": {"email": 1},
|
|
140
|
+
"name": "email_unique",
|
|
141
|
+
"unique": true
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Supported index types: `regular`, `text`, `vector`, `ttl`, `compound`.
|
|
149
|
+
|
|
150
|
+
### 2. CRUD Operations (Auto-Scoped)
|
|
151
|
+
|
|
152
|
+
All database operations are automatically scoped to your app:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
db = engine.get_scoped_db("my_app")
|
|
156
|
+
|
|
157
|
+
# Create
|
|
158
|
+
await db.tasks.insert_one({"title": "Build feature", "status": "pending"})
|
|
159
|
+
|
|
160
|
+
# Read
|
|
161
|
+
tasks = await db.tasks.find({"status": "pending"}).to_list(length=10)
|
|
162
|
+
|
|
163
|
+
# Update
|
|
164
|
+
await db.tasks.update_one({"_id": task_id}, {"$set": {"status": "done"}})
|
|
165
|
+
|
|
166
|
+
# Delete
|
|
167
|
+
await db.tasks.delete_one({"_id": task_id})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**What happens under the hood:**
|
|
171
|
+
```python
|
|
172
|
+
# You write:
|
|
173
|
+
await db.tasks.find({}).to_list(length=10)
|
|
174
|
+
|
|
175
|
+
# Engine executes:
|
|
176
|
+
# Collection: my_app_tasks
|
|
177
|
+
# Query: {"app_id": "my_app"}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3. Health Checks
|
|
181
|
+
|
|
182
|
+
Built-in observability:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@app.get("/health")
|
|
186
|
+
async def health():
|
|
187
|
+
status = await engine.get_health_status()
|
|
188
|
+
return {"status": status.get("status", "unknown")}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Why mdb-engine?
|
|
194
|
+
|
|
195
|
+
- **Zero boilerplate** — No more connection setup, index creation scripts, or auth handlers
|
|
196
|
+
- **Data isolation** — Multi-tenant ready with automatic app sandboxing
|
|
197
|
+
- **Manifest-driven** — Define your app's "DNA" in JSON, not scattered code
|
|
198
|
+
- **No lock-in** — Standard Motor/PyMongo underneath; export anytime with `mongodump --query='{"app_id":"my_app"}'`
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Advanced Features
|
|
203
|
+
|
|
204
|
+
| Feature | Description | Learn More |
|
|
205
|
+
|---------|-------------|------------|
|
|
206
|
+
| **Authentication** | JWT + Casbin/OSO RBAC | [Auth Guide](https://github.com/ranfysvalle02/mdb-engine/blob/main/docs/AUTHZ.md) |
|
|
207
|
+
| **Vector Search** | Atlas Vector Search + embeddings | [RAG Example](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/interactive_rag) |
|
|
208
|
+
| **Memory Service** | Persistent AI memory with Mem0 | [Chat Example](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/chit_chat) |
|
|
209
|
+
| **WebSockets** | Real-time updates from manifest | [Docs](https://github.com/ranfysvalle02/mdb-engine/blob/main/docs/ARCHITECTURE.md) |
|
|
210
|
+
| **Multi-App** | Secure cross-app data access | [Multi-App Example](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/advanced/multi_app) |
|
|
211
|
+
| **SSO** | Shared auth across apps | [Shared Auth Example](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/advanced/multi_app_shared) |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Full Examples
|
|
216
|
+
|
|
217
|
+
Clone and run:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git clone https://github.com/ranfysvalle02/mdb-engine.git
|
|
221
|
+
cd mdb-engine/examples/basic/chit_chat
|
|
222
|
+
docker-compose up --build
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Basic Examples
|
|
226
|
+
|
|
227
|
+
| Example | Description |
|
|
228
|
+
|---------|-------------|
|
|
229
|
+
| [chit_chat](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/chit_chat) | AI chat with persistent memory |
|
|
230
|
+
| [interactive_rag](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/interactive_rag) | RAG with vector search |
|
|
231
|
+
| [oso_hello_world](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/oso_hello_world) | OSO Cloud authorization |
|
|
232
|
+
| [parallax](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/parallax) | Dynamic schema generation |
|
|
233
|
+
| [vector_hacking](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/basic/vector_hacking) | Vector embeddings & attacks |
|
|
234
|
+
|
|
235
|
+
### Advanced Examples
|
|
236
|
+
|
|
237
|
+
| Example | Description |
|
|
238
|
+
|---------|-------------|
|
|
239
|
+
| [simple_app](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/advanced/simple_app) | Task management with `create_app()` pattern |
|
|
240
|
+
| [multi_app](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/advanced/multi_app) | Multi-tenant with cross-app access |
|
|
241
|
+
| [multi_app_shared](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples/advanced/multi_app_shared) | SSO with shared user pool |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Manual Setup (Alternative)
|
|
246
|
+
|
|
247
|
+
If you need more control over the FastAPI lifecycle:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from pathlib import Path
|
|
251
|
+
from fastapi import FastAPI
|
|
252
|
+
from mdb_engine import MongoDBEngine
|
|
253
|
+
|
|
254
|
+
app = FastAPI()
|
|
255
|
+
engine = MongoDBEngine(mongo_uri="mongodb://localhost:27017", db_name="my_database")
|
|
256
|
+
|
|
257
|
+
@app.on_event("startup")
|
|
258
|
+
async def startup():
|
|
259
|
+
await engine.initialize()
|
|
260
|
+
manifest = await engine.load_manifest(Path("manifest.json"))
|
|
261
|
+
await engine.register_app(manifest, create_indexes=True)
|
|
262
|
+
|
|
263
|
+
@app.on_event("shutdown")
|
|
264
|
+
async def shutdown():
|
|
265
|
+
await engine.shutdown()
|
|
266
|
+
|
|
267
|
+
@app.get("/items")
|
|
268
|
+
async def get_items():
|
|
269
|
+
db = engine.get_scoped_db("my_app")
|
|
270
|
+
return await db.items.find({}).to_list(length=10)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Links
|
|
276
|
+
|
|
277
|
+
- [GitHub Repository](https://github.com/ranfysvalle02/mdb-engine)
|
|
278
|
+
- [Documentation](https://github.com/ranfysvalle02/mdb-engine/tree/main/docs)
|
|
279
|
+
- [All Examples](https://github.com/ranfysvalle02/mdb-engine/tree/main/examples)
|
|
280
|
+
- [Quick Start Guide](https://github.com/ranfysvalle02/mdb-engine/blob/main/docs/QUICK_START.md)
|
|
281
|
+
- [Contributing](https://github.com/ranfysvalle02/mdb-engine/blob/main/CONTRIBUTING.md)
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
**Stop building scaffolding. Start building features.**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
|
|
2
|
+
mdb_engine/__init__.py,sha256=d5UN7QnkySDKriO4epSGNS2bBbgl_Bw18qu6Sd4I7xs,1629
|
|
3
|
+
mdb_engine/config.py,sha256=EsB0PHrRCCt24db6Ly88s6PQ3sm1esSLMa2G4AHpt5k,7315
|
|
4
|
+
mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
|
|
5
|
+
mdb_engine/exceptions.py,sha256=N8WYOgh1fOH0yJeR9I3FTXqpACIL_EQX3zcfUgrFvPI,8521
|
|
6
|
+
mdb_engine/auth/README.md,sha256=pOobYo31ENhGA3d_nTUXVK795w2wvvQ_ub10x9wK1pU,33838
|
|
7
|
+
mdb_engine/auth/__init__.py,sha256=0Gc0uFDAcj_BMeACaD0oICAn8YwVcpTf20DJbTzDxr8,5656
|
|
8
|
+
mdb_engine/auth/audit.py,sha256=LtdG9GdwYTSMI30zMygB0K8w4l179GaGvkNbxWgoQ2I,17461
|
|
9
|
+
mdb_engine/auth/casbin_factory.py,sha256=CjGU7cj_I2OVjsendxpK1ungvHIMHzGJptMDw7LzEtE,5898
|
|
10
|
+
mdb_engine/auth/casbin_models.py,sha256=7XtFmRBhhjw1nKprnluvjyJoTj5fzdPeQwVvo6fI-r0,955
|
|
11
|
+
mdb_engine/auth/config_defaults.py,sha256=7JYX8CFI7OP7GHFIDHTf3Jns09LJcQ7f0_GL9yBnyuA,2018
|
|
12
|
+
mdb_engine/auth/config_helpers.py,sha256=DpQ8QZAO2FVVKFmhTJIa4FYPS8Iy8O7vcVh_tg0INLg,6309
|
|
13
|
+
mdb_engine/auth/cookie_utils.py,sha256=F88p6_aMG0aSmGXCn0KYjweSMPHqfce1lrKHU4P5yQk,4913
|
|
14
|
+
mdb_engine/auth/csrf.py,sha256=Rx9qYt8owSJJwwj7Z_w4nzkaLbaqWoVpNv13qnqosIA,12458
|
|
15
|
+
mdb_engine/auth/decorators.py,sha256=47uyOK_6WmK18J6ELRKwwgaIkcp6kHOILXuARj82Ijw,12288
|
|
16
|
+
mdb_engine/auth/dependencies.py,sha256=hAmFhtOFFJWRpCFQyPuALJWcgSMZn_Os16S7Q5N9waQ,27038
|
|
17
|
+
mdb_engine/auth/helpers.py,sha256=BCrid985cYh-3h5ZMUV9TES0q40uJXio4oYKQZta7KA,1970
|
|
18
|
+
mdb_engine/auth/integration.py,sha256=MHb4d8ar1eywXJlo4I1dlvUJ6SP7KO_ql3kbVTs8FAU,21756
|
|
19
|
+
mdb_engine/auth/jwt.py,sha256=aRXLfgFjsebIARmPqgfcA5zSgSN44HD7BgSyOEFaQCM,7244
|
|
20
|
+
mdb_engine/auth/middleware.py,sha256=uMx1dOO1NaR4K2nDet3k_ASR7rc7ZbKD0HlEkVYGJvU,10869
|
|
21
|
+
mdb_engine/auth/oso_factory.py,sha256=sJfW9TlrKp6QF_f5kq84axWQJyPsNelDDKuYLmVsduU,11257
|
|
22
|
+
mdb_engine/auth/provider.py,sha256=SNpkws-QO56hOo8CZ6LMenLjLQYO0NK_31zMhYekXx0,21926
|
|
23
|
+
mdb_engine/auth/rate_limiter.py,sha256=kYuYIDlgkmYB2jx_UUmM9VrndUq2iTkdP7DZV-Jfu8o,15825
|
|
24
|
+
mdb_engine/auth/restrictions.py,sha256=TXa2lO3lZQeMtokhyVLP19-XbWwq2R8JMTw2KfaiDMg,8852
|
|
25
|
+
mdb_engine/auth/session_manager.py,sha256=XjYeE4i1998tBIefhp8VH3Oe7reyA_CC_qjw6zZ5EVY,15459
|
|
26
|
+
mdb_engine/auth/shared_middleware.py,sha256=nrW-JN3P6SQ-WjJcXU9scX8auuS3-LdKEQX9Iy-IjJw,22343
|
|
27
|
+
mdb_engine/auth/shared_users.py,sha256=7f1Wl0u-egcnaMPj1a2XzclMnulrYXpzOcFQM6Mvvus,26828
|
|
28
|
+
mdb_engine/auth/token_lifecycle.py,sha256=NiYDEUyooZRNAaT79yPHAyAfTK3MGOu8eNJtClR-PL0,6753
|
|
29
|
+
mdb_engine/auth/token_store.py,sha256=Mpvln3sPEE0l5zQmFtWxpVUKYjelWv8FJTLhOOn55MY,9161
|
|
30
|
+
mdb_engine/auth/users.py,sha256=gMEHC936aTkjtnGN5E3YzA1k5IThDcLFhzs0-02ujxI,50037
|
|
31
|
+
mdb_engine/auth/utils.py,sha256=g5ILBsrSxvBbhHKsJc9oVjlnmodrM3QJjQQKbnEy4Mk,26126
|
|
32
|
+
mdb_engine/cli/__init__.py,sha256=PANRi4THmL34d1mawlqxIrnuItXMdqoMTq5Z1zHd7rM,301
|
|
33
|
+
mdb_engine/cli/main.py,sha256=Y5ELFhvsr8zxFWv4WScOGNHiLUTdSXAJeUFLpRXCelg,811
|
|
34
|
+
mdb_engine/cli/utils.py,sha256=0bpxHB5PmHapnCRjlN4T2YRp5kzJsp87Uh_65MsCFJY,2766
|
|
35
|
+
mdb_engine/cli/commands/__init__.py,sha256=ZSzMhKdV9ILD5EbOSxDV9nURHo1e4bQ0c8AWpqsTEqM,115
|
|
36
|
+
mdb_engine/cli/commands/generate.py,sha256=2XDnyROYm5OOcEFczGeS6P0AtFgM74XigQbxbMTPvU4,17329
|
|
37
|
+
mdb_engine/cli/commands/migrate.py,sha256=VqNiobSq-ykUhv7NyHJZ-wZ60a8X0B6OqXz8OACsCr0,2077
|
|
38
|
+
mdb_engine/cli/commands/show.py,sha256=-PmyMfzzG1HP-PsD9n8LECyxwvlXypNS3g_VDwFg4oY,1914
|
|
39
|
+
mdb_engine/cli/commands/validate.py,sha256=tD8GkhuW8LKsRMUXvBs-wvVWQunw1XFlLvbpE_SQ9Zg,1688
|
|
40
|
+
mdb_engine/core/README.md,sha256=cl19Sw4Hj-BPBsh_FIhPpop_6S88V-KaHC6rnUCx8xw,17039
|
|
41
|
+
mdb_engine/core/__init__.py,sha256=xhnXyXWZ0a2Qr49QrYvLE5QuqSalcpo05hHHRVtDz_M,1952
|
|
42
|
+
mdb_engine/core/app_registration.py,sha256=zRK2JTeT0rDW4XdOgFAV-j9Og7xijXdov4u1VVp7p1Y,13386
|
|
43
|
+
mdb_engine/core/app_secrets.py,sha256=ipLsC_QdnIFhrCLNQ3nXvBAfndSor4WNRJyqPc0-J8Q,10555
|
|
44
|
+
mdb_engine/core/connection.py,sha256=9WRccQ2Zz8K6PS6yC2XXxhZkppALvAWZJhfLmsJBnbI,8383
|
|
45
|
+
mdb_engine/core/encryption.py,sha256=jblhUuu_L_lht0qlbn5vqDsdj9rN80uL9DCFDCD-zxw,7540
|
|
46
|
+
mdb_engine/core/engine.py,sha256=hW86qxrHvaQdCJgyI93W0GVVxVB7ZUuY66QAiWaGXRA,55486
|
|
47
|
+
mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
|
|
48
|
+
mdb_engine/core/manifest.py,sha256=K95aiGvZ2YQ3pUIGJyMeJTvveg9HgGdJUpd6fO7AxmI,132305
|
|
49
|
+
mdb_engine/core/ray_integration.py,sha256=Qe3jAYQhOKeBBlWFAcqzciDWN2zctJh_6uMTIai1tS0,13617
|
|
50
|
+
mdb_engine/core/seeding.py,sha256=I5g43xdoEdC4rNwc_Ih0L6QOZguPcpAWrS6HvJ2a3fE,6433
|
|
51
|
+
mdb_engine/core/service_initialization.py,sha256=gJNIB0As4vg6UXRgt1EDo5p3zzlHfF8NZqjCAlB75mA,12997
|
|
52
|
+
mdb_engine/core/types.py,sha256=v_lSnBTyl2JHX39cPvPsnOxXhu8xaGC92UektvYPOvg,11239
|
|
53
|
+
mdb_engine/database/README.md,sha256=cWPUce2PmrnIu4JQPF_RzsQ0Ju4pHIjhNhiFT0OdPKs,18481
|
|
54
|
+
mdb_engine/database/__init__.py,sha256=rrc3eZFli3K2zrvVdDbMBi8YkmoHYzP6JNT0AUBE5VU,981
|
|
55
|
+
mdb_engine/database/abstraction.py,sha256=FLVhFuS4OTZIRQyDkUBZdIlhqRndqNA89EO0cJvNsN0,23462
|
|
56
|
+
mdb_engine/database/connection.py,sha256=PDmwkjb4wYhLTUjlcYHscorces69rIuSxt5wdYixGQw,13777
|
|
57
|
+
mdb_engine/database/query_validator.py,sha256=AIWkQ-c6HAqJ-ixn-hq_0yfC2vdf1-jbEpzYZNmfEqs,13275
|
|
58
|
+
mdb_engine/database/resource_limiter.py,sha256=n54-I6bjEk5jxNGMXyb6_fArDCiyJ69_h9goFW5GzUs,6936
|
|
59
|
+
mdb_engine/database/scoped_wrapper.py,sha256=S_QIqGVyCteKAYlSUHNIqqc0S80jWhzHV-Lfa-ezNG4,94794
|
|
60
|
+
mdb_engine/embeddings/README.md,sha256=lehHJm7gADRze1Em59Da7DWFuVqDhMKmLz8UaTF12Rc,4997
|
|
61
|
+
mdb_engine/embeddings/__init__.py,sha256=UReBuU_0IOFg2kCpMjmZFV9b88PepL6oNok9Osd8t-4,2024
|
|
62
|
+
mdb_engine/embeddings/dependencies.py,sha256=aQXZnIVLxWU1qnKh-fih0iCcdX3cPd0AdsNEajBLp4w,5915
|
|
63
|
+
mdb_engine/embeddings/service.py,sha256=v5Ch4VMADDUw-tqNs8XbaG9JJktvO7cqniNJ8Mes56k,25991
|
|
64
|
+
mdb_engine/indexes/README.md,sha256=r7duq-1vtqHhBk1cwoBMYYh_dfTzxiaQaPE3mLB_3JQ,15341
|
|
65
|
+
mdb_engine/indexes/__init__.py,sha256=9QFJ6qo_yD26dZcKyKjj-hhesFpaomBt-mWTtYTQvqc,613
|
|
66
|
+
mdb_engine/indexes/helpers.py,sha256=v5iYqS3yOekqwxdbgnI1ifYH4oESatgXYJiJO1GvhSU,4259
|
|
67
|
+
mdb_engine/indexes/manager.py,sha256=CkA62LofmRLtfcm1cyzyx89p6Nge8yb3MIshmWAWPC8,32472
|
|
68
|
+
mdb_engine/memory/README.md,sha256=vy2FSXADcqG18YrFBfrMph9a1vYKuLijhs8ziNFdvRk,10631
|
|
69
|
+
mdb_engine/memory/__init__.py,sha256=e4kAYgxd_-WAH8GovTwjEBO9hvASu_kXEupMgksAL-U,1008
|
|
70
|
+
mdb_engine/memory/service.py,sha256=evoQhnslLVc3fEUhikdby_mt5iz9_T2LPeCEQX4Psd0,47553
|
|
71
|
+
mdb_engine/observability/README.md,sha256=CMgQaC1H8ESmCitfbhJifz6-XoXH_FPNE4MvuZ-oFas,13085
|
|
72
|
+
mdb_engine/observability/__init__.py,sha256=jjLsrW6Gy2ayrbfLrgHsDB61NxWWkYLHwv0q-N3fxjA,1213
|
|
73
|
+
mdb_engine/observability/health.py,sha256=kZ9LXcJ3_8tKXRWXNNCz3qsj0PkZ0BSTdzisc8ds2vw,9197
|
|
74
|
+
mdb_engine/observability/logging.py,sha256=yo_KnUtqjPx_KvqNrAxW8ud6HQjI7Lk6T7Lj7IMW1uY,4073
|
|
75
|
+
mdb_engine/observability/metrics.py,sha256=EL9-ZFOaxxIdU8PWIqPRyPZHhP3fc4VyUawRSS2GIY4,10726
|
|
76
|
+
mdb_engine/routing/README.md,sha256=WVvTQXDq0amryrjkCu0wP_piOEwFjLukjmPz2mroWHY,13658
|
|
77
|
+
mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknUM,2438
|
|
78
|
+
mdb_engine/routing/websockets.py,sha256=X6-MG0mAN8ZEgdmZD8edaiDx8DqMCN-hV3coIOdgZNc,29346
|
|
79
|
+
mdb_engine/utils/__init__.py,sha256=_xjHB5p6WLWBql1DyDqf5zdjj2xpfMlK25Y6BH9-oFk,145
|
|
80
|
+
mdb_engine-0.1.7.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
81
|
+
mdb_engine-0.1.7.dist-info/METADATA,sha256=xhr0VvNWMKVjLgu-fkRbXzFWLf7nb8B5u_4J1LXkXfE,9305
|
|
82
|
+
mdb_engine-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
83
|
+
mdb_engine-0.1.7.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
|
|
84
|
+
mdb_engine-0.1.7.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
|
|
85
|
+
mdb_engine-0.1.7.dist-info/RECORD,,
|