framework-m-studio 0.2.3__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.
- framework_m_studio/__init__.py +6 -1
- framework_m_studio/app.py +56 -11
- framework_m_studio/checklist_parser.py +421 -0
- framework_m_studio/cli/__init__.py +752 -0
- framework_m_studio/cli/build.py +421 -0
- framework_m_studio/cli/dev.py +214 -0
- framework_m_studio/cli/new.py +754 -0
- framework_m_studio/cli/quality.py +157 -0
- framework_m_studio/cli/studio.py +159 -0
- framework_m_studio/cli/utility.py +50 -0
- framework_m_studio/codegen/generator.py +6 -2
- framework_m_studio/codegen/parser.py +101 -4
- framework_m_studio/codegen/templates/doctype.py.jinja2 +19 -10
- framework_m_studio/codegen/test_generator.py +6 -2
- framework_m_studio/discovery.py +15 -5
- framework_m_studio/docs_generator.py +298 -2
- framework_m_studio/protocol_scanner.py +435 -0
- framework_m_studio/routes.py +39 -11
- {framework_m_studio-0.2.3.dist-info → framework_m_studio-0.3.0.dist-info}/METADATA +7 -2
- framework_m_studio-0.3.0.dist-info/RECORD +32 -0
- framework_m_studio-0.3.0.dist-info/entry_points.txt +18 -0
- framework_m_studio/cli.py +0 -247
- framework_m_studio/static/assets/index-BJ5Noua8.js +0 -171
- framework_m_studio/static/assets/index-CnPUX2YK.css +0 -1
- framework_m_studio/static/favicon.ico +0 -0
- framework_m_studio/static/index.html +0 -40
- framework_m_studio-0.2.3.dist-info/RECORD +0 -28
- framework_m_studio-0.2.3.dist-info/entry_points.txt +0 -4
- {framework_m_studio-0.2.3.dist-info → framework_m_studio-0.3.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Protocol Scanner - Extract Protocol definitions for documentation.
|
|
2
|
+
|
|
3
|
+
This module scans Python files for Protocol class definitions and extracts
|
|
4
|
+
their method signatures, docstrings, and adapter implementations.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Parse Protocol classes using LibCST
|
|
8
|
+
- Extract method signatures and docstrings
|
|
9
|
+
- Parse adapter names from docstrings
|
|
10
|
+
- Generate markdown documentation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import libcst as cst
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Data Models
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MethodInfo:
|
|
29
|
+
"""Parsed method information from a Protocol class."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
signature: str
|
|
33
|
+
docstring: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ProtocolInfo:
|
|
38
|
+
"""Parsed Protocol information."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
file_path: str
|
|
42
|
+
docstring: str | None = None
|
|
43
|
+
methods: list[MethodInfo] = field(default_factory=list)
|
|
44
|
+
adapters: list[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =============================================================================
|
|
48
|
+
# LibCST Visitor for Protocol Parsing
|
|
49
|
+
# =============================================================================
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProtocolParserVisitor(cst.CSTVisitor):
|
|
53
|
+
"""LibCST visitor to extract Protocol information."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.protocols: list[ProtocolInfo] = []
|
|
57
|
+
self._current_class: str | None = None
|
|
58
|
+
self._current_docstring: str | None = None
|
|
59
|
+
self._current_methods: list[MethodInfo] = []
|
|
60
|
+
self._is_protocol: bool = False
|
|
61
|
+
|
|
62
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool:
|
|
63
|
+
"""Visit class definition."""
|
|
64
|
+
class_name = node.name.value
|
|
65
|
+
|
|
66
|
+
# Check if this is a Protocol class
|
|
67
|
+
is_protocol = False
|
|
68
|
+
for arg in node.bases:
|
|
69
|
+
base_name = _get_base_name(arg)
|
|
70
|
+
if "Protocol" in base_name:
|
|
71
|
+
is_protocol = True
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if is_protocol:
|
|
75
|
+
self._current_class = class_name
|
|
76
|
+
self._is_protocol = True
|
|
77
|
+
self._current_methods = []
|
|
78
|
+
self._current_docstring = _extract_docstring(node)
|
|
79
|
+
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def leave_ClassDef(self, node: cst.ClassDef) -> None:
|
|
83
|
+
"""Leave class definition."""
|
|
84
|
+
if self._current_class == node.name.value and self._is_protocol:
|
|
85
|
+
# Extract adapters from docstring
|
|
86
|
+
adapters = _extract_adapters(self._current_docstring)
|
|
87
|
+
|
|
88
|
+
protocol = ProtocolInfo(
|
|
89
|
+
name=self._current_class,
|
|
90
|
+
file_path="",
|
|
91
|
+
docstring=self._current_docstring,
|
|
92
|
+
methods=self._current_methods,
|
|
93
|
+
adapters=adapters,
|
|
94
|
+
)
|
|
95
|
+
self.protocols.append(protocol)
|
|
96
|
+
|
|
97
|
+
self._current_class = None
|
|
98
|
+
self._is_protocol = False
|
|
99
|
+
self._current_methods = []
|
|
100
|
+
self._current_docstring = None
|
|
101
|
+
|
|
102
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
|
|
103
|
+
"""Visit function/method definition."""
|
|
104
|
+
if self._is_protocol and self._current_class:
|
|
105
|
+
method_name = node.name.value
|
|
106
|
+
if not method_name.startswith("_"):
|
|
107
|
+
# Get full signature
|
|
108
|
+
signature = _build_signature(node)
|
|
109
|
+
method_docstring = _extract_function_docstring(node)
|
|
110
|
+
|
|
111
|
+
method = MethodInfo(
|
|
112
|
+
name=method_name,
|
|
113
|
+
signature=signature,
|
|
114
|
+
docstring=method_docstring,
|
|
115
|
+
)
|
|
116
|
+
self._current_methods.append(method)
|
|
117
|
+
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# Helper Functions
|
|
123
|
+
# =============================================================================
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _get_base_name(arg: cst.Arg) -> str:
|
|
127
|
+
"""Get base class name from Arg node."""
|
|
128
|
+
if isinstance(arg.value, cst.Name):
|
|
129
|
+
return arg.value.value
|
|
130
|
+
if isinstance(arg.value, cst.Subscript) and isinstance(arg.value.value, cst.Name):
|
|
131
|
+
return arg.value.value.value
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _extract_docstring(node: cst.ClassDef) -> str | None:
|
|
136
|
+
"""Extract docstring from class definition."""
|
|
137
|
+
if not node.body or not node.body.body:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
first_stmt = node.body.body[0]
|
|
141
|
+
if not isinstance(first_stmt, cst.SimpleStatementLine):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
if not first_stmt.body or not isinstance(first_stmt.body[0], cst.Expr):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
expr = first_stmt.body[0].value
|
|
148
|
+
if isinstance(expr, cst.SimpleString):
|
|
149
|
+
value = expr.value
|
|
150
|
+
if value.startswith('"""') or value.startswith("'''"):
|
|
151
|
+
return value[3:-3].strip()
|
|
152
|
+
elif value.startswith('"') or value.startswith("'"):
|
|
153
|
+
return value[1:-1].strip()
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _extract_function_docstring(node: cst.FunctionDef) -> str | None:
|
|
159
|
+
"""Extract docstring from function definition."""
|
|
160
|
+
if not node.body or not node.body.body:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
first_stmt = node.body.body[0]
|
|
164
|
+
if not isinstance(first_stmt, cst.SimpleStatementLine):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if not first_stmt.body or not isinstance(first_stmt.body[0], cst.Expr):
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
expr = first_stmt.body[0].value
|
|
171
|
+
if isinstance(expr, cst.SimpleString):
|
|
172
|
+
value = expr.value
|
|
173
|
+
if value.startswith('"""') or value.startswith("'''"):
|
|
174
|
+
return value[3:-3].strip()
|
|
175
|
+
elif value.startswith('"') or value.startswith("'"):
|
|
176
|
+
return value[1:-1].strip()
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _build_signature(node: cst.FunctionDef) -> str:
|
|
182
|
+
"""Build method signature string."""
|
|
183
|
+
# Get async prefix
|
|
184
|
+
is_async = node.asynchronous is not None
|
|
185
|
+
prefix = "async def " if is_async else "def "
|
|
186
|
+
|
|
187
|
+
# Get function name and parameters
|
|
188
|
+
name = node.name.value
|
|
189
|
+
params = cst.Module(body=[]).code_for_node(node.params)
|
|
190
|
+
|
|
191
|
+
# Get return type if available
|
|
192
|
+
return_type = ""
|
|
193
|
+
if node.returns:
|
|
194
|
+
return_type = " -> " + cst.Module(body=[]).code_for_node(
|
|
195
|
+
node.returns.annotation
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return f"{prefix}{name}({params}){return_type}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _extract_adapters(docstring: str | None) -> list[str]:
|
|
202
|
+
"""Extract adapter names from Protocol docstring.
|
|
203
|
+
|
|
204
|
+
Looks for patterns like:
|
|
205
|
+
- Implementations:
|
|
206
|
+
- SomeAdapter: Description
|
|
207
|
+
"""
|
|
208
|
+
if not docstring:
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
adapters: list[str] = []
|
|
212
|
+
|
|
213
|
+
# Look for adapter names after "Implementations:" line
|
|
214
|
+
in_implementations = False
|
|
215
|
+
for line in docstring.split("\n"):
|
|
216
|
+
line = line.strip()
|
|
217
|
+
|
|
218
|
+
if "Implementations:" in line or "Implementation:" in line:
|
|
219
|
+
in_implementations = True
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if in_implementations:
|
|
223
|
+
# Check for "- AdapterName: description" pattern
|
|
224
|
+
match = re.match(r"-\s*(\w+)(?:Adapter|Cache|Repository)?:", line)
|
|
225
|
+
if match:
|
|
226
|
+
# Extract the full adapter name
|
|
227
|
+
adapter_match = re.match(r"-\s*(\w+):", line)
|
|
228
|
+
if adapter_match:
|
|
229
|
+
adapters.append(adapter_match.group(1))
|
|
230
|
+
elif line.startswith("-"):
|
|
231
|
+
# Simple "- AdapterName" pattern
|
|
232
|
+
adapter_match = re.match(r"-\s*(\w+)", line)
|
|
233
|
+
if adapter_match:
|
|
234
|
+
adapters.append(adapter_match.group(1))
|
|
235
|
+
elif not line:
|
|
236
|
+
# Empty line may end the list
|
|
237
|
+
if adapters:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
return adapters
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# =============================================================================
|
|
244
|
+
# Public API
|
|
245
|
+
# =============================================================================
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_protocol_file(file_path: Path) -> list[dict[str, Any]]:
|
|
249
|
+
"""Parse a Python file and extract Protocol definitions.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
file_path: Path to the Python file
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of protocol info dictionaries
|
|
256
|
+
"""
|
|
257
|
+
source = file_path.read_text(encoding="utf-8")
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
tree = cst.parse_module(source)
|
|
261
|
+
except cst.ParserSyntaxError:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
visitor = ProtocolParserVisitor()
|
|
265
|
+
tree.visit(visitor)
|
|
266
|
+
|
|
267
|
+
result = []
|
|
268
|
+
for proto in visitor.protocols:
|
|
269
|
+
proto.file_path = str(file_path.absolute())
|
|
270
|
+
result.append(protocol_to_dict(proto))
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def scan_protocols(interfaces_dir: Path) -> list[dict[str, Any]]:
|
|
276
|
+
"""Scan a directory for Protocol definitions.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
interfaces_dir: Path to interfaces directory
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List of all protocol info dictionaries
|
|
283
|
+
"""
|
|
284
|
+
protocols: list[dict[str, Any]] = []
|
|
285
|
+
|
|
286
|
+
for py_file in interfaces_dir.glob("*.py"):
|
|
287
|
+
if py_file.name.startswith("_"):
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
file_protocols = parse_protocol_file(py_file)
|
|
291
|
+
protocols.extend(file_protocols)
|
|
292
|
+
|
|
293
|
+
return protocols
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def generate_protocol_markdown(protocol_info: dict[str, Any]) -> str:
|
|
297
|
+
"""Generate markdown documentation for a Protocol.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
protocol_info: Protocol information dictionary
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Markdown string
|
|
304
|
+
"""
|
|
305
|
+
from pathlib import Path
|
|
306
|
+
|
|
307
|
+
name = protocol_info.get("name", "Unknown")
|
|
308
|
+
docstring = protocol_info.get("docstring", "")
|
|
309
|
+
methods = protocol_info.get("methods", [])
|
|
310
|
+
adapters = protocol_info.get("adapters", [])
|
|
311
|
+
file_path = protocol_info.get("file_path", "")
|
|
312
|
+
|
|
313
|
+
lines = [f"# {name}", ""]
|
|
314
|
+
|
|
315
|
+
if docstring:
|
|
316
|
+
# Clean up the docstring - remove the "Implementations:" section
|
|
317
|
+
clean_docstring = _clean_docstring_for_markdown(docstring)
|
|
318
|
+
if clean_docstring:
|
|
319
|
+
lines.extend([clean_docstring, ""])
|
|
320
|
+
|
|
321
|
+
# Source file link
|
|
322
|
+
if file_path:
|
|
323
|
+
filename = Path(file_path).name
|
|
324
|
+
file_uri = f"file://{file_path}"
|
|
325
|
+
lines.extend([f"**Source**: [{filename}]({file_uri})", ""])
|
|
326
|
+
|
|
327
|
+
# Methods section
|
|
328
|
+
if methods:
|
|
329
|
+
lines.extend(["## Methods", ""])
|
|
330
|
+
for method in methods:
|
|
331
|
+
lines.append(f"### `{method['name']}`")
|
|
332
|
+
lines.append("")
|
|
333
|
+
lines.append("```python")
|
|
334
|
+
lines.append(method["signature"])
|
|
335
|
+
lines.append("```")
|
|
336
|
+
lines.append("")
|
|
337
|
+
if method.get("docstring"):
|
|
338
|
+
lines.extend([method["docstring"], ""])
|
|
339
|
+
|
|
340
|
+
# Adapters section
|
|
341
|
+
if adapters:
|
|
342
|
+
lines.extend(["## Adapters", ""])
|
|
343
|
+
for adapter in adapters:
|
|
344
|
+
lines.append(f"- `{adapter}`")
|
|
345
|
+
lines.append("")
|
|
346
|
+
|
|
347
|
+
return "\n".join(lines)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _clean_docstring_for_markdown(docstring: str) -> str:
|
|
351
|
+
"""Remove Implementations section from docstring for markdown."""
|
|
352
|
+
lines = docstring.split("\n")
|
|
353
|
+
result_lines = []
|
|
354
|
+
skip_until_empty = False
|
|
355
|
+
|
|
356
|
+
for line in lines:
|
|
357
|
+
if "Implementations:" in line or "Implementation:" in line:
|
|
358
|
+
skip_until_empty = True
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
if skip_until_empty:
|
|
362
|
+
if line.strip().startswith("-"):
|
|
363
|
+
continue
|
|
364
|
+
if not line.strip():
|
|
365
|
+
skip_until_empty = False
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
result_lines.append(line)
|
|
369
|
+
|
|
370
|
+
return "\n".join(result_lines).strip()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def protocol_to_dict(protocol: ProtocolInfo) -> dict[str, Any]:
|
|
374
|
+
"""Convert ProtocolInfo to dictionary."""
|
|
375
|
+
return {
|
|
376
|
+
"name": protocol.name,
|
|
377
|
+
"file_path": protocol.file_path,
|
|
378
|
+
"docstring": protocol.docstring,
|
|
379
|
+
"methods": [
|
|
380
|
+
{
|
|
381
|
+
"name": m.name,
|
|
382
|
+
"signature": m.signature,
|
|
383
|
+
"docstring": m.docstring,
|
|
384
|
+
}
|
|
385
|
+
for m in protocol.methods
|
|
386
|
+
],
|
|
387
|
+
"adapters": protocol.adapters,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def generate_comparison_table(
|
|
392
|
+
protocols: list[dict[str, Any]],
|
|
393
|
+
indie_adapters: set[str],
|
|
394
|
+
) -> str:
|
|
395
|
+
"""Generate Indie vs Enterprise comparison table.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
protocols: List of protocol info dicts
|
|
399
|
+
indie_adapters: Set of adapter names considered "Indie" (simple, local)
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Markdown table string
|
|
403
|
+
"""
|
|
404
|
+
lines = [
|
|
405
|
+
"# Protocol Adapter Comparison",
|
|
406
|
+
"",
|
|
407
|
+
"| Protocol | Indie | Enterprise |",
|
|
408
|
+
"|----------|-------|------------|",
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
for proto in sorted(protocols, key=lambda p: p["name"]):
|
|
412
|
+
name = proto["name"]
|
|
413
|
+
adapters = proto.get("adapters", [])
|
|
414
|
+
|
|
415
|
+
indie_list = [a for a in adapters if a in indie_adapters]
|
|
416
|
+
enterprise_list = [a for a in adapters if a not in indie_adapters]
|
|
417
|
+
|
|
418
|
+
indie_str = ", ".join(f"`{a}`" for a in indie_list) or "-"
|
|
419
|
+
enterprise_str = ", ".join(f"`{a}`" for a in enterprise_list) or "-"
|
|
420
|
+
|
|
421
|
+
lines.append(f"| {name} | {indie_str} | {enterprise_str} |")
|
|
422
|
+
|
|
423
|
+
lines.append("")
|
|
424
|
+
return "\n".join(lines)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
__all__ = [
|
|
428
|
+
"MethodInfo",
|
|
429
|
+
"ProtocolInfo",
|
|
430
|
+
"generate_comparison_table",
|
|
431
|
+
"generate_protocol_markdown",
|
|
432
|
+
"parse_protocol_file",
|
|
433
|
+
"protocol_to_dict",
|
|
434
|
+
"scan_protocols",
|
|
435
|
+
]
|
framework_m_studio/routes.py
CHANGED
|
@@ -47,6 +47,10 @@ class FieldSchema(BaseModel):
|
|
|
47
47
|
description: str | None = None
|
|
48
48
|
hidden: bool = False
|
|
49
49
|
read_only: bool = False
|
|
50
|
+
link_doctype: str | None = None # For Link field type - which DocType to link to
|
|
51
|
+
table_doctype: str | None = (
|
|
52
|
+
None # For Table field type - which DocType for child table
|
|
53
|
+
)
|
|
50
54
|
validators: ValidatorSchema | None = None
|
|
51
55
|
|
|
52
56
|
|
|
@@ -193,6 +197,8 @@ class DocTypeController(Controller):
|
|
|
193
197
|
"required": f.required,
|
|
194
198
|
"label": f.label,
|
|
195
199
|
"description": f.description,
|
|
200
|
+
"link_doctype": f.link_doctype, # Include Link field metadata
|
|
201
|
+
"table_doctype": f.table_doctype, # Include Table field metadata
|
|
196
202
|
"validators": clean_validators(f.validators),
|
|
197
203
|
}
|
|
198
204
|
for f in doctype.fields
|
|
@@ -255,19 +261,37 @@ class DocTypeController(Controller):
|
|
|
255
261
|
old_controller.unlink()
|
|
256
262
|
break
|
|
257
263
|
|
|
258
|
-
# Determine target directory
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
# Determine target directory - use namespaced structure: src/<app>/doctypes/
|
|
265
|
+
app_name = data.app
|
|
266
|
+
|
|
267
|
+
# Try to detect app name from pyproject.toml if not provided
|
|
268
|
+
if not app_name:
|
|
269
|
+
pyproject = project_root / "pyproject.toml"
|
|
270
|
+
if pyproject.exists():
|
|
271
|
+
import re
|
|
272
|
+
|
|
273
|
+
content = pyproject.read_text()
|
|
274
|
+
match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', content)
|
|
275
|
+
if match:
|
|
276
|
+
app_name = match.group(1).replace("-", "_")
|
|
277
|
+
|
|
278
|
+
if app_name:
|
|
279
|
+
# Namespaced structure: src/<app>/doctypes/<doctype>/
|
|
280
|
+
namespace_dir = project_root / "src" / app_name / "doctypes"
|
|
281
|
+
if (
|
|
282
|
+
namespace_dir.exists()
|
|
283
|
+
or not (project_root / "src" / "doctypes").exists()
|
|
284
|
+
):
|
|
285
|
+
target_dir = project_root / "src" / app_name
|
|
286
|
+
else:
|
|
287
|
+
# Fallback to legacy flat structure if it exists
|
|
288
|
+
target_dir = project_root / "src"
|
|
264
289
|
else:
|
|
265
|
-
# Default to src/
|
|
266
290
|
target_dir = project_root / "src"
|
|
267
291
|
if not target_dir.exists():
|
|
268
292
|
target_dir = project_root
|
|
269
293
|
|
|
270
|
-
# Create folder structure: src
|
|
294
|
+
# Create folder structure: src/<app>/doctypes/<name>/ (namespaced)
|
|
271
295
|
doctype_folder = target_dir / "doctypes" / sanitized_folder_name
|
|
272
296
|
doctype_folder.mkdir(parents=True, exist_ok=True)
|
|
273
297
|
|
|
@@ -283,9 +307,11 @@ class DocTypeController(Controller):
|
|
|
283
307
|
init_code = _generate_init_code(sanitized_class_name)
|
|
284
308
|
(doctype_folder / "__init__.py").write_text(init_code, encoding="utf-8")
|
|
285
309
|
|
|
286
|
-
# Generate and write test file
|
|
310
|
+
# Generate and write test file in separate tests/ directory
|
|
287
311
|
test_code = _generate_test_code(sanitized_class_name, sanitized_folder_name)
|
|
288
|
-
|
|
312
|
+
tests_dir = project_root / "tests" / "doctypes" / sanitized_folder_name
|
|
313
|
+
tests_dir.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
(tests_dir / f"test_{sanitized_folder_name}.py").write_text(
|
|
289
315
|
test_code, encoding="utf-8"
|
|
290
316
|
)
|
|
291
317
|
|
|
@@ -399,6 +425,8 @@ def _generate_doctype_code(schema: CreateDocTypeRequest) -> str:
|
|
|
399
425
|
"label": f.label,
|
|
400
426
|
"default": f.default,
|
|
401
427
|
"description": f.description,
|
|
428
|
+
"link_doctype": f.link_doctype, # Preserve Link field metadata
|
|
429
|
+
"table_doctype": f.table_doctype, # Preserve Table field metadata
|
|
402
430
|
"validators": (
|
|
403
431
|
{
|
|
404
432
|
"min_length": f.validators.min_length,
|
|
@@ -432,7 +460,7 @@ def _generate_controller_code(name: str) -> str:
|
|
|
432
460
|
"",
|
|
433
461
|
"from __future__ import annotations",
|
|
434
462
|
"",
|
|
435
|
-
"from
|
|
463
|
+
"from framework_m_core.domain.base_controller import BaseController",
|
|
436
464
|
f"from .doctype import {name}",
|
|
437
465
|
"",
|
|
438
466
|
"",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framework-m-studio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Framework M Studio - Visual DocType Builder & Developer Tools
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/castlecraft/framework-m
|
|
6
6
|
Project-URL: Documentation, https://gitlab.com/castlecraft/framework-m#readme
|
|
@@ -17,7 +17,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
|
-
Requires-Dist: framework-m
|
|
20
|
+
Requires-Dist: framework-m-core>=0.5.0
|
|
21
|
+
Requires-Dist: framework-m>=0.4.2
|
|
22
|
+
Requires-Dist: honcho>=1.1.0
|
|
21
23
|
Requires-Dist: jinja2>=3.1.0
|
|
22
24
|
Requires-Dist: libcst>=1.0.0
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
@@ -27,7 +29,10 @@ Description-Content-Type: text/markdown
|
|
|
27
29
|
Visual DocType builder and developer tools for Framework M.
|
|
28
30
|
|
|
29
31
|
[](https://badge.fury.io/py/framework-m-studio)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
34
|
[](https://gitlab.com/castlecraft/framework-m/-/pipelines)
|
|
35
|
+
[](https://github.com/astral-sh/ruff)
|
|
31
36
|
|
|
32
37
|
## Overview
|
|
33
38
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
framework_m_studio/__init__.py,sha256=9c5EY-sChvaWoZt4FS38xsdckvapt4hO3SsziAb23io,589
|
|
2
|
+
framework_m_studio/app.py,sha256=rzN7R-YzjysRVFDVVTX__K9zZ7fCMDmuYT75rvgVyp0,10412
|
|
3
|
+
framework_m_studio/checklist_parser.py,sha256=IdTtr1hMUd19ySx2hxknEcBQP-B08TfK7k0mgPdaKNc,12079
|
|
4
|
+
framework_m_studio/discovery.py,sha256=bqs9KZDzNhyAMYTgCa9FAce0OUYys3c_gN6i2Xr__BI,6123
|
|
5
|
+
framework_m_studio/docs_generator.py,sha256=okxMvD2BOqyLyAo52m6ZqUIovbcnTq3v2D9u9ZEjHDo,19074
|
|
6
|
+
framework_m_studio/protocol_scanner.py,sha256=oNlxU3wInWjeVkZwJM-hDuRZneU69Y9cXtLLPIg5BDs,12923
|
|
7
|
+
framework_m_studio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
framework_m_studio/routes.py,sha256=ViYaRdS9NxBuUwXIE_Fdg7mzu_fH2TTyQnMReykV5Yk,18979
|
|
9
|
+
framework_m_studio/sdk_generator.py,sha256=L48vURRZauURDmB_F_yYs01kauvnt2SvWkSv3EgzrD8,7283
|
|
10
|
+
framework_m_studio/workspace.py,sha256=VSJfX3WWXLGKBZ-lmQmw3IBtrLUjtI0W8vxJoZa1CNY,8234
|
|
11
|
+
framework_m_studio/cli/__init__.py,sha256=b-MsMBgRaYOBBFiL_DBKJXpHVeN6-dYhd4RvjsZnN7U,21589
|
|
12
|
+
framework_m_studio/cli/build.py,sha256=b3RftIvTW9pyj0CSRj_XMpRewYzI1ZVjcLywHlh2X8g,12360
|
|
13
|
+
framework_m_studio/cli/dev.py,sha256=lPTI4Ou5g3HGF6JugNpYg5TtelnLNeOjXip5FMbtMes,6135
|
|
14
|
+
framework_m_studio/cli/new.py,sha256=piUeWmidT0Ff4LW_jRZ9ZkWTCOFvjZJxIm83cB3N5js,21146
|
|
15
|
+
framework_m_studio/cli/quality.py,sha256=vuGAavMKlDymk4oel3jHLVKONCf-Sn17NWRjkTSkd6s,3796
|
|
16
|
+
framework_m_studio/cli/studio.py,sha256=-44TRjvb9OMQiHWTWW-_7Gi_7jOoLJ97__pf_8tKfCU,3798
|
|
17
|
+
framework_m_studio/cli/utility.py,sha256=XQahBScWf6F9RdojTatI_MjtClNvpVo7aC1tbku2nVg,1051
|
|
18
|
+
framework_m_studio/codegen/__init__.py,sha256=t7DB_zR1DEovL9TYXbsDyQuMyMBTG6YLrVt5wK1OeLI,814
|
|
19
|
+
framework_m_studio/codegen/generator.py,sha256=EbyPlcffJgDIHi7tFFMNOkXRolQj-YHHTA0ZLPE21HQ,9103
|
|
20
|
+
framework_m_studio/codegen/parser.py,sha256=R5w51eQaG2BBPvWBDdLQMdNNs39ybzeojj68fTdd1jU,23415
|
|
21
|
+
framework_m_studio/codegen/test_generator.py,sha256=vPlIsdekM-Siok7nSiMGVug5eEZM141B0WSAuXRVYXQ,10928
|
|
22
|
+
framework_m_studio/codegen/transformer.py,sha256=Ux8-A6UqRaVWKcf_y079WhTchciUunv-CocPXjC326I,12144
|
|
23
|
+
framework_m_studio/codegen/templates/doctype.py.jinja2,sha256=rEYseoOrj7U1Wrqc1ATa0JLHZCGbe9FC6nOnC6xaN7A,2839
|
|
24
|
+
framework_m_studio/codegen/templates/test_doctype.py.jinja2,sha256=Zeo1Mj67pkVIVfcJWo3VgPtaZDOoWeRKAAyHag1nW0I,1614
|
|
25
|
+
framework_m_studio/git/__init__.py,sha256=wsRJ77pUCS081CCN2hx6EpgU9DfY-Uel6cnQqcwBpU0,35
|
|
26
|
+
framework_m_studio/git/adapter.py,sha256=hdpsRPS4wbOA6gX7V9Q4bsmuykmLI2UnTUuwATKhBOU,8973
|
|
27
|
+
framework_m_studio/git/github_provider.py,sha256=Lw_-1Pxm19VKArH4f5e0-la0Vi1HGVCuXnrTbEyR_9s,8522
|
|
28
|
+
framework_m_studio/git/protocol.py,sha256=GyswO1ZlL1BzqM9pAQA7XRj8X1tw04E3jRYaUhdz-Ew,5665
|
|
29
|
+
framework_m_studio-0.3.0.dist-info/METADATA,sha256=mWbWZfp5TqgGKjnqA-FTxxqom9arj7GyiZ3if83D4YU,2463
|
|
30
|
+
framework_m_studio-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
31
|
+
framework_m_studio-0.3.0.dist-info/entry_points.txt,sha256=hc1m9Lf-_4GQcugLyBjWg56SpyhJTaOHKDc1bgkxFfI,885
|
|
32
|
+
framework_m_studio-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[framework_m_core.cli_commands]
|
|
2
|
+
codegen = framework_m_studio.cli:codegen_app
|
|
3
|
+
console = framework_m_studio.cli.utility:console_command
|
|
4
|
+
dev = framework_m_studio.cli.dev:dev_command
|
|
5
|
+
docs = framework_m_studio.cli:docs_app
|
|
6
|
+
docs:adr = framework_m_studio.cli:docs_adr
|
|
7
|
+
docs:export = framework_m_studio.cli:docs_export
|
|
8
|
+
docs:generate = framework_m_studio.cli:docs_generate
|
|
9
|
+
docs:rfc = framework_m_studio.cli:docs_rfc
|
|
10
|
+
format = framework_m_studio.cli.quality:format_command
|
|
11
|
+
lint = framework_m_studio.cli.quality:lint_command
|
|
12
|
+
new = framework_m_studio.cli.new:new_app_command
|
|
13
|
+
new:app = framework_m_studio.cli.new:new_app_command
|
|
14
|
+
new:doctype = framework_m_studio.cli.new:new_doctype_command
|
|
15
|
+
routes = framework_m_studio.cli.utility:routes_command
|
|
16
|
+
studio = framework_m_studio.cli:studio_app
|
|
17
|
+
test = framework_m_studio.cli.quality:test_command
|
|
18
|
+
typecheck = framework_m_studio.cli.quality:typecheck_command
|