mdb-engine 0.2.4__py3-none-any.whl → 0.3.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.
mdb_engine/__init__.py CHANGED
@@ -81,7 +81,8 @@ 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.2.4" # Patch version bump for exception handling improvements
84
+ __version__ = "0.3.0" # Minor version bump: Multi-app mounting, SSO improvements,
85
+ # and exception handling fixes
85
86
 
86
87
  __all__ = [
87
88
  # Core Engine
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
- await engine.initialize()
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
- await engine._initialize_shared_user_pool(app, app_manifest)
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
- await engine.shutdown()
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",
@@ -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.2.4
3
+ Version: 0.3.0
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
- **Step 1**: Create your `manifest.json`:
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": "my_app",
117
- "name": "My App",
118
+ "slug": "todo_app",
119
+ "name": "Todo List App",
118
120
  "managed_indexes": {
119
- "tasks": [
121
+ "todos": [
120
122
  {
121
123
  "type": "regular",
122
- "keys": {"status": 1, "created_at": -1},
123
- "name": "status_sort"
124
+ "keys": {"completed": 1, "created_at": -1},
125
+ "name": "completed_sort"
124
126
  }
125
127
  ]
126
128
  }
127
129
  }
128
130
  ```
129
131
 
130
- **Step 2**: Create your FastAPI app:
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 fastapi import Depends
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 the engine
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 is loaded automatically!
145
- app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
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
- # Use request-scoped dependencies - all queries automatically isolated
148
- @app.post("/tasks")
149
- async def create_task(task: dict, db=Depends(get_scoped_db)):
150
- result = await db.tasks.insert_one(task)
151
- return {"id": str(result.inserted_id)}
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
- - ✅ Engine loaded your `manifest.json`
156
- - ✅ Indexes created automatically from `managed_indexes`
157
- - ✅ Database queries automatically scoped to your app
158
- - ✅ Lifecycle management handled (startup/shutdown)
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. Your data is automatically sandboxed, indexes are created, and cleanup is handled.
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=ll3Xc4FaoNqqK_lnWOJTyFHY3uAyoB40g3NnBjwyx5U,3155
2
+ mdb_engine/__init__.py,sha256=MUAh3xwHgMQZ9FyYIfDnOwiUficDQbkZ3sBb43v-U64,3189
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
@@ -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=ciQHxHEcFh-DOQ0X9gV5kKk9WAxj3GKg5JDIPfFZjMg,69459
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=ROaMKrlHY2gi77ScQCWu8U_QB7CVCoR8CQrP1gmtrtw,134756
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.2.4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
93
- mdb_engine-0.2.4.dist-info/METADATA,sha256=DRYO0436-KCFjF-HHi1deGWWoO0Yq3vzKl53qow2ldE,12750
94
- mdb_engine-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
95
- mdb_engine-0.2.4.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
96
- mdb_engine-0.2.4.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
97
- mdb_engine-0.2.4.dist-info/RECORD,,
92
+ mdb_engine-0.3.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
93
+ mdb_engine-0.3.0.dist-info/METADATA,sha256=yifqmZSH5TBouzzOqnelCgGXlnMMqmNgTjRl7SXzk5g,15810
94
+ mdb_engine-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
95
+ mdb_engine-0.3.0.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
96
+ mdb_engine-0.3.0.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
97
+ mdb_engine-0.3.0.dist-info/RECORD,,