mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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 +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,425 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate command for CLI.
|
|
3
3
|
|
|
4
|
-
Generates
|
|
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
|
|
18
|
+
import json
|
|
10
19
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
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: list[str] | None = 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
|
+
}
|
|
314
|
+
|
|
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 ---
|
|
18
407
|
|
|
19
|
-
|
|
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
|
|
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
|
|
@@ -71,7 +476,7 @@ def generate(
|
|
|
71
476
|
)
|
|
72
477
|
|
|
73
478
|
# Generate template manifest
|
|
74
|
-
manifest:
|
|
479
|
+
manifest: dict[str, Any] = {
|
|
75
480
|
"schema_version": CURRENT_SCHEMA_VERSION,
|
|
76
481
|
"slug": slug,
|
|
77
482
|
"name": name,
|
|
@@ -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
|
-
|
|
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
|
@@ -8,12 +8,12 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def load_manifest_file(file_path: Path) ->
|
|
16
|
+
def load_manifest_file(file_path: Path) -> dict[str, Any]:
|
|
17
17
|
"""
|
|
18
18
|
Load a manifest JSON file.
|
|
19
19
|
|
|
@@ -30,13 +30,13 @@ 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,
|
|
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
|
-
def save_manifest_file(file_path: Path, manifest:
|
|
39
|
+
def save_manifest_file(file_path: Path, manifest: dict[str, Any]) -> None:
|
|
40
40
|
"""
|
|
41
41
|
Save a manifest dictionary to a JSON file.
|
|
42
42
|
|
|
@@ -51,10 +51,10 @@ 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
|
-
def format_manifest_output(manifest:
|
|
57
|
+
def format_manifest_output(manifest: dict[str, Any], format_type: str) -> str:
|
|
58
58
|
"""
|
|
59
59
|
Format manifest for output.
|
|
60
60
|
|