mdb-engine 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,425 @@
1
1
  """
2
2
  Generate command for CLI.
3
3
 
4
- Generates a template manifest.json file.
4
+ Generates manifests and full app scaffolding.
5
+
6
+ Usage:
7
+ # Generate manifest only (existing behavior)
8
+ mdb generate manifest --slug my-app --name "My App"
9
+
10
+ # Generate full app structure (new)
11
+ mdb generate app --slug my-app --name "My App"
12
+ mdb generate app --slug my-app --multi-site # Opt-in multi-site
13
+ mdb generate app --slug my-app --ray # Opt-in Ray support
5
14
 
6
15
  This module is part of MDB_ENGINE - MongoDB Engine.
7
16
  """
8
17
 
9
- import sys
18
+ import json
10
19
  from pathlib import Path
11
- from typing import Any, Dict
20
+ from typing import Any, Dict, List, Optional
12
21
 
13
22
  import click
14
23
 
15
24
  from ...core.manifest import CURRENT_SCHEMA_VERSION
16
25
  from ..utils import save_manifest_file
17
26
 
27
+ # --- App Templates ---
28
+
29
+ SINGLE_APP_WEB_TEMPLATE = '''"""
30
+ {app_name} Application
31
+
32
+ Single-app mode - simple and straightforward.
33
+ Generated by MDB Engine CLI.
34
+ """
35
+
36
+ import os
37
+ from pathlib import Path
38
+
39
+ from mdb_engine import MongoDBEngine
40
+
41
+ # Initialize engine
42
+ engine = MongoDBEngine(
43
+ mongo_uri=os.getenv("MONGODB_URI", "mongodb://localhost:27017"),
44
+ db_name=os.getenv("MONGODB_DB", "mdb_runtime"),
45
+ )
46
+
47
+ # Create FastAPI app with automatic lifecycle management
48
+ app = engine.create_app(
49
+ slug="{slug}",
50
+ manifest=Path(__file__).parent / "manifest.json",
51
+ )
52
+
53
+
54
+ @app.get("/")
55
+ async def index():
56
+ """Root endpoint."""
57
+ return {{"app": "{slug}", "status": "ok"}}
58
+
59
+
60
+ @app.get("/health")
61
+ async def health():
62
+ """Health check endpoint."""
63
+ return {{"status": "healthy", "app": "{slug}"}}
64
+
65
+
66
+ if __name__ == "__main__":
67
+ import uvicorn
68
+ uvicorn.run(app, host="0.0.0.0", port=8000)
69
+ '''
70
+
71
+ MULTI_SITE_WEB_TEMPLATE = '''"""
72
+ {app_name} Application
73
+
74
+ Multi-site mode with optional Ray support.
75
+ Generated by MDB Engine CLI.
76
+
77
+ Multi-site mode is auto-detected from manifest when:
78
+ - cross_app_policy is "explicit", or
79
+ - read_scopes includes multiple apps
80
+ """
81
+
82
+ import os
83
+ from pathlib import Path
84
+
85
+ from mdb_engine import MongoDBEngine
86
+
87
+ # Initialize engine (with optional Ray support)
88
+ engine = MongoDBEngine(
89
+ mongo_uri=os.getenv("MONGODB_URI", "mongodb://localhost:27017"),
90
+ db_name=os.getenv("MONGODB_DB", "mdb_runtime"),
91
+ enable_ray={enable_ray},
92
+ )
93
+
94
+ # Create FastAPI app with automatic lifecycle management
95
+ # Multi-site mode is auto-detected from manifest
96
+ app = engine.create_app(
97
+ slug="{slug}",
98
+ manifest=Path(__file__).parent / "manifest.json",
99
+ )
100
+
101
+
102
+ @app.get("/")
103
+ async def index():
104
+ """Root endpoint."""
105
+ # Get scoped database (token auto-retrieved)
106
+ db = engine.get_scoped_db("{slug}")
107
+ return {{"app": "{slug}", "status": "ok"}}
108
+
109
+
110
+ @app.get("/health")
111
+ async def health():
112
+ """Health check endpoint."""
113
+ return {{
114
+ "status": "healthy",
115
+ "app": "{slug}",
116
+ "ray_enabled": engine.has_ray,
117
+ }}
118
+
119
+
120
+ if __name__ == "__main__":
121
+ import uvicorn
122
+ uvicorn.run(app, host="0.0.0.0", port=8000)
123
+ '''
124
+
125
+ RAY_ACTOR_TEMPLATE = '''"""
126
+ Ray Actor for {app_name}.
127
+
128
+ Provides isolated processing with Ray.
129
+ Generated by MDB Engine CLI.
130
+ """
131
+
132
+ import logging
133
+ from typing import Any, Dict, Optional
134
+
135
+ from mdb_engine.core.ray_integration import (
136
+ RAY_AVAILABLE,
137
+ AppRayActor,
138
+ ray_actor_decorator,
139
+ )
140
+
141
+ logger = logging.getLogger(__name__)
142
+
143
+ if RAY_AVAILABLE:
144
+ @ray_actor_decorator(app_slug="{slug}", isolated=True)
145
+ class {actor_class_name}(AppRayActor):
146
+ """
147
+ Ray actor for {app_name}.
148
+
149
+ This actor runs in an isolated environment with its own
150
+ database connection and state.
151
+ """
152
+
153
+ async def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
154
+ """
155
+ Process data in isolated Ray environment.
156
+
157
+ Args:
158
+ data: Input data to process
159
+
160
+ Returns:
161
+ Processed result
162
+ """
163
+ db = await self.get_app_db()
164
+
165
+ # Example: Store and retrieve
166
+ # await db.items.insert_one(data)
167
+ # result = await db.items.find_one({{"_id": data.get("id")}})
168
+
169
+ return {{"status": "processed", "app": self.app_slug}}
170
+
171
+ async def get_stats(self) -> Dict[str, Any]:
172
+ """Get processing statistics."""
173
+ db = await self.get_app_db()
174
+ # count = await db.items.count_documents({{}})
175
+ return {{"app": self.app_slug, "count": 0}}
176
+ else:
177
+ # Fallback when Ray not available
178
+ class {actor_class_name}:
179
+ """Fallback actor class when Ray is not available."""
180
+
181
+ def __init__(self, *args, **kwargs):
182
+ logger.warning(
183
+ "Ray not available - {actor_class_name} running in fallback mode"
184
+ )
185
+
186
+ async def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
187
+ return {{"status": "fallback", "error": "Ray not available"}}
188
+ '''
189
+
190
+
191
+ # --- Generator Class ---
192
+
193
+
194
+ class AppGenerator:
195
+ """
196
+ Generate apps with Rails-like structure.
197
+
198
+ Supports:
199
+ - Single-app mode (default): Simple, direct MongoDBEngine usage
200
+ - Multi-site mode (opt-in): MongoDBEngine with token-based auth and cross-app access
201
+ - Ray support (opt-in): Isolated processing environments with Ray actors
202
+ """
203
+
204
+ @classmethod
205
+ def generate(
206
+ cls,
207
+ app_slug: str,
208
+ app_name: str,
209
+ description: str = "",
210
+ output_dir: Path = Path("."),
211
+ multi_site: bool = False,
212
+ enable_ray: bool = False,
213
+ read_scopes: Optional[List[str]] = None,
214
+ ) -> Path:
215
+ """
216
+ Generate a new app with proper structure.
217
+
218
+ Args:
219
+ app_slug: App identifier (e.g., "blog", "shop")
220
+ app_name: Human-readable app name
221
+ description: App description
222
+ output_dir: Directory to create app in
223
+ multi_site: Enable multi-site mode (default: False)
224
+ enable_ray: Enable Ray support (default: False)
225
+ read_scopes: Cross-app read scopes (default: [app_slug])
226
+
227
+ Returns:
228
+ Path to created app directory
229
+ """
230
+ app_path = output_dir / app_slug
231
+
232
+ if app_path.exists():
233
+ raise click.ClickException(f"App '{app_slug}' already exists at {app_path}")
234
+
235
+ # Create directory structure
236
+ app_path.mkdir(parents=True, exist_ok=True)
237
+ (app_path / "templates").mkdir(exist_ok=True)
238
+
239
+ if enable_ray:
240
+ (app_path / "actors").mkdir(exist_ok=True)
241
+
242
+ # Generate manifest.json
243
+ manifest = cls._generate_manifest(
244
+ app_slug=app_slug,
245
+ app_name=app_name,
246
+ description=description,
247
+ multi_site=multi_site,
248
+ enable_ray=enable_ray,
249
+ read_scopes=read_scopes or [app_slug],
250
+ )
251
+ manifest_path = app_path / "manifest.json"
252
+ with open(manifest_path, "w") as f:
253
+ json.dump(manifest, f, indent=2)
254
+
255
+ # Generate web.py
256
+ web_content = cls._generate_web_py(
257
+ app_slug=app_slug,
258
+ app_name=app_name,
259
+ multi_site=multi_site,
260
+ enable_ray=enable_ray,
261
+ )
262
+ (app_path / "web.py").write_text(web_content)
263
+
264
+ # Generate Ray actor if enabled
265
+ if enable_ray:
266
+ actor_content = cls._generate_ray_actor(
267
+ app_slug=app_slug,
268
+ app_name=app_name,
269
+ )
270
+ (app_path / "actors" / "__init__.py").write_text(actor_content)
271
+
272
+ # Generate basic index.html template
273
+ index_html = cls._generate_index_html(app_name)
274
+ (app_path / "templates" / "index.html").write_text(index_html)
275
+
276
+ return app_path
277
+
278
+ @classmethod
279
+ def _generate_manifest(
280
+ cls,
281
+ app_slug: str,
282
+ app_name: str,
283
+ description: str,
284
+ multi_site: bool,
285
+ enable_ray: bool,
286
+ read_scopes: List[str],
287
+ ) -> Dict[str, Any]:
288
+ """Generate manifest.json content."""
289
+ manifest: Dict[str, Any] = {
290
+ "schema_version": CURRENT_SCHEMA_VERSION,
291
+ "slug": app_slug,
292
+ "name": app_name,
293
+ "status": "active",
294
+ }
295
+
296
+ if description:
297
+ manifest["description"] = description
298
+
299
+ # Data access configuration
300
+ manifest["data_access"] = {
301
+ "read_scopes": read_scopes,
302
+ "write_scope": app_slug,
303
+ "cross_app_policy": "explicit" if multi_site else "none",
304
+ }
305
+
306
+ # Auth configuration (minimal by default)
307
+ manifest["auth"] = {
308
+ "policy": {
309
+ "provider": "casbin",
310
+ "required": False,
311
+ "allow_anonymous": True,
312
+ }
313
+ }
18
314
 
19
- @click.command()
315
+ # Managed indexes placeholder
316
+ manifest["managed_indexes"] = {}
317
+
318
+ return manifest
319
+
320
+ @classmethod
321
+ def _generate_web_py(
322
+ cls,
323
+ app_slug: str,
324
+ app_name: str,
325
+ multi_site: bool,
326
+ enable_ray: bool,
327
+ ) -> str:
328
+ """Generate web.py content."""
329
+ if multi_site:
330
+ return MULTI_SITE_WEB_TEMPLATE.format(
331
+ app_name=app_name,
332
+ slug=app_slug,
333
+ enable_ray=str(enable_ray),
334
+ )
335
+ else:
336
+ return SINGLE_APP_WEB_TEMPLATE.format(
337
+ app_name=app_name,
338
+ slug=app_slug,
339
+ )
340
+
341
+ @classmethod
342
+ def _generate_ray_actor(
343
+ cls,
344
+ app_slug: str,
345
+ app_name: str,
346
+ ) -> str:
347
+ """Generate Ray actor content."""
348
+ # Convert slug to class name: my_app -> MyAppActor
349
+ words = app_slug.replace("-", "_").split("_")
350
+ actor_class_name = "".join(word.capitalize() for word in words) + "Actor"
351
+
352
+ return RAY_ACTOR_TEMPLATE.format(
353
+ app_name=app_name,
354
+ slug=app_slug,
355
+ actor_class_name=actor_class_name,
356
+ )
357
+
358
+ @classmethod
359
+ def _generate_index_html(cls, app_name: str) -> str:
360
+ """Generate basic index.html template."""
361
+ return f"""<!DOCTYPE html>
362
+ <html lang="en">
363
+ <head>
364
+ <meta charset="UTF-8">
365
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
366
+ <title>{app_name}</title>
367
+ <style>
368
+ body {{
369
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
370
+ max-width: 800px;
371
+ margin: 0 auto;
372
+ padding: 2rem;
373
+ background: #f5f5f5;
374
+ }}
375
+ .container {{
376
+ background: white;
377
+ padding: 2rem;
378
+ border-radius: 8px;
379
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
380
+ }}
381
+ h1 {{
382
+ color: #333;
383
+ margin-top: 0;
384
+ }}
385
+ .status {{
386
+ display: inline-block;
387
+ padding: 0.25rem 0.75rem;
388
+ background: #e8f5e9;
389
+ color: #2e7d32;
390
+ border-radius: 4px;
391
+ font-size: 0.875rem;
392
+ }}
393
+ </style>
394
+ </head>
395
+ <body>
396
+ <div class="container">
397
+ <h1>{app_name}</h1>
398
+ <p class="status">Active</p>
399
+ <p>Welcome to your new MDB Engine app!</p>
400
+ </div>
401
+ </body>
402
+ </html>
403
+ """
404
+
405
+
406
+ # --- CLI Commands ---
407
+
408
+
409
+ @click.group()
410
+ def generate() -> None:
411
+ """
412
+ Generate manifests and app scaffolding.
413
+
414
+ Examples:
415
+ mdb generate manifest --slug my-app --name "My App"
416
+ mdb generate app --slug my-app --name "My App"
417
+ mdb generate app --slug my-app --multi-site --ray
418
+ """
419
+ pass
420
+
421
+
422
+ @generate.command("manifest")
20
423
  @click.option(
21
424
  "--slug",
22
425
  "-s",
@@ -48,7 +451,7 @@ from ..utils import save_manifest_file
48
451
  is_flag=True,
49
452
  help="Generate minimal template (basic fields only)",
50
453
  )
51
- def generate(
454
+ def generate_manifest(
52
455
  slug: str,
53
456
  name: str,
54
457
  description: str,
@@ -58,10 +461,12 @@ def generate(
58
461
  """
59
462
  Generate a template manifest.json file.
60
463
 
464
+ This is the original generate command behavior.
465
+
61
466
  Examples:
62
- mdb generate --slug my-app --name "My App"
63
- mdb generate --slug my-app --name "My App" --output custom.json
64
- mdb generate --slug my-app --name "My App" --minimal
467
+ mdb generate manifest --slug my-app --name "My App"
468
+ mdb generate manifest --slug my-app --name "My App" --output custom.json
469
+ mdb generate manifest --slug my-app --name "My App" --minimal
65
470
  """
66
471
  try:
67
472
  # Validate slug format
@@ -100,6 +505,137 @@ def generate(
100
505
  # Save manifest
101
506
  save_manifest_file(output, manifest)
102
507
  click.echo(click.style(f"✅ Generated manifest template: {output}", fg="green"))
103
- sys.exit(0)
508
+
509
+ except click.ClickException:
510
+ raise
511
+
512
+
513
+ @generate.command("app")
514
+ @click.option(
515
+ "--slug",
516
+ "-s",
517
+ prompt="App slug",
518
+ help="App slug (lowercase alphanumeric, underscores, hyphens)",
519
+ )
520
+ @click.option(
521
+ "--name",
522
+ "-n",
523
+ prompt="App name",
524
+ help="Human-readable app name",
525
+ )
526
+ @click.option(
527
+ "--description",
528
+ "-d",
529
+ default="",
530
+ help="App description",
531
+ )
532
+ @click.option(
533
+ "--output",
534
+ "-o",
535
+ default=".",
536
+ type=click.Path(path_type=Path),
537
+ help="Output directory (default: current directory)",
538
+ )
539
+ @click.option(
540
+ "--multi-site",
541
+ is_flag=True,
542
+ help="Enable multi-site mode (token-based auth and cross-app access)",
543
+ )
544
+ @click.option(
545
+ "--ray",
546
+ is_flag=True,
547
+ help="Enable Ray support for distributed processing",
548
+ )
549
+ @click.option(
550
+ "--read-scopes",
551
+ "-r",
552
+ multiple=True,
553
+ help="Cross-app read scopes (can specify multiple)",
554
+ )
555
+ def generate_app(
556
+ slug: str,
557
+ name: str,
558
+ description: str,
559
+ output: Path,
560
+ multi_site: bool,
561
+ ray: bool,
562
+ read_scopes: tuple,
563
+ ) -> None:
564
+ """
565
+ Generate a full app with proper structure.
566
+
567
+ Creates a complete app directory with:
568
+ - manifest.json
569
+ - web.py (FastAPI application)
570
+ - templates/ directory
571
+ - actors/ directory (if --ray enabled)
572
+
573
+ Examples:
574
+ # Single-app mode (default)
575
+ mdb generate app --slug my-app --name "My App"
576
+
577
+ # Multi-site mode
578
+ mdb generate app --slug my-app --name "My App" --multi-site
579
+
580
+ # With Ray support
581
+ mdb generate app --slug my-app --name "My App" --ray
582
+
583
+ # Full featured
584
+ mdb generate app --slug my-app --name "My App" --multi-site --ray
585
+
586
+ # With cross-app read access
587
+ mdb generate app --slug dashboard --name "Dashboard" --multi-site \\
588
+ --read-scopes dashboard --read-scopes click_tracker
589
+ """
590
+ try:
591
+ # Validate slug format
592
+ if not slug or not all(c.isalnum() or c in ("_", "-") for c in slug):
593
+ raise click.ClickException(
594
+ "Invalid slug format. Use lowercase alphanumeric, underscores, or hyphens."
595
+ )
596
+
597
+ # Convert read_scopes tuple to list
598
+ scopes_list = list(read_scopes) if read_scopes else None
599
+
600
+ # Generate app
601
+ app_path = AppGenerator.generate(
602
+ app_slug=slug,
603
+ app_name=name,
604
+ description=description,
605
+ output_dir=output,
606
+ multi_site=multi_site,
607
+ enable_ray=ray,
608
+ read_scopes=scopes_list,
609
+ )
610
+
611
+ # Print success message
612
+ click.echo(click.style(f"✅ Generated app '{name}' at: {app_path}", fg="green"))
613
+ click.echo("")
614
+ click.echo("Created files:")
615
+ click.echo(f" - {app_path}/manifest.json")
616
+ click.echo(f" - {app_path}/web.py")
617
+ click.echo(f" - {app_path}/templates/index.html")
618
+
619
+ if ray:
620
+ click.echo(f" - {app_path}/actors/__init__.py")
621
+
622
+ click.echo("")
623
+ click.echo("Next steps:")
624
+ click.echo(f" cd {app_path}")
625
+ click.echo(" uvicorn web:app --reload")
626
+
627
+ if multi_site:
628
+ click.echo("")
629
+ click.echo(click.style("Multi-site mode enabled:", fg="yellow"))
630
+ click.echo(f" Set {slug.upper()}_SECRET environment variable for app token")
631
+
632
+ if ray:
633
+ click.echo("")
634
+ click.echo(click.style("Ray support enabled:", fg="yellow"))
635
+ click.echo(" Ensure Ray is running: ray start --head")
636
+ click.echo(" Set RAY_ADDRESS environment variable")
637
+
104
638
  except click.ClickException:
105
639
  raise
640
+ except (OSError, ValueError, KeyError, RuntimeError) as e:
641
+ raise click.ClickException(f"Error generating app: {e}") from e
@@ -42,14 +42,10 @@ def validate(manifest_file: Path, verbose: bool) -> None:
42
42
  is_valid, error_message, error_paths = validator.validate(manifest)
43
43
 
44
44
  if is_valid:
45
- click.echo(
46
- click.style(f"✅ Manifest '{manifest_file}' is valid!", fg="green")
47
- )
45
+ click.echo(click.style(f"✅ Manifest '{manifest_file}' is valid!", fg="green"))
48
46
  sys.exit(0)
49
47
  else:
50
- click.echo(
51
- click.style(f"❌ Manifest '{manifest_file}' is invalid!", fg="red")
52
- )
48
+ click.echo(click.style(f"❌ Manifest '{manifest_file}' is invalid!", fg="red"))
53
49
  if error_message:
54
50
  click.echo(click.style(f"Error: {error_message}", fg="red"))
55
51
  if error_paths and verbose:
@@ -60,4 +56,4 @@ def validate(manifest_file: Path, verbose: bool) -> None:
60
56
  except click.ClickException:
61
57
  raise
62
58
  except (FileNotFoundError, ValueError, KeyError) as e:
63
- raise click.ClickException(str(e))
59
+ raise click.ClickException(str(e)) from e
mdb_engine/cli/utils.py CHANGED
@@ -30,10 +30,10 @@ def load_manifest_file(file_path: Path) -> Dict[str, Any]:
30
30
  raise click.ClickException(f"Manifest file not found: {file_path}")
31
31
 
32
32
  try:
33
- with open(file_path, "r", encoding="utf-8") as f:
33
+ with open(file_path, encoding="utf-8") as f:
34
34
  return json.load(f)
35
35
  except json.JSONDecodeError as e:
36
- raise click.ClickException(f"Invalid JSON in manifest file: {e}")
36
+ raise click.ClickException(f"Invalid JSON in manifest file: {e}") from e
37
37
 
38
38
 
39
39
  def save_manifest_file(file_path: Path, manifest: Dict[str, Any]) -> None:
@@ -51,7 +51,7 @@ def save_manifest_file(file_path: Path, manifest: Dict[str, Any]) -> None:
51
51
  with open(file_path, "w", encoding="utf-8") as f:
52
52
  json.dump(manifest, f, indent=2, ensure_ascii=False)
53
53
  except OSError as e:
54
- raise click.ClickException(f"Failed to write manifest file: {e}")
54
+ raise click.ClickException(f"Failed to write manifest file: {e}") from e
55
55
 
56
56
 
57
57
  def format_manifest_output(manifest: Dict[str, Any], format_type: str) -> str:
mdb_engine/config.py CHANGED
@@ -66,18 +66,12 @@ class EngineConfig:
66
66
  """
67
67
  self.mongo_uri = mongo_uri or os.getenv("MONGO_URI", "")
68
68
  self.db_name = db_name or os.getenv("DB_NAME", "")
69
- self.max_pool_size = max_pool_size or int(
70
- os.getenv("MONGO_MAX_POOL_SIZE", "50")
71
- )
72
- self.min_pool_size = min_pool_size or int(
73
- os.getenv("MONGO_MIN_POOL_SIZE", "10")
74
- )
69
+ self.max_pool_size = max_pool_size or int(os.getenv("MONGO_MAX_POOL_SIZE", "50"))
70
+ self.min_pool_size = min_pool_size or int(os.getenv("MONGO_MIN_POOL_SIZE", "10"))
75
71
  self.server_selection_timeout_ms = server_selection_timeout_ms or int(
76
72
  os.getenv("MONGO_SERVER_SELECTION_TIMEOUT_MS", "5000")
77
73
  )
78
- self.authz_cache_ttl = authz_cache_ttl or int(
79
- os.getenv("AUTHZ_CACHE_TTL", "300")
80
- )
74
+ self.authz_cache_ttl = authz_cache_ttl or int(os.getenv("AUTHZ_CACHE_TTL", "300"))
81
75
 
82
76
  def validate(self) -> None:
83
77
  """
@@ -115,9 +109,7 @@ class EngineConfig:
115
109
  )
116
110
 
117
111
  if self.authz_cache_ttl < 0:
118
- raise ValueError(
119
- f"authz_cache_ttl must be >= 0, got {self.authz_cache_ttl}"
120
- )
112
+ raise ValueError(f"authz_cache_ttl must be >= 0, got {self.authz_cache_ttl}")
121
113
 
122
114
 
123
115
  # Pydantic-based configuration (optional, only if Pydantic is available)
@@ -138,9 +130,7 @@ if PYDANTIC_AVAILABLE:
138
130
  )
139
131
  """
140
132
 
141
- mongo_uri: str = Field(
142
- ..., env="MONGO_URI", description="MongoDB connection URI"
143
- )
133
+ mongo_uri: str = Field(..., env="MONGO_URI", description="MongoDB connection URI")
144
134
  db_name: str = Field(..., env="DB_NAME", description="Database name")
145
135
  max_pool_size: int = Field(
146
136
  50,
@@ -203,15 +193,11 @@ ACCESS_TOKEN_TTL: int = int(os.getenv("ACCESS_TOKEN_TTL", "900")) # 15 minutes
203
193
  REFRESH_TOKEN_TTL: int = int(os.getenv("REFRESH_TOKEN_TTL", "604800")) # 7 days
204
194
  """Refresh token TTL in seconds (default: 604800 / 7 days)."""
205
195
 
206
- TOKEN_ROTATION_ENABLED: bool = (
207
- os.getenv("TOKEN_ROTATION_ENABLED", "true").lower() == "true"
208
- )
196
+ TOKEN_ROTATION_ENABLED: bool = os.getenv("TOKEN_ROTATION_ENABLED", "true").lower() == "true"
209
197
  """Whether to rotate refresh tokens on each use (default: true)."""
210
198
 
211
199
  MAX_SESSIONS_PER_USER: int = int(os.getenv("MAX_SESSIONS_PER_USER", "10"))
212
200
  """Maximum number of concurrent sessions per user (default: 10)."""
213
201
 
214
- SESSION_INACTIVITY_TIMEOUT: int = int(
215
- os.getenv("SESSION_INACTIVITY_TIMEOUT", "1800")
216
- ) # 30 minutes
202
+ SESSION_INACTIVITY_TIMEOUT: int = int(os.getenv("SESSION_INACTIVITY_TIMEOUT", "1800")) # 30 minutes
217
203
  """Session inactivity timeout in seconds (default: 1800 / 30 minutes)."""