framework-m-studio 0.2.2__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.
@@ -0,0 +1,16 @@
1
+ """Framework M Studio - Visual DocType Builder & Developer Tools.
2
+
3
+ This package provides development-time tools for Framework M:
4
+ - Visual DocType builder (Studio UI)
5
+ - LibCST-based code generators
6
+ - Extended CLI commands (codegen, docs)
7
+
8
+ This package is separated from framework-m core to keep the
9
+ runtime lightweight. Install as a dev dependency.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1,283 @@
1
+ """Framework M Studio - Litestar Application.
2
+
3
+ This module provides the Studio web application for visual DocType building.
4
+ It serves both the API endpoints and the React SPA.
5
+
6
+ Usage:
7
+ m studio # Starts on port 9000
8
+ m studio --port 9001 # Custom port
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from litestar import Litestar, Response, Router, get
17
+ from litestar.config.cors import CORSConfig
18
+ from litestar.response import File, Redirect
19
+ from litestar.static_files import create_static_files_router
20
+
21
+ from framework_m_studio.routes import DocTypeController
22
+
23
+ # Path to static files (built React app)
24
+ STATIC_DIR = Path(__file__).parent / "static"
25
+
26
+
27
+ # =============================================================================
28
+ # Studio API Routes (Health & Field Types)
29
+ # =============================================================================
30
+
31
+
32
+ @get(["/studio/api", "/studio/api/"], tags=["Studio"])
33
+ async def api_root() -> dict[str, Any]:
34
+ """API root endpoint with available endpoints."""
35
+ return {
36
+ "service": "framework-m-studio",
37
+ "version": "0.1.0",
38
+ "endpoints": {
39
+ "health": "/studio/api/health",
40
+ "field_types": "/studio/api/field-types",
41
+ "doctypes": "/studio/api/doctypes",
42
+ },
43
+ }
44
+
45
+
46
+ def _get_spa_response(path: str) -> Response[Any]:
47
+ """Helper to serve SPA files."""
48
+ if not STATIC_DIR.exists():
49
+ # Development mode: no built assets yet
50
+ return Response(
51
+ content={
52
+ "message": "Studio UI not built yet",
53
+ "hint": "Run: cd apps/studio/studio_ui && pnpm build",
54
+ "api_health": "/studio/api/health",
55
+ "api_doctypes": "/studio/api/doctypes",
56
+ "api_field_types": "/studio/api/field-types",
57
+ },
58
+ media_type="application/json",
59
+ )
60
+
61
+ # Check for actual file
62
+ file_path = STATIC_DIR / path
63
+ if file_path.is_file():
64
+ # Serve the actual file with proper content type detection
65
+ content = file_path.read_bytes()
66
+ # Let Litestar infer content type from file extension
67
+ from mimetypes import guess_type
68
+
69
+ content_type, _ = guess_type(str(file_path))
70
+ return Response(
71
+ content=content,
72
+ media_type=content_type or "application/octet-stream",
73
+ )
74
+
75
+ # Serve index.html for SPA routing (fallback for client-side routes)
76
+ index_path = STATIC_DIR / "index.html"
77
+ if index_path.exists():
78
+ content = index_path.read_bytes()
79
+ return Response(
80
+ content=content,
81
+ media_type="text/html; charset=utf-8",
82
+ headers={
83
+ "Cache-Control": "no-cache, no-store, must-revalidate",
84
+ "Pragma": "no-cache",
85
+ "Expires": "0",
86
+ },
87
+ )
88
+
89
+ return Response(
90
+ content={"error": "Studio UI not found"},
91
+ media_type="application/json",
92
+ status_code=404,
93
+ )
94
+
95
+
96
+ @get("/studio/ui/{path:path}", include_in_schema=False)
97
+ async def serve_spa(path: str) -> Response[Any]:
98
+ """Serve Studio SPA with client-side routing support under /studio/ui/*."""
99
+ return _get_spa_response(path)
100
+
101
+
102
+ @get(["/studio/ui", "/studio/ui/"], include_in_schema=False)
103
+ async def serve_studio_root() -> Response[Any]:
104
+ """Serve Studio root (index.html) at /studio/ui."""
105
+ return _get_spa_response("index.html")
106
+
107
+
108
+ @get(["/studio", "/studio/"], include_in_schema=False)
109
+ async def redirect_to_ui() -> Redirect:
110
+ """Redirect /studio to /studio/ui/ for convenience."""
111
+ return Redirect(path="/studio/ui/")
112
+
113
+
114
+ @get("/studio/api/health", tags=["Studio"])
115
+ async def health_check() -> dict[str, str]:
116
+ """Health check endpoint for Studio API."""
117
+ return {"status": "ok", "service": "framework-m-studio"}
118
+
119
+
120
+ @get("/studio/api/field-types", tags=["Studio"])
121
+ async def list_field_types() -> dict[str, Any]:
122
+ """List available field types.
123
+
124
+ Returns built-in types plus any registered by installed apps.
125
+ Uses FieldRegistry for dynamic discovery.
126
+ """
127
+ try:
128
+ from framework_m.adapters.db.field_registry import FieldRegistry
129
+
130
+ types_list = []
131
+ for type_info in FieldRegistry.get_instance().get_all_types():
132
+ types_list.append(
133
+ {
134
+ "name": type_info.name,
135
+ "pydantic_type": type_info.pydantic_type,
136
+ "label": type_info.label,
137
+ "ui_widget": type_info.ui_widget,
138
+ "category": type_info.category,
139
+ "validators": type_info.validators,
140
+ }
141
+ )
142
+ return {"field_types": types_list}
143
+ except ImportError:
144
+ # Fallback to static list if framework_m not available
145
+ return {
146
+ "field_types": [
147
+ {
148
+ "name": "str",
149
+ "pydantic_type": "str",
150
+ "label": "Text",
151
+ "ui_widget": "text",
152
+ "category": "text",
153
+ },
154
+ {
155
+ "name": "int",
156
+ "pydantic_type": "int",
157
+ "label": "Integer",
158
+ "ui_widget": "number",
159
+ "category": "number",
160
+ },
161
+ {
162
+ "name": "float",
163
+ "pydantic_type": "float",
164
+ "label": "Decimal",
165
+ "ui_widget": "number",
166
+ "category": "number",
167
+ },
168
+ {
169
+ "name": "bool",
170
+ "pydantic_type": "bool",
171
+ "label": "Checkbox",
172
+ "ui_widget": "checkbox",
173
+ "category": "boolean",
174
+ },
175
+ {
176
+ "name": "date",
177
+ "pydantic_type": "date",
178
+ "label": "Date",
179
+ "ui_widget": "date",
180
+ "category": "datetime",
181
+ },
182
+ {
183
+ "name": "datetime",
184
+ "pydantic_type": "datetime",
185
+ "label": "DateTime",
186
+ "ui_widget": "datetime",
187
+ "category": "datetime",
188
+ },
189
+ ]
190
+ }
191
+
192
+
193
+ # =============================================================================
194
+ # SPA Serving (Catch-all for React Router)
195
+ # =============================================================================
196
+
197
+
198
+ @get("/favicon.ico", include_in_schema=False)
199
+ async def serve_favicon() -> File | dict[str, str]:
200
+ """Serve favicon from static directory."""
201
+ favicon_path = STATIC_DIR / "favicon.ico"
202
+ if favicon_path.exists():
203
+ return File(path=favicon_path)
204
+ return {"error": "Favicon not found"}
205
+
206
+
207
+ @get("/", include_in_schema=False)
208
+ async def root_redirect() -> Redirect:
209
+ """Redirect root to /studio/."""
210
+ return Redirect(path="/studio/")
211
+
212
+
213
+ # =============================================================================
214
+ # Router and App Assembly
215
+ # =============================================================================
216
+
217
+ studio_api_router = Router(
218
+ path="/",
219
+ route_handlers=[
220
+ api_root,
221
+ health_check,
222
+ list_field_types,
223
+ ],
224
+ )
225
+
226
+
227
+ def create_app() -> Litestar:
228
+ """Create the Studio Litestar application."""
229
+ route_handlers: list[Any] = [
230
+ studio_api_router,
231
+ DocTypeController, # File System API for DocTypes
232
+ ]
233
+ # Always include favicon and redirects
234
+ route_handlers.extend(
235
+ [
236
+ serve_favicon,
237
+ root_redirect,
238
+ ]
239
+ )
240
+
241
+ # Create static files router for Studio SPA
242
+ # HashRouter is used on the frontend, so we only need to serve index.html at root
243
+ # and static assets.
244
+
245
+ # Static assets (js/css/images)
246
+ if STATIC_DIR.exists():
247
+ route_handlers.append(
248
+ create_static_files_router(
249
+ path="/studio/ui/assets",
250
+ directories=[STATIC_DIR / "assets"],
251
+ name="studio_assets",
252
+ html_mode=False,
253
+ )
254
+ )
255
+
256
+ # We mount the serve_studio_root handler which has path="/studio" and "/studio/"
257
+ route_handlers.append(serve_studio_root)
258
+ route_handlers.append(serve_spa)
259
+ route_handlers.append(redirect_to_ui)
260
+ # CORS config for development (frontend on different port)
261
+ cors_config = CORSConfig(
262
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
263
+ allow_methods=["*"],
264
+ allow_headers=["*"],
265
+ allow_credentials=True,
266
+ )
267
+
268
+ return Litestar(
269
+ route_handlers=route_handlers,
270
+ cors_config=cors_config,
271
+ debug=True,
272
+ openapi_config=None, # Studio doesn't need its own OpenAPI
273
+ )
274
+
275
+
276
+ # Application instance for uvicorn
277
+ app = create_app()
278
+
279
+
280
+ __all__ = [
281
+ "app",
282
+ "create_app",
283
+ ]
@@ -0,0 +1,247 @@
1
+ """Framework M Studio CLI Commands.
2
+
3
+ This module provides CLI commands that are registered via entry points
4
+ when framework-m-studio is installed. These extend the base `m` CLI
5
+ with developer tools.
6
+
7
+ Entry Point Registration (pyproject.toml):
8
+ [project.entry-points."framework_m.cli_commands"]
9
+ codegen = "framework_m_studio.cli:codegen_app"
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Annotated
15
+
16
+ import cyclopts
17
+
18
+ # =============================================================================
19
+ # Codegen Sub-App
20
+ # =============================================================================
21
+
22
+ codegen_app = cyclopts.App(
23
+ name="codegen",
24
+ help="Code generation tools for Framework M",
25
+ )
26
+
27
+
28
+ @codegen_app.command(name="client")
29
+ def codegen_client(
30
+ lang: Annotated[
31
+ str,
32
+ cyclopts.Parameter(
33
+ name="--lang",
34
+ help="Target language: ts (TypeScript) or py (Python)",
35
+ ),
36
+ ] = "ts",
37
+ out: Annotated[
38
+ str,
39
+ cyclopts.Parameter(
40
+ name="--out",
41
+ help="Output directory for generated code",
42
+ ),
43
+ ] = "./generated",
44
+ openapi_url: Annotated[
45
+ str,
46
+ cyclopts.Parameter(
47
+ name="--openapi-url",
48
+ help="URL to fetch OpenAPI schema from",
49
+ ),
50
+ ] = "http://localhost:8000/schema/openapi.json",
51
+ ) -> None:
52
+ """Generate API client from OpenAPI schema.
53
+
54
+ Examples:
55
+ m codegen client --lang ts --out ./frontend/src/api
56
+ m codegen client --lang py --out ./scripts/api_client
57
+ """
58
+ from pathlib import Path
59
+
60
+ from framework_m_studio.sdk_generator import (
61
+ fetch_openapi_schema,
62
+ generate_typescript_client,
63
+ generate_typescript_types,
64
+ )
65
+
66
+ print(f"Generating {lang.upper()} client...")
67
+ print(f" OpenAPI URL: {openapi_url}")
68
+ print(f" Output: {out}")
69
+
70
+ # Create output directory
71
+ output_path = Path(out)
72
+ output_path.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Fetch schema and generate
75
+ schema = fetch_openapi_schema(openapi_url)
76
+ if lang.lower() == "ts":
77
+ types_code = generate_typescript_types(schema)
78
+ client_code = generate_typescript_client(schema)
79
+ (output_path / "types.ts").write_text(types_code)
80
+ (output_path / "client.ts").write_text(client_code)
81
+
82
+
83
+ @codegen_app.command(name="doctype")
84
+ def codegen_doctype(
85
+ name: Annotated[
86
+ str,
87
+ cyclopts.Parameter(help="DocType class name (PascalCase)"),
88
+ ],
89
+ app: Annotated[
90
+ str | None,
91
+ cyclopts.Parameter(
92
+ name="--app",
93
+ help="Target app directory",
94
+ ),
95
+ ] = None,
96
+ ) -> None:
97
+ """Generate DocType Python code from schema.
98
+
99
+ This is the programmatic version of the Studio UI's
100
+ DocType builder. Useful for CI/CD pipelines.
101
+
102
+ Examples:
103
+ m codegen doctype Invoice --app apps/billing
104
+ """
105
+ print(f"Generating DocType: {name}")
106
+ if app:
107
+ print(f" Target app: {app}")
108
+ print()
109
+ print("⚠️ Not yet implemented. Coming in Phase 07.")
110
+ print(" Will use LibCST for code generation")
111
+
112
+
113
+ # =============================================================================
114
+ # Docs Sub-App (Optional - registered via separate entry point if needed)
115
+ # =============================================================================
116
+
117
+ docs_app = cyclopts.App(
118
+ name="docs",
119
+ help="Documentation generation tools",
120
+ )
121
+
122
+
123
+ @docs_app.command(name="generate")
124
+ def docs_generate(
125
+ output: Annotated[
126
+ str,
127
+ cyclopts.Parameter(
128
+ name="--output",
129
+ help="Output directory for documentation",
130
+ ),
131
+ ] = "./docs/api",
132
+ openapi_url: Annotated[
133
+ str | None,
134
+ cyclopts.Parameter(
135
+ name="--openapi-url",
136
+ help="URL to fetch OpenAPI schema from (optional)",
137
+ ),
138
+ ] = None,
139
+ ) -> None:
140
+ """Generate API documentation from DocTypes.
141
+
142
+ Examples:
143
+ m docs generate --output ./docs/api
144
+ """
145
+ from pathlib import Path
146
+
147
+ from framework_m_studio.docs_generator import run_docs_generate
148
+
149
+ # Use current working directory as project root
150
+ project_root = Path.cwd()
151
+
152
+ # Look in src/doctypes if it exists, otherwise use project root
153
+ doctypes_dir = project_root / "src" / "doctypes"
154
+ scan_root = project_root / "src" if doctypes_dir.exists() else project_root
155
+
156
+ run_docs_generate(
157
+ output=output,
158
+ project_root=str(scan_root),
159
+ openapi_url=openapi_url,
160
+ )
161
+
162
+
163
+ # =============================================================================
164
+ # Studio Sub-App (Main command to start Studio server)
165
+ # =============================================================================
166
+
167
+ studio_app = cyclopts.App(
168
+ name="studio",
169
+ help="Start Framework M Studio visual editor",
170
+ )
171
+
172
+
173
+ @studio_app.default
174
+ def studio_serve(
175
+ port: Annotated[
176
+ int,
177
+ cyclopts.Parameter(
178
+ name="--port",
179
+ help="Port to run Studio on",
180
+ ),
181
+ ] = 9000,
182
+ host: Annotated[
183
+ str,
184
+ cyclopts.Parameter(
185
+ name="--host",
186
+ help="Host to bind to",
187
+ ),
188
+ ] = "127.0.0.1",
189
+ reload: Annotated[
190
+ bool,
191
+ cyclopts.Parameter(
192
+ name="--reload",
193
+ help="Enable auto-reload for development",
194
+ ),
195
+ ] = False,
196
+ cloud: Annotated[
197
+ bool,
198
+ cyclopts.Parameter(
199
+ name="--cloud",
200
+ help="Enable cloud mode (Git-backed workspaces)",
201
+ ),
202
+ ] = False,
203
+ ) -> None:
204
+ """Start Framework M Studio.
205
+
206
+ Examples:
207
+ m studio # Start on port 9000
208
+ m studio --port 8000 # Custom port
209
+ m studio --reload # Development mode
210
+ m studio --cloud # Enable cloud mode
211
+ """
212
+ import os
213
+
214
+ import uvicorn
215
+
216
+ # Print startup banner
217
+ print()
218
+ print("🎨 Starting Framework M Studio")
219
+ print(f" ➜ Local: http://{host}:{port}/studio/")
220
+ print(f" ➜ API: http://{host}:{port}/studio/api/")
221
+ print(f" 🔌 API Health: http://{host}:{port}/studio/api/health")
222
+ print()
223
+
224
+ if cloud:
225
+ print("☁️ Cloud mode enabled - Git-backed workspaces")
226
+ os.environ["STUDIO_CLOUD_MODE"] = "1"
227
+ print()
228
+
229
+ # Start uvicorn
230
+ uvicorn.run(
231
+ "framework_m_studio.app:app",
232
+ host=host,
233
+ port=port,
234
+ reload=reload,
235
+ log_level="info",
236
+ )
237
+
238
+
239
+ __all__ = [
240
+ "codegen_app",
241
+ "codegen_client",
242
+ "codegen_doctype",
243
+ "docs_app",
244
+ "docs_generate",
245
+ "studio_app",
246
+ "studio_serve",
247
+ ]
@@ -0,0 +1,34 @@
1
+ """Code generation package for Framework M Studio.
2
+
3
+ This package provides:
4
+ - LibCST-based parsing for existing DocType files
5
+ - Jinja2-based code generation for new files
6
+ - Transformers for modifying existing files while preserving formatting
7
+ - Test generators for DocTypes
8
+ """
9
+
10
+ from framework_m_studio.codegen.generator import (
11
+ generate_doctype_source,
12
+ update_doctype_source,
13
+ )
14
+ from framework_m_studio.codegen.parser import (
15
+ ConfigSchema,
16
+ DocTypeSchema,
17
+ FieldSchema,
18
+ parse_doctype,
19
+ )
20
+ from framework_m_studio.codegen.test_generator import (
21
+ generate_test,
22
+ generate_test_file,
23
+ )
24
+
25
+ __all__ = [
26
+ "ConfigSchema",
27
+ "DocTypeSchema",
28
+ "FieldSchema",
29
+ "generate_doctype_source",
30
+ "generate_test",
31
+ "generate_test_file",
32
+ "parse_doctype",
33
+ "update_doctype_source",
34
+ ]