mdb-engine 0.2.4__py3-none-any.whl → 0.3.1__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 +1 -1
- mdb_engine/auth/shared_middleware.py +18 -2
- mdb_engine/core/engine.py +417 -5
- mdb_engine/core/manifest.py +93 -0
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/METADATA +127 -23
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/RECORD +10 -10
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/WHEEL +0 -0
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.2.4.dist-info → mdb_engine-0.3.1.dist-info}/top_level.txt +0 -0
mdb_engine/__init__.py
CHANGED
|
@@ -81,7 +81,7 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
|
|
|
81
81
|
# Utilities
|
|
82
82
|
from .utils import clean_mongo_doc, clean_mongo_docs
|
|
83
83
|
|
|
84
|
-
__version__ = "0.
|
|
84
|
+
__version__ = "0.3.1" # Patch version bump: Fix public route matching for mounted apps
|
|
85
85
|
|
|
86
86
|
__all__ = [
|
|
87
87
|
# Core Engine
|
|
@@ -82,6 +82,22 @@ def _compute_fingerprint(request: Request) -> str:
|
|
|
82
82
|
return hashlib.sha256(fingerprint_string.encode()).hexdigest()
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _get_request_path(request: Request) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Get the request path relative to the mount point.
|
|
88
|
+
|
|
89
|
+
For mounted apps (via create_multi_app), use request.scope["path"] which
|
|
90
|
+
contains the path relative to the mount point. For non-mounted apps,
|
|
91
|
+
fall back to request.url.path.
|
|
92
|
+
|
|
93
|
+
This ensures public routes in manifests (which are relative paths like "/")
|
|
94
|
+
match correctly when apps are mounted at prefixes like "/auth-hub".
|
|
95
|
+
"""
|
|
96
|
+
# Use scope["path"] which is relative to mount point for mounted apps
|
|
97
|
+
# Fall back to url.path for non-mounted apps
|
|
98
|
+
return request.scope.get("path", request.url.path)
|
|
99
|
+
|
|
100
|
+
|
|
85
101
|
class SharedAuthMiddleware(BaseHTTPMiddleware):
|
|
86
102
|
"""
|
|
87
103
|
Middleware for shared authentication across multi-app deployments.
|
|
@@ -169,7 +185,7 @@ class SharedAuthMiddleware(BaseHTTPMiddleware):
|
|
|
169
185
|
# However, for Lazy middleware, we want to skip if not initialized yet
|
|
170
186
|
return await call_next(request)
|
|
171
187
|
|
|
172
|
-
is_public = self._is_public_route(request
|
|
188
|
+
is_public = self._is_public_route(_get_request_path(request))
|
|
173
189
|
|
|
174
190
|
# Extract token from cookie or header
|
|
175
191
|
token = self._extract_token(request)
|
|
@@ -522,7 +538,7 @@ def _create_lazy_middleware_class(
|
|
|
522
538
|
)
|
|
523
539
|
return await call_next(request)
|
|
524
540
|
|
|
525
|
-
is_public = _is_public_route_helper(request
|
|
541
|
+
is_public = _is_public_route_helper(_get_request_path(request), self._public_routes)
|
|
526
542
|
token = _extract_token_helper(
|
|
527
543
|
request, self._cookie_name, self._header_name, self._header_prefix
|
|
528
544
|
)
|
mdb_engine/core/engine.py
CHANGED
|
@@ -1095,13 +1095,14 @@ class MongoDBEngine:
|
|
|
1095
1095
|
| None = None,
|
|
1096
1096
|
on_shutdown: Callable[["FastAPI", "MongoDBEngine", dict[str, Any]], Awaitable[None]]
|
|
1097
1097
|
| None = None,
|
|
1098
|
+
is_sub_app: bool = False,
|
|
1098
1099
|
**fastapi_kwargs: Any,
|
|
1099
1100
|
) -> "FastAPI":
|
|
1100
1101
|
"""
|
|
1101
1102
|
Create a FastAPI application with proper lifespan management.
|
|
1102
1103
|
|
|
1103
1104
|
This method creates a FastAPI app that:
|
|
1104
|
-
1. Initializes the engine on startup
|
|
1105
|
+
1. Initializes the engine on startup (unless is_sub_app=True)
|
|
1105
1106
|
2. Loads and registers the manifest
|
|
1106
1107
|
3. Auto-detects multi-site mode from manifest
|
|
1107
1108
|
4. Auto-configures auth based on manifest auth.mode:
|
|
@@ -1119,6 +1120,9 @@ class MongoDBEngine:
|
|
|
1119
1120
|
Signature: async def callback(app, engine, manifest) -> None
|
|
1120
1121
|
on_shutdown: Optional async callback called before engine shutdown.
|
|
1121
1122
|
Signature: async def callback(app, engine, manifest) -> None
|
|
1123
|
+
is_sub_app: If True, skip engine initialization and lifespan management.
|
|
1124
|
+
Used when mounting as a child app in create_multi_app().
|
|
1125
|
+
Defaults to False for backward compatibility.
|
|
1122
1126
|
**fastapi_kwargs: Additional arguments passed to FastAPI()
|
|
1123
1127
|
|
|
1124
1128
|
Returns:
|
|
@@ -1177,8 +1181,9 @@ class MongoDBEngine:
|
|
|
1177
1181
|
"""Lifespan context manager for initialization and cleanup."""
|
|
1178
1182
|
nonlocal app_manifest, is_multi_site
|
|
1179
1183
|
|
|
1180
|
-
# Initialize engine
|
|
1181
|
-
|
|
1184
|
+
# Initialize engine (skip if sub-app - parent manages lifecycle)
|
|
1185
|
+
if not is_sub_app:
|
|
1186
|
+
await engine.initialize()
|
|
1182
1187
|
|
|
1183
1188
|
# Load and register manifest
|
|
1184
1189
|
app_manifest = await engine.load_manifest(manifest_path)
|
|
@@ -1207,7 +1212,20 @@ class MongoDBEngine:
|
|
|
1207
1212
|
logger.info(f"Shared auth mode for '{slug}' - SSO enabled")
|
|
1208
1213
|
# Initialize shared user pool and set on app.state
|
|
1209
1214
|
# Middleware was already added at app creation time (lazy version)
|
|
1210
|
-
|
|
1215
|
+
# For sub-apps, check if parent already initialized user pool
|
|
1216
|
+
if is_sub_app:
|
|
1217
|
+
# Check if parent app has user_pool (set by parent's initialization)
|
|
1218
|
+
# If not, initialize it (shouldn't happen, but handle gracefully)
|
|
1219
|
+
if not hasattr(app.state, "user_pool") or app.state.user_pool is None:
|
|
1220
|
+
logger.warning(
|
|
1221
|
+
f"Sub-app '{slug}' uses shared auth but user_pool not found. "
|
|
1222
|
+
"Initializing now (parent should have initialized it)."
|
|
1223
|
+
)
|
|
1224
|
+
await engine._initialize_shared_user_pool(app, app_manifest)
|
|
1225
|
+
else:
|
|
1226
|
+
logger.debug(f"Sub-app '{slug}' using shared user_pool from parent app")
|
|
1227
|
+
else:
|
|
1228
|
+
await engine._initialize_shared_user_pool(app, app_manifest)
|
|
1211
1229
|
else:
|
|
1212
1230
|
logger.info(f"Per-app auth mode for '{slug}'")
|
|
1213
1231
|
# Auto-retrieve app token for "app" mode
|
|
@@ -1416,7 +1434,9 @@ class MongoDBEngine:
|
|
|
1416
1434
|
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1417
1435
|
logger.warning(f"on_shutdown callback failed for '{slug}': {e}")
|
|
1418
1436
|
|
|
1419
|
-
|
|
1437
|
+
# Shutdown engine (skip if sub-app - parent manages lifecycle)
|
|
1438
|
+
if not is_sub_app:
|
|
1439
|
+
await engine.shutdown()
|
|
1420
1440
|
|
|
1421
1441
|
# Create FastAPI app
|
|
1422
1442
|
app = FastAPI(title=app_title, lifespan=lifespan, **fastapi_kwargs)
|
|
@@ -1502,6 +1522,398 @@ class MongoDBEngine:
|
|
|
1502
1522
|
|
|
1503
1523
|
return app
|
|
1504
1524
|
|
|
1525
|
+
def _validate_path_prefixes(self, apps: list[dict[str, Any]]) -> tuple[bool, list[str]]:
|
|
1526
|
+
"""
|
|
1527
|
+
Validate path prefixes for multi-app mounting.
|
|
1528
|
+
|
|
1529
|
+
Checks:
|
|
1530
|
+
- All prefixes start with '/'
|
|
1531
|
+
- No prefix is a prefix of another (e.g., '/app' conflicts with '/app/v2')
|
|
1532
|
+
- No conflicts with reserved paths ('/health', '/docs', '/openapi.json')
|
|
1533
|
+
|
|
1534
|
+
Args:
|
|
1535
|
+
apps: List of app configs with 'path_prefix' keys
|
|
1536
|
+
|
|
1537
|
+
Returns:
|
|
1538
|
+
Tuple of (is_valid, list_of_errors)
|
|
1539
|
+
"""
|
|
1540
|
+
errors: list[str] = []
|
|
1541
|
+
reserved_paths = {"/health", "/docs", "/openapi.json", "/redoc"}
|
|
1542
|
+
|
|
1543
|
+
# Extract path prefixes
|
|
1544
|
+
path_prefixes: list[str] = []
|
|
1545
|
+
for app_config in apps:
|
|
1546
|
+
path_prefix = app_config.get("path_prefix", f"/{app_config.get('slug', 'unknown')}")
|
|
1547
|
+
if not path_prefix.startswith("/"):
|
|
1548
|
+
app_slug = app_config.get("slug", "unknown")
|
|
1549
|
+
errors.append(f"Path prefix '{path_prefix}' must start with '/' (app: {app_slug})")
|
|
1550
|
+
continue
|
|
1551
|
+
path_prefixes.append(path_prefix)
|
|
1552
|
+
|
|
1553
|
+
# Check for conflicts with reserved paths
|
|
1554
|
+
for prefix in path_prefixes:
|
|
1555
|
+
if prefix in reserved_paths:
|
|
1556
|
+
errors.append(
|
|
1557
|
+
f"Path prefix '{prefix}' conflicts with reserved path. "
|
|
1558
|
+
"Reserved paths: /health, /docs, /openapi.json, /redoc"
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
# Check for prefix conflicts (one prefix being a prefix of another)
|
|
1562
|
+
path_prefixes_sorted = sorted(path_prefixes)
|
|
1563
|
+
for i, prefix1 in enumerate(path_prefixes_sorted):
|
|
1564
|
+
for prefix2 in path_prefixes_sorted[i + 1 :]:
|
|
1565
|
+
if prefix1.startswith(prefix2) or prefix2.startswith(prefix1):
|
|
1566
|
+
errors.append(
|
|
1567
|
+
f"Path prefix conflict: '{prefix1}' and '{prefix2}' overlap. "
|
|
1568
|
+
"One cannot be a prefix of another."
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
# Check for duplicates
|
|
1572
|
+
if len(path_prefixes) != len(set(path_prefixes)):
|
|
1573
|
+
seen = set()
|
|
1574
|
+
for prefix in path_prefixes:
|
|
1575
|
+
if prefix in seen:
|
|
1576
|
+
errors.append(f"Duplicate path prefix: '{prefix}'")
|
|
1577
|
+
seen.add(prefix)
|
|
1578
|
+
|
|
1579
|
+
return len(errors) == 0, errors
|
|
1580
|
+
|
|
1581
|
+
def create_multi_app(
|
|
1582
|
+
self,
|
|
1583
|
+
apps: list[dict[str, Any]] | None = None,
|
|
1584
|
+
multi_app_manifest: Path | None = None,
|
|
1585
|
+
title: str = "Multi-App API",
|
|
1586
|
+
root_path: str = "",
|
|
1587
|
+
**fastapi_kwargs: Any,
|
|
1588
|
+
) -> "FastAPI":
|
|
1589
|
+
"""
|
|
1590
|
+
Create a parent FastAPI app that mounts multiple child apps.
|
|
1591
|
+
|
|
1592
|
+
Each child app is mounted at a path prefix (e.g., /auth-hub, /pwd-zero) and
|
|
1593
|
+
maintains its own routes, middleware, and state while sharing the engine instance.
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
apps: List of app configurations. Each dict should have:
|
|
1597
|
+
- slug: App slug (required)
|
|
1598
|
+
- manifest: Path to manifest.json (required)
|
|
1599
|
+
- path_prefix: Optional path prefix (defaults to /{slug})
|
|
1600
|
+
- on_startup: Optional startup callback function
|
|
1601
|
+
- on_shutdown: Optional shutdown callback function
|
|
1602
|
+
multi_app_manifest: Path to a multi-app manifest.json that defines all apps.
|
|
1603
|
+
Format:
|
|
1604
|
+
{
|
|
1605
|
+
"multi_app": {
|
|
1606
|
+
"enabled": true,
|
|
1607
|
+
"apps": [
|
|
1608
|
+
{
|
|
1609
|
+
"slug": "app1",
|
|
1610
|
+
"manifest": "./app1/manifest.json",
|
|
1611
|
+
"path_prefix": "/app1",
|
|
1612
|
+
}
|
|
1613
|
+
]
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
title: Title for the parent FastAPI app
|
|
1617
|
+
root_path: Root path prefix for all mounted apps (optional)
|
|
1618
|
+
**fastapi_kwargs: Additional arguments passed to FastAPI()
|
|
1619
|
+
|
|
1620
|
+
Returns:
|
|
1621
|
+
Parent FastAPI application with all child apps mounted
|
|
1622
|
+
|
|
1623
|
+
Raises:
|
|
1624
|
+
ValueError: If configuration is invalid or path prefixes conflict
|
|
1625
|
+
RuntimeError: If engine is not initialized
|
|
1626
|
+
|
|
1627
|
+
Example:
|
|
1628
|
+
# Programmatic approach
|
|
1629
|
+
engine = MongoDBEngine(mongo_uri=..., db_name=...)
|
|
1630
|
+
app = engine.create_multi_app(
|
|
1631
|
+
apps=[
|
|
1632
|
+
{
|
|
1633
|
+
"slug": "auth-hub",
|
|
1634
|
+
"manifest": Path("./auth-hub/manifest.json"),
|
|
1635
|
+
"path_prefix": "/auth-hub",
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
"slug": "pwd-zero",
|
|
1639
|
+
"manifest": Path("./pwd-zero/manifest.json"),
|
|
1640
|
+
"path_prefix": "/pwd-zero",
|
|
1641
|
+
},
|
|
1642
|
+
]
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
# Manifest-based approach
|
|
1646
|
+
app = engine.create_multi_app(
|
|
1647
|
+
multi_app_manifest=Path("./multi_app_manifest.json")
|
|
1648
|
+
)
|
|
1649
|
+
"""
|
|
1650
|
+
import json
|
|
1651
|
+
|
|
1652
|
+
from fastapi import FastAPI
|
|
1653
|
+
|
|
1654
|
+
engine = self
|
|
1655
|
+
|
|
1656
|
+
# Load configuration from manifest or apps parameter
|
|
1657
|
+
if multi_app_manifest:
|
|
1658
|
+
manifest_path = Path(multi_app_manifest)
|
|
1659
|
+
with open(manifest_path) as f:
|
|
1660
|
+
multi_app_config = json.load(f)
|
|
1661
|
+
|
|
1662
|
+
multi_app_section = multi_app_config.get("multi_app", {})
|
|
1663
|
+
if not multi_app_section.get("enabled", False):
|
|
1664
|
+
raise ValueError(
|
|
1665
|
+
"multi_app.enabled must be True in multi_app_manifest to use multi-app mode"
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
apps_config = multi_app_section.get("apps", [])
|
|
1669
|
+
if not apps_config:
|
|
1670
|
+
raise ValueError("multi_app.apps must contain at least one app")
|
|
1671
|
+
|
|
1672
|
+
# Resolve manifest paths relative to multi_app_manifest location
|
|
1673
|
+
manifest_dir = manifest_path.parent
|
|
1674
|
+
apps = []
|
|
1675
|
+
for app_config in apps_config:
|
|
1676
|
+
manifest_rel_path = app_config.get("manifest")
|
|
1677
|
+
if not manifest_rel_path:
|
|
1678
|
+
raise ValueError(f"App '{app_config.get('slug')}' missing 'manifest' field")
|
|
1679
|
+
|
|
1680
|
+
# Resolve relative to multi_app_manifest location
|
|
1681
|
+
manifest_full_path = (manifest_dir / manifest_rel_path).resolve()
|
|
1682
|
+
slug = app_config.get("slug")
|
|
1683
|
+
path_prefix = app_config.get("path_prefix", f"/{slug}")
|
|
1684
|
+
|
|
1685
|
+
apps.append(
|
|
1686
|
+
{
|
|
1687
|
+
"slug": slug,
|
|
1688
|
+
"manifest": manifest_full_path,
|
|
1689
|
+
"path_prefix": path_prefix,
|
|
1690
|
+
}
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
elif apps is not None:
|
|
1694
|
+
apps_config = apps
|
|
1695
|
+
# Convert Path objects to Path if they're strings
|
|
1696
|
+
apps = []
|
|
1697
|
+
for app_config in apps_config:
|
|
1698
|
+
manifest = app_config.get("manifest")
|
|
1699
|
+
if isinstance(manifest, str):
|
|
1700
|
+
manifest = Path(manifest)
|
|
1701
|
+
apps.append(
|
|
1702
|
+
{
|
|
1703
|
+
"slug": app_config.get("slug"),
|
|
1704
|
+
"manifest": manifest,
|
|
1705
|
+
"path_prefix": app_config.get("path_prefix", f"/{app_config.get('slug')}"),
|
|
1706
|
+
"on_startup": app_config.get("on_startup"),
|
|
1707
|
+
"on_shutdown": app_config.get("on_shutdown"),
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
else:
|
|
1711
|
+
raise ValueError("Either 'apps' or 'multi_app_manifest' must be provided")
|
|
1712
|
+
|
|
1713
|
+
if not apps:
|
|
1714
|
+
raise ValueError("At least one app must be configured")
|
|
1715
|
+
|
|
1716
|
+
# Validate path prefixes
|
|
1717
|
+
is_valid, errors = self._validate_path_prefixes(apps)
|
|
1718
|
+
if not is_valid:
|
|
1719
|
+
raise ValueError(
|
|
1720
|
+
"Path prefix validation failed:\n" + "\n".join(f" - {e}" for e in errors)
|
|
1721
|
+
)
|
|
1722
|
+
|
|
1723
|
+
# Check if any app uses shared auth
|
|
1724
|
+
has_shared_auth = False
|
|
1725
|
+
for app_config in apps:
|
|
1726
|
+
try:
|
|
1727
|
+
manifest_path = app_config["manifest"]
|
|
1728
|
+
with open(manifest_path) as f:
|
|
1729
|
+
app_manifest_pre = json.load(f)
|
|
1730
|
+
auth_config = app_manifest_pre.get("auth", {})
|
|
1731
|
+
if auth_config.get("mode") == "shared":
|
|
1732
|
+
has_shared_auth = True
|
|
1733
|
+
break
|
|
1734
|
+
except (FileNotFoundError, json.JSONDecodeError, KeyError) as e:
|
|
1735
|
+
logger.warning(f"Could not check auth mode for app '{app_config.get('slug')}': {e}")
|
|
1736
|
+
|
|
1737
|
+
# State for parent app
|
|
1738
|
+
mounted_apps: list[dict[str, Any]] = []
|
|
1739
|
+
shared_user_pool_initialized = False
|
|
1740
|
+
|
|
1741
|
+
@asynccontextmanager
|
|
1742
|
+
async def lifespan(app: FastAPI):
|
|
1743
|
+
"""Lifespan context manager for parent app."""
|
|
1744
|
+
nonlocal mounted_apps, shared_user_pool_initialized
|
|
1745
|
+
|
|
1746
|
+
# Initialize engine
|
|
1747
|
+
await engine.initialize()
|
|
1748
|
+
|
|
1749
|
+
# Initialize shared user pool once if any app uses shared auth
|
|
1750
|
+
if has_shared_auth:
|
|
1751
|
+
logger.info("Initializing shared user pool for multi-app deployment")
|
|
1752
|
+
# Find first app with shared auth to get manifest for initialization
|
|
1753
|
+
for app_config in apps:
|
|
1754
|
+
try:
|
|
1755
|
+
manifest_path = app_config["manifest"]
|
|
1756
|
+
with open(manifest_path) as f:
|
|
1757
|
+
app_manifest_pre = json.load(f)
|
|
1758
|
+
auth_config = app_manifest_pre.get("auth", {})
|
|
1759
|
+
if auth_config.get("mode") == "shared":
|
|
1760
|
+
await engine._initialize_shared_user_pool(app, app_manifest_pre)
|
|
1761
|
+
shared_user_pool_initialized = True
|
|
1762
|
+
logger.info("Shared user pool initialized for multi-app deployment")
|
|
1763
|
+
break
|
|
1764
|
+
except (FileNotFoundError, json.JSONDecodeError, KeyError) as e:
|
|
1765
|
+
app_slug = app_config.get("slug", "unknown")
|
|
1766
|
+
logger.warning(
|
|
1767
|
+
f"Could not initialize shared user pool from app '{app_slug}': {e}"
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
# Mount each child app
|
|
1771
|
+
for app_config in apps:
|
|
1772
|
+
slug = app_config["slug"]
|
|
1773
|
+
manifest_path = app_config["manifest"]
|
|
1774
|
+
path_prefix = app_config["path_prefix"]
|
|
1775
|
+
on_startup = app_config.get("on_startup")
|
|
1776
|
+
on_shutdown = app_config.get("on_shutdown")
|
|
1777
|
+
|
|
1778
|
+
try:
|
|
1779
|
+
# Create child app as sub-app (shares engine and lifecycle)
|
|
1780
|
+
child_app = engine.create_app(
|
|
1781
|
+
slug=slug,
|
|
1782
|
+
manifest=manifest_path,
|
|
1783
|
+
is_sub_app=True, # Important: marks as sub-app
|
|
1784
|
+
on_startup=on_startup,
|
|
1785
|
+
on_shutdown=on_shutdown,
|
|
1786
|
+
)
|
|
1787
|
+
|
|
1788
|
+
# Share user_pool with child app if shared auth is enabled
|
|
1789
|
+
if shared_user_pool_initialized and hasattr(app.state, "user_pool"):
|
|
1790
|
+
child_app.state.user_pool = app.state.user_pool
|
|
1791
|
+
# Also share audit_log if available
|
|
1792
|
+
if hasattr(app.state, "audit_log"):
|
|
1793
|
+
child_app.state.audit_log = app.state.audit_log
|
|
1794
|
+
logger.debug(f"Shared user_pool with child app '{slug}'")
|
|
1795
|
+
|
|
1796
|
+
# Mount child app at path prefix
|
|
1797
|
+
app.mount(path_prefix, child_app)
|
|
1798
|
+
mounted_apps.append(
|
|
1799
|
+
{
|
|
1800
|
+
"slug": slug,
|
|
1801
|
+
"path_prefix": path_prefix,
|
|
1802
|
+
"status": "mounted",
|
|
1803
|
+
}
|
|
1804
|
+
)
|
|
1805
|
+
logger.info(f"Mounted app '{slug}' at path prefix '{path_prefix}'")
|
|
1806
|
+
|
|
1807
|
+
except (
|
|
1808
|
+
FileNotFoundError,
|
|
1809
|
+
json.JSONDecodeError,
|
|
1810
|
+
ValueError,
|
|
1811
|
+
KeyError,
|
|
1812
|
+
RuntimeError,
|
|
1813
|
+
) as e:
|
|
1814
|
+
logger.error(f"Failed to mount app '{slug}': {e}", exc_info=True)
|
|
1815
|
+
mounted_apps.append(
|
|
1816
|
+
{
|
|
1817
|
+
"slug": slug,
|
|
1818
|
+
"path_prefix": path_prefix,
|
|
1819
|
+
"status": "failed",
|
|
1820
|
+
"error": str(e),
|
|
1821
|
+
}
|
|
1822
|
+
)
|
|
1823
|
+
# Continue with other apps even if one fails
|
|
1824
|
+
continue
|
|
1825
|
+
|
|
1826
|
+
# Expose engine and mounted apps info on parent app state
|
|
1827
|
+
app.state.engine = engine
|
|
1828
|
+
app.state.mounted_apps = mounted_apps
|
|
1829
|
+
app.state.is_multi_app = True
|
|
1830
|
+
|
|
1831
|
+
yield
|
|
1832
|
+
|
|
1833
|
+
# Shutdown is handled by parent app
|
|
1834
|
+
await engine.shutdown()
|
|
1835
|
+
|
|
1836
|
+
# Create parent FastAPI app
|
|
1837
|
+
parent_app = FastAPI(title=title, lifespan=lifespan, root_path=root_path, **fastapi_kwargs)
|
|
1838
|
+
|
|
1839
|
+
# Add request scope middleware
|
|
1840
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
1841
|
+
|
|
1842
|
+
from ..di import ScopeManager
|
|
1843
|
+
|
|
1844
|
+
class RequestScopeMiddleware(BaseHTTPMiddleware):
|
|
1845
|
+
"""Middleware that manages request-scoped DI instances."""
|
|
1846
|
+
|
|
1847
|
+
async def dispatch(self, request, call_next):
|
|
1848
|
+
ScopeManager.begin_request()
|
|
1849
|
+
try:
|
|
1850
|
+
response = await call_next(request)
|
|
1851
|
+
return response
|
|
1852
|
+
finally:
|
|
1853
|
+
ScopeManager.end_request()
|
|
1854
|
+
|
|
1855
|
+
parent_app.add_middleware(RequestScopeMiddleware)
|
|
1856
|
+
logger.debug("RequestScopeMiddleware added for parent app")
|
|
1857
|
+
|
|
1858
|
+
# Add shared CORS middleware if configured
|
|
1859
|
+
# (Individual apps can add their own CORS, but parent-level is useful)
|
|
1860
|
+
try:
|
|
1861
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
1862
|
+
|
|
1863
|
+
parent_app.add_middleware(
|
|
1864
|
+
CORSMiddleware,
|
|
1865
|
+
allow_origins=["*"], # Can be configured via manifest later
|
|
1866
|
+
allow_credentials=True,
|
|
1867
|
+
allow_methods=["*"],
|
|
1868
|
+
allow_headers=["*"],
|
|
1869
|
+
)
|
|
1870
|
+
logger.debug("CORS middleware added for parent app")
|
|
1871
|
+
except ImportError:
|
|
1872
|
+
logger.warning("CORS middleware not available")
|
|
1873
|
+
|
|
1874
|
+
# Add unified health check endpoint
|
|
1875
|
+
@parent_app.get("/health")
|
|
1876
|
+
async def health_check():
|
|
1877
|
+
"""Unified health check for all mounted apps."""
|
|
1878
|
+
from ..observability import check_engine_health, check_mongodb_health
|
|
1879
|
+
|
|
1880
|
+
# Both are async functions
|
|
1881
|
+
engine_health = await check_engine_health(engine)
|
|
1882
|
+
mongo_health = await check_mongodb_health(engine.mongo_client)
|
|
1883
|
+
|
|
1884
|
+
mounted_status = {}
|
|
1885
|
+
for mounted_app_info in mounted_apps:
|
|
1886
|
+
mounted_status[mounted_app_info["slug"]] = {
|
|
1887
|
+
"path_prefix": mounted_app_info["path_prefix"],
|
|
1888
|
+
"status": mounted_app_info["status"],
|
|
1889
|
+
}
|
|
1890
|
+
if "error" in mounted_app_info:
|
|
1891
|
+
mounted_status[mounted_app_info["slug"]]["error"] = mounted_app_info["error"]
|
|
1892
|
+
|
|
1893
|
+
overall_status = (
|
|
1894
|
+
"healthy"
|
|
1895
|
+
if engine_health.status.value == "healthy"
|
|
1896
|
+
and mongo_health.status.value == "healthy"
|
|
1897
|
+
else "unhealthy"
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
return {
|
|
1901
|
+
"status": overall_status,
|
|
1902
|
+
"engine": {
|
|
1903
|
+
"status": engine_health.status.value,
|
|
1904
|
+
"message": engine_health.message,
|
|
1905
|
+
},
|
|
1906
|
+
"mongodb": {
|
|
1907
|
+
"status": mongo_health.status.value,
|
|
1908
|
+
"message": mongo_health.message,
|
|
1909
|
+
},
|
|
1910
|
+
"mounted_apps": mounted_status,
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
logger.info(f"Multi-app parent created with {len(apps)} app(s) configured")
|
|
1914
|
+
|
|
1915
|
+
return parent_app
|
|
1916
|
+
|
|
1505
1917
|
async def _initialize_shared_user_pool(
|
|
1506
1918
|
self,
|
|
1507
1919
|
app: "FastAPI",
|
mdb_engine/core/manifest.py
CHANGED
|
@@ -1732,6 +1732,99 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1732
1732
|
"additionalProperties": False,
|
|
1733
1733
|
"description": "Observability configuration (health checks, metrics, logging)",
|
|
1734
1734
|
},
|
|
1735
|
+
"multi_app": {
|
|
1736
|
+
"type": "object",
|
|
1737
|
+
"properties": {
|
|
1738
|
+
"enabled": {
|
|
1739
|
+
"type": "boolean",
|
|
1740
|
+
"default": False,
|
|
1741
|
+
"description": "Enable multi-app mounting mode",
|
|
1742
|
+
},
|
|
1743
|
+
"apps": {
|
|
1744
|
+
"type": "array",
|
|
1745
|
+
"items": {
|
|
1746
|
+
"type": "object",
|
|
1747
|
+
"properties": {
|
|
1748
|
+
"slug": {
|
|
1749
|
+
"type": "string",
|
|
1750
|
+
"pattern": "^[a-z0-9_-]+$",
|
|
1751
|
+
"description": (
|
|
1752
|
+
"App slug (lowercase alphanumeric, underscores, hyphens)"
|
|
1753
|
+
),
|
|
1754
|
+
},
|
|
1755
|
+
"manifest": {
|
|
1756
|
+
"type": "string",
|
|
1757
|
+
"description": (
|
|
1758
|
+
"Path to manifest.json file "
|
|
1759
|
+
"(relative to multi_app manifest or absolute)"
|
|
1760
|
+
),
|
|
1761
|
+
},
|
|
1762
|
+
"path_prefix": {
|
|
1763
|
+
"type": "string",
|
|
1764
|
+
"pattern": "^/.*",
|
|
1765
|
+
"default": "/{slug}",
|
|
1766
|
+
"description": (
|
|
1767
|
+
"Path prefix for mounting (defaults to /{slug}). "
|
|
1768
|
+
"Must start with '/' and be unique across all apps."
|
|
1769
|
+
),
|
|
1770
|
+
},
|
|
1771
|
+
"on_startup": {
|
|
1772
|
+
"type": "string",
|
|
1773
|
+
"description": (
|
|
1774
|
+
"Optional: Python function path for startup callback "
|
|
1775
|
+
"(e.g., 'module.function_name'). "
|
|
1776
|
+
"Not yet supported in manifest-based config."
|
|
1777
|
+
),
|
|
1778
|
+
},
|
|
1779
|
+
"on_shutdown": {
|
|
1780
|
+
"type": "string",
|
|
1781
|
+
"description": (
|
|
1782
|
+
"Optional: Python function path for shutdown callback "
|
|
1783
|
+
"(e.g., 'module.function_name'). "
|
|
1784
|
+
"Not yet supported in manifest-based config."
|
|
1785
|
+
),
|
|
1786
|
+
},
|
|
1787
|
+
},
|
|
1788
|
+
"required": ["slug", "manifest"],
|
|
1789
|
+
"additionalProperties": False,
|
|
1790
|
+
},
|
|
1791
|
+
"minItems": 1,
|
|
1792
|
+
"description": "List of apps to mount in multi-app mode",
|
|
1793
|
+
},
|
|
1794
|
+
"shared_middleware": {
|
|
1795
|
+
"type": "object",
|
|
1796
|
+
"properties": {
|
|
1797
|
+
"cors": {
|
|
1798
|
+
"type": "boolean",
|
|
1799
|
+
"default": True,
|
|
1800
|
+
"description": "Enable CORS middleware at parent level (default: true)",
|
|
1801
|
+
},
|
|
1802
|
+
"rate_limiting": {
|
|
1803
|
+
"type": "boolean",
|
|
1804
|
+
"default": True,
|
|
1805
|
+
"description": (
|
|
1806
|
+
"Enable rate limiting middleware at parent level (default: true)"
|
|
1807
|
+
),
|
|
1808
|
+
},
|
|
1809
|
+
"health_checks": {
|
|
1810
|
+
"type": "boolean",
|
|
1811
|
+
"default": True,
|
|
1812
|
+
"description": (
|
|
1813
|
+
"Enable unified health check endpoint at /health (default: true)"
|
|
1814
|
+
),
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
"additionalProperties": False,
|
|
1818
|
+
"description": "Shared middleware configuration for parent app",
|
|
1819
|
+
},
|
|
1820
|
+
},
|
|
1821
|
+
"additionalProperties": False,
|
|
1822
|
+
"description": (
|
|
1823
|
+
"Multi-app mounting configuration. When enabled, allows mounting "
|
|
1824
|
+
"multiple FastAPI apps under a single parent app with path prefixes. "
|
|
1825
|
+
"Useful for deploying multiple apps (e.g., SSO apps) on a single service."
|
|
1826
|
+
),
|
|
1827
|
+
},
|
|
1735
1828
|
"initial_data": {
|
|
1736
1829
|
"type": "object",
|
|
1737
1830
|
"patternProperties": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdb-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: MongoDB Engine
|
|
5
5
|
Home-page: https://github.com/ranfysvalle02/mdb-engine
|
|
6
6
|
Author: Fabian Valle
|
|
@@ -106,58 +106,162 @@ pip install mdb-engine
|
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
|
109
|
-
## 30-Second Quick Start
|
|
109
|
+
## 30-Second Quick Start: Build a Todo List API
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
Let's build a complete CRUD todo list app in 3 steps!
|
|
112
|
+
|
|
113
|
+
### Step 1: Create `manifest.json`
|
|
112
114
|
|
|
113
115
|
```json
|
|
114
116
|
{
|
|
115
117
|
"schema_version": "2.0",
|
|
116
|
-
"slug": "
|
|
117
|
-
"name": "
|
|
118
|
+
"slug": "todo_app",
|
|
119
|
+
"name": "Todo List App",
|
|
118
120
|
"managed_indexes": {
|
|
119
|
-
"
|
|
121
|
+
"todos": [
|
|
120
122
|
{
|
|
121
123
|
"type": "regular",
|
|
122
|
-
"keys": {"
|
|
123
|
-
"name": "
|
|
124
|
+
"keys": {"completed": 1, "created_at": -1},
|
|
125
|
+
"name": "completed_sort"
|
|
124
126
|
}
|
|
125
127
|
]
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
```
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
### Step 2: Create `app.py` with Full CRUD
|
|
131
133
|
|
|
132
134
|
```python
|
|
135
|
+
from datetime import datetime
|
|
133
136
|
from pathlib import Path
|
|
134
|
-
from
|
|
137
|
+
from typing import Optional
|
|
138
|
+
|
|
139
|
+
from bson import ObjectId
|
|
140
|
+
from fastapi import Depends, HTTPException
|
|
141
|
+
from pydantic import BaseModel
|
|
142
|
+
|
|
135
143
|
from mdb_engine import MongoDBEngine
|
|
136
144
|
from mdb_engine.dependencies import get_scoped_db
|
|
137
145
|
|
|
138
|
-
# Initialize
|
|
146
|
+
# Initialize engine
|
|
139
147
|
engine = MongoDBEngine(
|
|
140
148
|
mongo_uri="mongodb://localhost:27017",
|
|
141
149
|
db_name="my_database"
|
|
142
150
|
)
|
|
143
151
|
|
|
144
|
-
# Create app - manifest.json
|
|
145
|
-
app = engine.create_app(
|
|
152
|
+
# Create app - manifest.json loaded automatically!
|
|
153
|
+
app = engine.create_app(
|
|
154
|
+
slug="todo_app",
|
|
155
|
+
manifest=Path("manifest.json")
|
|
156
|
+
)
|
|
146
157
|
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
# Pydantic models
|
|
159
|
+
class TodoCreate(BaseModel):
|
|
160
|
+
title: str
|
|
161
|
+
description: Optional[str] = None
|
|
162
|
+
|
|
163
|
+
class TodoUpdate(BaseModel):
|
|
164
|
+
title: Optional[str] = None
|
|
165
|
+
description: Optional[str] = None
|
|
166
|
+
completed: Optional[bool] = None
|
|
167
|
+
|
|
168
|
+
# CREATE - Add a new todo
|
|
169
|
+
@app.post("/todos")
|
|
170
|
+
async def create_todo(todo: TodoCreate, db=Depends(get_scoped_db)):
|
|
171
|
+
doc = {
|
|
172
|
+
**todo.dict(),
|
|
173
|
+
"completed": False,
|
|
174
|
+
"created_at": datetime.utcnow()
|
|
175
|
+
}
|
|
176
|
+
result = await db.todos.insert_one(doc)
|
|
177
|
+
return {"id": str(result.inserted_id), "message": "Todo created"}
|
|
178
|
+
|
|
179
|
+
# READ - List all todos
|
|
180
|
+
@app.get("/todos")
|
|
181
|
+
async def list_todos(completed: Optional[bool] = None, db=Depends(get_scoped_db)):
|
|
182
|
+
query = {}
|
|
183
|
+
if completed is not None:
|
|
184
|
+
query["completed"] = completed
|
|
185
|
+
|
|
186
|
+
todos = await db.todos.find(query).sort("created_at", -1).to_list(length=100)
|
|
187
|
+
for todo in todos:
|
|
188
|
+
todo["_id"] = str(todo["_id"])
|
|
189
|
+
return {"todos": todos, "count": len(todos)}
|
|
190
|
+
|
|
191
|
+
# READ - Get single todo
|
|
192
|
+
@app.get("/todos/{todo_id}")
|
|
193
|
+
async def get_todo(todo_id: str, db=Depends(get_scoped_db)):
|
|
194
|
+
todo = await db.todos.find_one({"_id": ObjectId(todo_id)})
|
|
195
|
+
if not todo:
|
|
196
|
+
raise HTTPException(status_code=404, detail="Todo not found")
|
|
197
|
+
todo["_id"] = str(todo["_id"])
|
|
198
|
+
return todo
|
|
199
|
+
|
|
200
|
+
# UPDATE - Update a todo
|
|
201
|
+
@app.put("/todos/{todo_id}")
|
|
202
|
+
async def update_todo(todo_id: str, todo: TodoUpdate, db=Depends(get_scoped_db)):
|
|
203
|
+
updates = {k: v for k, v in todo.dict(exclude_unset=True).items() if v is not None}
|
|
204
|
+
if not updates:
|
|
205
|
+
raise HTTPException(status_code=400, detail="No fields to update")
|
|
206
|
+
|
|
207
|
+
updates["updated_at"] = datetime.utcnow()
|
|
208
|
+
result = await db.todos.update_one(
|
|
209
|
+
{"_id": ObjectId(todo_id)},
|
|
210
|
+
{"$set": updates}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if result.matched_count == 0:
|
|
214
|
+
raise HTTPException(status_code=404, detail="Todo not found")
|
|
215
|
+
return {"message": "Todo updated"}
|
|
216
|
+
|
|
217
|
+
# DELETE - Delete a todo
|
|
218
|
+
@app.delete("/todos/{todo_id}")
|
|
219
|
+
async def delete_todo(todo_id: str, db=Depends(get_scoped_db)):
|
|
220
|
+
result = await db.todos.delete_one({"_id": ObjectId(todo_id)})
|
|
221
|
+
if result.deleted_count == 0:
|
|
222
|
+
raise HTTPException(status_code=404, detail="Todo not found")
|
|
223
|
+
return {"message": "Todo deleted"}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Step 3: Run It!
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Start MongoDB (if not running)
|
|
230
|
+
mongod
|
|
231
|
+
|
|
232
|
+
# Install dependencies
|
|
233
|
+
pip install mdb-engine fastapi uvicorn
|
|
234
|
+
|
|
235
|
+
# Run the app
|
|
236
|
+
uvicorn app:app --reload
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Test your API:**
|
|
240
|
+
```bash
|
|
241
|
+
# Create a todo
|
|
242
|
+
curl -X POST http://localhost:8000/todos \
|
|
243
|
+
-H "Content-Type: application/json" \
|
|
244
|
+
-d '{"title": "Buy groceries", "description": "Milk and eggs"}'
|
|
245
|
+
|
|
246
|
+
# List todos
|
|
247
|
+
curl http://localhost:8000/todos
|
|
248
|
+
|
|
249
|
+
# Update a todo (replace {id} with actual ID)
|
|
250
|
+
curl -X PUT http://localhost:8000/todos/{id} \
|
|
251
|
+
-H "Content-Type: application/json" \
|
|
252
|
+
-d '{"completed": true}'
|
|
253
|
+
|
|
254
|
+
# Delete a todo
|
|
255
|
+
curl -X DELETE http://localhost:8000/todos/{id}
|
|
152
256
|
```
|
|
153
257
|
|
|
154
258
|
**What just happened?**
|
|
155
|
-
- ✅
|
|
156
|
-
- ✅ Indexes created
|
|
157
|
-
- ✅
|
|
158
|
-
- ✅
|
|
259
|
+
- ✅ **Automatic scoping**: All queries filtered by `app_id` — your data is isolated
|
|
260
|
+
- ✅ **Indexes created**: The `completed_sort` index was created automatically
|
|
261
|
+
- ✅ **Lifecycle managed**: Startup/shutdown handled automatically
|
|
262
|
+
- ✅ **Zero boilerplate**: No connection setup, no index scripts, no auth handlers
|
|
159
263
|
|
|
160
|
-
That's it
|
|
264
|
+
**That's it!** You now have a fully functional, production-ready todo API with automatic data sandboxing, index management, and lifecycle handling.
|
|
161
265
|
|
|
162
266
|
---
|
|
163
267
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
|
|
2
|
-
mdb_engine/__init__.py,sha256=
|
|
2
|
+
mdb_engine/__init__.py,sha256=hyAHO2lyogYrvCIKKoTLuaReM3g0f7gygVUbg2Iht5k,3163
|
|
3
3
|
mdb_engine/config.py,sha256=DTAyxfKB8ogyI0v5QR9Y-SJOgXQr_eDBCKxNBSqEyLc,7269
|
|
4
4
|
mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
|
|
5
5
|
mdb_engine/dependencies.py,sha256=MJuYQhZ9ZGzXlip1ha5zba9Rvn04HDPWahJFJH81Q2s,14107
|
|
@@ -26,7 +26,7 @@ mdb_engine/auth/provider.py,sha256=FOSHn-jp4VYtxvmjnzho5kH6y-xDKWbcKUorYRRl1C4,2
|
|
|
26
26
|
mdb_engine/auth/rate_limiter.py,sha256=l3EYZE1Kz9yVfZwNrKq_1AgdD7GXB1WOLSqqGQVSSgA,15808
|
|
27
27
|
mdb_engine/auth/restrictions.py,sha256=tOyQBO_w0bK9zmTsOPZf9cbvh4oITvpNfSxIXt-XrcU,8824
|
|
28
28
|
mdb_engine/auth/session_manager.py,sha256=ywWJjTarm-obgJ3zO3s-1cdqEYe0XrozlY00q_yMJ8I,15396
|
|
29
|
-
mdb_engine/auth/shared_middleware.py,sha256=
|
|
29
|
+
mdb_engine/auth/shared_middleware.py,sha256=ZyGjv3cQ5pLeFBKrW4X2W11e0-ZsuOhJaWvGAQ7I2Kw,31137
|
|
30
30
|
mdb_engine/auth/shared_users.py,sha256=25OBks4VRHaYZW7R61vnplV7wmr7RRpDctSgnej_nxc,26773
|
|
31
31
|
mdb_engine/auth/token_lifecycle.py,sha256=Q9S1X2Y6W7Ckt5PvyYXswBRh2Tg9DGpyRv_3Xve7VYQ,6708
|
|
32
32
|
mdb_engine/auth/token_store.py,sha256=-B8j5RH5YEoKsswF4rnMoI51BaxMe4icke3kuehXmcI,9121
|
|
@@ -46,9 +46,9 @@ mdb_engine/core/app_registration.py,sha256=7szt2a7aBkpSppjmhdkkPPYMKGKo0MkLKZeEe
|
|
|
46
46
|
mdb_engine/core/app_secrets.py,sha256=bo-syg9UUATibNyXEZs-0TTYWG-JaY-2S0yNSGA12n0,10524
|
|
47
47
|
mdb_engine/core/connection.py,sha256=XnwuPG34pJ7kJGJ84T0mhj1UZ6_CLz_9qZf6NRYGIS8,8346
|
|
48
48
|
mdb_engine/core/encryption.py,sha256=RZ5LPF5g28E3ZBn6v1IMw_oas7u9YGFtBcEj8lTi9LM,7515
|
|
49
|
-
mdb_engine/core/engine.py,sha256=
|
|
49
|
+
mdb_engine/core/engine.py,sha256=R1mU99Aa3z0P4xlPnofm9H9sZr4CWnsAaa85j-N3iJQ,86857
|
|
50
50
|
mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
|
|
51
|
-
mdb_engine/core/manifest.py,sha256=
|
|
51
|
+
mdb_engine/core/manifest.py,sha256=jguhjVPAHMZGxOJcdSGouv9_XiKmxUjDmyjn2yXHCj4,139205
|
|
52
52
|
mdb_engine/core/ray_integration.py,sha256=vexYOzztscvRYje1xTNmXJbi99oJxCaVJAwKfTNTF_E,13610
|
|
53
53
|
mdb_engine/core/seeding.py,sha256=c5IhdwlqUf_4Q5FFTAhPLaHPaUr_Txo3z_DUwZmWsFs,6421
|
|
54
54
|
mdb_engine/core/service_initialization.py,sha256=rtb6BaPvFqomwT_s7bdbbvqi5m74llT0LkJFEhVG9Gg,12996
|
|
@@ -89,9 +89,9 @@ mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknU
|
|
|
89
89
|
mdb_engine/routing/websockets.py,sha256=3X4OjQv_Nln4UmeifJky0gFhMG8A6alR77I8g1iIOLY,29311
|
|
90
90
|
mdb_engine/utils/__init__.py,sha256=lDxQSGqkV4fVw5TWIk6FA6_eey_ZnEtMY0fir3cpAe8,236
|
|
91
91
|
mdb_engine/utils/mongo.py,sha256=Oqtv4tQdpiiZzrilGLEYQPo8Vmh8WsTQypxQs8Of53s,3369
|
|
92
|
-
mdb_engine-0.
|
|
93
|
-
mdb_engine-0.
|
|
94
|
-
mdb_engine-0.
|
|
95
|
-
mdb_engine-0.
|
|
96
|
-
mdb_engine-0.
|
|
97
|
-
mdb_engine-0.
|
|
92
|
+
mdb_engine-0.3.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
93
|
+
mdb_engine-0.3.1.dist-info/METADATA,sha256=Zxjb2zcIkgk4-eUJbWv0nDs9OMKpeJuiANzINiQkMAw,15810
|
|
94
|
+
mdb_engine-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
95
|
+
mdb_engine-0.3.1.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
|
|
96
|
+
mdb_engine-0.3.1.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
|
|
97
|
+
mdb_engine-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|