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.
- framework_m_studio/__init__.py +16 -0
- framework_m_studio/app.py +283 -0
- framework_m_studio/cli.py +247 -0
- framework_m_studio/codegen/__init__.py +34 -0
- framework_m_studio/codegen/generator.py +291 -0
- framework_m_studio/codegen/parser.py +545 -0
- framework_m_studio/codegen/templates/doctype.py.jinja2 +69 -0
- framework_m_studio/codegen/templates/test_doctype.py.jinja2 +58 -0
- framework_m_studio/codegen/test_generator.py +368 -0
- framework_m_studio/codegen/transformer.py +406 -0
- framework_m_studio/discovery.py +193 -0
- framework_m_studio/docs_generator.py +318 -0
- framework_m_studio/git/__init__.py +1 -0
- framework_m_studio/git/adapter.py +309 -0
- framework_m_studio/git/github_provider.py +321 -0
- framework_m_studio/git/protocol.py +249 -0
- framework_m_studio/py.typed +0 -0
- framework_m_studio/routes.py +552 -0
- framework_m_studio/sdk_generator.py +239 -0
- framework_m_studio/workspace.py +295 -0
- framework_m_studio-0.2.2.dist-info/METADATA +65 -0
- framework_m_studio-0.2.2.dist-info/RECORD +24 -0
- framework_m_studio-0.2.2.dist-info/WHEEL +4 -0
- framework_m_studio-0.2.2.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""Studio API Routes - File System API.
|
|
2
|
+
|
|
3
|
+
This module provides the REST API for DocType file operations:
|
|
4
|
+
- GET /studio/api/doctypes - List all DocTypes in project
|
|
5
|
+
- GET /studio/api/doctype/{name} - Get DocType schema
|
|
6
|
+
- POST /studio/api/doctype/{name} - Create/update DocType
|
|
7
|
+
- DELETE /studio/api/doctype/{name} - Delete DocType
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, ClassVar
|
|
14
|
+
|
|
15
|
+
from litestar import Controller, delete, get, post
|
|
16
|
+
from litestar.exceptions import NotFoundException
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from framework_m_studio.discovery import (
|
|
20
|
+
doctype_to_dict,
|
|
21
|
+
scan_doctypes,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Request/Response Models
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ValidatorSchema(BaseModel):
|
|
30
|
+
"""Validator constraints for a field."""
|
|
31
|
+
|
|
32
|
+
min_length: int | None = None
|
|
33
|
+
max_length: int | None = None
|
|
34
|
+
pattern: str | None = None
|
|
35
|
+
min_value: float | None = None
|
|
36
|
+
max_value: float | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FieldSchema(BaseModel):
|
|
40
|
+
"""Field definition for DocType creation/update."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
type: str
|
|
44
|
+
label: str | None = None
|
|
45
|
+
default: str | None = None
|
|
46
|
+
required: bool = True
|
|
47
|
+
description: str | None = None
|
|
48
|
+
hidden: bool = False
|
|
49
|
+
read_only: bool = False
|
|
50
|
+
validators: ValidatorSchema | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DocTypeSchema(BaseModel):
|
|
54
|
+
"""DocType schema for creation/update."""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
module: str | None = None
|
|
58
|
+
docstring: str | None = None
|
|
59
|
+
fields: list[FieldSchema] = []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DocTypeListResponse(BaseModel):
|
|
63
|
+
"""Response for listing DocTypes."""
|
|
64
|
+
|
|
65
|
+
doctypes: list[dict[str, Any]]
|
|
66
|
+
count: int
|
|
67
|
+
project_root: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DocTypeResponse(BaseModel):
|
|
71
|
+
"""Response for single DocType."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
module: str
|
|
75
|
+
file_path: str
|
|
76
|
+
docstring: str | None
|
|
77
|
+
fields: list[dict[str, Any]]
|
|
78
|
+
meta: dict[str, Any]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CreateDocTypeRequest(BaseModel):
|
|
82
|
+
"""Request body for creating a DocType."""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
app: str | None = None
|
|
86
|
+
docstring: str | None = None
|
|
87
|
+
fields: list[FieldSchema] = []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MessageResponse(BaseModel):
|
|
91
|
+
"""Generic message response."""
|
|
92
|
+
|
|
93
|
+
message: str
|
|
94
|
+
file_path: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# Project Root Detection
|
|
99
|
+
# =============================================================================
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_project_root() -> Path:
|
|
103
|
+
"""Get the project root directory.
|
|
104
|
+
|
|
105
|
+
Priority:
|
|
106
|
+
1. If CWD has src/doctypes directory, use CWD (local app mode)
|
|
107
|
+
2. If CWD has doctypes directory, use CWD
|
|
108
|
+
3. Look for pyproject.toml or .git in CWD only (don't traverse up)
|
|
109
|
+
4. Fall back to CWD
|
|
110
|
+
|
|
111
|
+
This ensures Studio scans only the current directory, not parent repos.
|
|
112
|
+
"""
|
|
113
|
+
cwd = Path.cwd()
|
|
114
|
+
|
|
115
|
+
# Priority 1: CWD has src/doctypes (standard app structure)
|
|
116
|
+
if (cwd / "src" / "doctypes").exists():
|
|
117
|
+
return cwd
|
|
118
|
+
|
|
119
|
+
# Priority 2: CWD has doctypes directly
|
|
120
|
+
if (cwd / "doctypes").exists():
|
|
121
|
+
return cwd
|
|
122
|
+
|
|
123
|
+
# Priority 3: CWD has project markers (don't traverse up!)
|
|
124
|
+
if (cwd / "pyproject.toml").exists() or (cwd / ".git").exists():
|
|
125
|
+
return cwd
|
|
126
|
+
|
|
127
|
+
# Fall back to CWD
|
|
128
|
+
return cwd
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# File System API Controller
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DocTypeController(Controller):
|
|
137
|
+
"""Controller for DocType file system operations."""
|
|
138
|
+
|
|
139
|
+
path = "/studio/api"
|
|
140
|
+
tags: ClassVar[list[str]] = ["Studio - DocTypes"] # type: ignore[misc]
|
|
141
|
+
|
|
142
|
+
@get("/doctypes")
|
|
143
|
+
async def list_doctypes(self) -> DocTypeListResponse:
|
|
144
|
+
"""List all DocTypes in the project.
|
|
145
|
+
|
|
146
|
+
Scans the project for Python files containing DocType class definitions.
|
|
147
|
+
"""
|
|
148
|
+
project_root = get_project_root()
|
|
149
|
+
|
|
150
|
+
# Scan for DocTypes
|
|
151
|
+
doctypes = scan_doctypes(project_root)
|
|
152
|
+
|
|
153
|
+
return DocTypeListResponse(
|
|
154
|
+
doctypes=[doctype_to_dict(dt) for dt in doctypes],
|
|
155
|
+
count=len(doctypes),
|
|
156
|
+
project_root=str(project_root),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@get("/doctype/{name:str}")
|
|
160
|
+
async def get_doctype(self, name: str) -> DocTypeResponse:
|
|
161
|
+
"""Get a specific DocType by name.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: DocType class name (e.g., "Todo", "User")
|
|
165
|
+
"""
|
|
166
|
+
project_root = get_project_root()
|
|
167
|
+
|
|
168
|
+
# Scan for DocTypes and find the matching one
|
|
169
|
+
doctypes = scan_doctypes(project_root)
|
|
170
|
+
|
|
171
|
+
for doctype in doctypes:
|
|
172
|
+
if doctype.name == name:
|
|
173
|
+
# Helper to clean validators
|
|
174
|
+
def clean_validators(
|
|
175
|
+
validators: dict[str, Any],
|
|
176
|
+
) -> dict[str, Any] | None:
|
|
177
|
+
if not validators:
|
|
178
|
+
return None
|
|
179
|
+
# Remove None/empty values
|
|
180
|
+
cleaned = {k: v for k, v in validators.items() if v is not None}
|
|
181
|
+
return cleaned if cleaned else None
|
|
182
|
+
|
|
183
|
+
return DocTypeResponse(
|
|
184
|
+
name=doctype.name,
|
|
185
|
+
module=doctype.module,
|
|
186
|
+
file_path=doctype.file_path,
|
|
187
|
+
docstring=doctype.docstring,
|
|
188
|
+
fields=[
|
|
189
|
+
{
|
|
190
|
+
"name": f.name,
|
|
191
|
+
"type": f.type,
|
|
192
|
+
"default": f.default,
|
|
193
|
+
"required": f.required,
|
|
194
|
+
"label": f.label,
|
|
195
|
+
"description": f.description,
|
|
196
|
+
"validators": clean_validators(f.validators),
|
|
197
|
+
}
|
|
198
|
+
for f in doctype.fields
|
|
199
|
+
],
|
|
200
|
+
meta=doctype.meta,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
raise NotFoundException(f"DocType '{name}' not found")
|
|
204
|
+
|
|
205
|
+
@post("/doctype/{name:str}")
|
|
206
|
+
async def create_or_update_doctype(
|
|
207
|
+
self,
|
|
208
|
+
name: str,
|
|
209
|
+
data: CreateDocTypeRequest,
|
|
210
|
+
) -> MessageResponse:
|
|
211
|
+
"""Create or update a DocType.
|
|
212
|
+
|
|
213
|
+
Creates a folder structure with:
|
|
214
|
+
- doctype.py: DocType class definition
|
|
215
|
+
- controller.py: Controller with lifecycle hooks
|
|
216
|
+
- __init__.py: Package exports
|
|
217
|
+
- test_<name>.py: Test file
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
name: Original DocType class name (from URL path)
|
|
221
|
+
data: DocType schema including fields (data.name is the new name)
|
|
222
|
+
"""
|
|
223
|
+
project_root = get_project_root()
|
|
224
|
+
|
|
225
|
+
# Sanitize the DocType name for valid Python identifier
|
|
226
|
+
# e.g., "Sales Invoice" -> class "SalesInvoice", folder "sales_invoice"
|
|
227
|
+
sanitized_class_name = _sanitize_doctype_name(data.name)
|
|
228
|
+
sanitized_folder_name = _to_snake_case(sanitized_class_name)
|
|
229
|
+
|
|
230
|
+
# Update the data name to the sanitized version
|
|
231
|
+
data.name = sanitized_class_name
|
|
232
|
+
|
|
233
|
+
# Check if this is a rename operation
|
|
234
|
+
is_rename = name != sanitized_class_name
|
|
235
|
+
old_folder_path = None
|
|
236
|
+
|
|
237
|
+
if is_rename:
|
|
238
|
+
# Find the old folder and mark for deletion
|
|
239
|
+
doctypes = scan_doctypes(project_root)
|
|
240
|
+
for doctype in doctypes:
|
|
241
|
+
if doctype.name == name:
|
|
242
|
+
old_file = Path(doctype.file_path)
|
|
243
|
+
# Check if it's in a folder structure or flat file
|
|
244
|
+
if old_file.parent.name == _to_snake_case(name):
|
|
245
|
+
old_folder_path = old_file.parent
|
|
246
|
+
else:
|
|
247
|
+
# Old flat file structure - delete both files
|
|
248
|
+
old_folder_path = None
|
|
249
|
+
if old_file.exists():
|
|
250
|
+
old_file.unlink()
|
|
251
|
+
old_controller = (
|
|
252
|
+
old_file.parent / f"{old_file.stem}_controller.py"
|
|
253
|
+
)
|
|
254
|
+
if old_controller.exists():
|
|
255
|
+
old_controller.unlink()
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
# Determine target directory
|
|
259
|
+
if data.app:
|
|
260
|
+
# Use specified app directory
|
|
261
|
+
target_dir = project_root / data.app / "src"
|
|
262
|
+
if not target_dir.exists():
|
|
263
|
+
target_dir = project_root / data.app
|
|
264
|
+
else:
|
|
265
|
+
# Default to src/
|
|
266
|
+
target_dir = project_root / "src"
|
|
267
|
+
if not target_dir.exists():
|
|
268
|
+
target_dir = project_root
|
|
269
|
+
|
|
270
|
+
# Create folder structure: src/doctypes/<name>/
|
|
271
|
+
doctype_folder = target_dir / "doctypes" / sanitized_folder_name
|
|
272
|
+
doctype_folder.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
# Generate and write doctype.py
|
|
275
|
+
doctype_code = _generate_doctype_code(data)
|
|
276
|
+
(doctype_folder / "doctype.py").write_text(doctype_code, encoding="utf-8")
|
|
277
|
+
|
|
278
|
+
# Generate and write controller.py
|
|
279
|
+
controller_code = _generate_controller_code(sanitized_class_name)
|
|
280
|
+
(doctype_folder / "controller.py").write_text(controller_code, encoding="utf-8")
|
|
281
|
+
|
|
282
|
+
# Generate and write __init__.py
|
|
283
|
+
init_code = _generate_init_code(sanitized_class_name)
|
|
284
|
+
(doctype_folder / "__init__.py").write_text(init_code, encoding="utf-8")
|
|
285
|
+
|
|
286
|
+
# Generate and write test file
|
|
287
|
+
test_code = _generate_test_code(sanitized_class_name, sanitized_folder_name)
|
|
288
|
+
(doctype_folder / f"test_{sanitized_folder_name}.py").write_text(
|
|
289
|
+
test_code, encoding="utf-8"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Delete old folder if this was a rename
|
|
293
|
+
if is_rename and old_folder_path and old_folder_path.exists():
|
|
294
|
+
import shutil
|
|
295
|
+
|
|
296
|
+
shutil.rmtree(old_folder_path)
|
|
297
|
+
|
|
298
|
+
action = "renamed" if is_rename else "created"
|
|
299
|
+
return MessageResponse(
|
|
300
|
+
message=f"DocType '{sanitized_class_name}' {action} successfully",
|
|
301
|
+
file_path=str(doctype_folder),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@delete("/doctype/{name:str}", status_code=200)
|
|
305
|
+
async def delete_doctype(self, name: str) -> MessageResponse:
|
|
306
|
+
"""Delete a DocType directory and all its files.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
name: DocType class name to delete
|
|
310
|
+
"""
|
|
311
|
+
import shutil
|
|
312
|
+
|
|
313
|
+
project_root = get_project_root()
|
|
314
|
+
|
|
315
|
+
# Find the DocType file
|
|
316
|
+
doctypes = scan_doctypes(project_root)
|
|
317
|
+
|
|
318
|
+
for doctype in doctypes:
|
|
319
|
+
if doctype.name == name:
|
|
320
|
+
file_path = Path(doctype.file_path)
|
|
321
|
+
doctype_dir = file_path.parent
|
|
322
|
+
|
|
323
|
+
# Delete the entire DocType directory
|
|
324
|
+
if doctype_dir.exists() and doctype_dir.is_dir():
|
|
325
|
+
shutil.rmtree(doctype_dir)
|
|
326
|
+
return MessageResponse(
|
|
327
|
+
message=f"DocType '{name}' deleted successfully",
|
|
328
|
+
file_path=str(doctype_dir),
|
|
329
|
+
)
|
|
330
|
+
elif file_path.exists():
|
|
331
|
+
# Fallback: delete just the file if no directory
|
|
332
|
+
file_path.unlink()
|
|
333
|
+
return MessageResponse(
|
|
334
|
+
message=f"DocType '{name}' deleted successfully",
|
|
335
|
+
file_path=str(file_path),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
raise NotFoundException(f"DocType '{name}' not found")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# =============================================================================
|
|
342
|
+
# Code Generation Helpers
|
|
343
|
+
# =============================================================================
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _sanitize_doctype_name(name: str) -> str:
|
|
347
|
+
"""Sanitize DocType name to be valid Python identifier.
|
|
348
|
+
|
|
349
|
+
- Replace spaces with underscores
|
|
350
|
+
- Remove all special characters except underscores
|
|
351
|
+
- Convert to PascalCase
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
name: Raw DocType name (e.g., "Sales Invoice", "sales-order")
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Sanitized PascalCase name (e.g., "SalesInvoice", "SalesOrder")
|
|
358
|
+
"""
|
|
359
|
+
import re
|
|
360
|
+
|
|
361
|
+
# Replace hyphens and spaces with underscores first
|
|
362
|
+
s = re.sub(r"[-\s]+", "_", name)
|
|
363
|
+
|
|
364
|
+
# Remove any non-alphanumeric characters except underscores
|
|
365
|
+
s = re.sub(r"[^a-zA-Z0-9_]", "", s)
|
|
366
|
+
|
|
367
|
+
# Split by underscores and capitalize first letter of each word
|
|
368
|
+
# Use word[0].upper() + word[1:] instead of capitalize() to preserve case
|
|
369
|
+
parts = s.split("_")
|
|
370
|
+
pascal = "".join((word[0].upper() + word[1:]) if word else "" for word in parts)
|
|
371
|
+
|
|
372
|
+
return pascal or "DocType"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _to_snake_case(name: str) -> str:
|
|
376
|
+
"""Convert PascalCase to snake_case."""
|
|
377
|
+
import re
|
|
378
|
+
|
|
379
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
380
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _generate_doctype_code(schema: CreateDocTypeRequest) -> str:
|
|
384
|
+
"""Generate Python code for a DocType.
|
|
385
|
+
|
|
386
|
+
Now uses the Jinja2 template system for consistent code generation.
|
|
387
|
+
"""
|
|
388
|
+
from framework_m_studio.codegen.generator import generate_doctype_source
|
|
389
|
+
|
|
390
|
+
# Convert CreateDocTypeRequest to dict format expected by generator
|
|
391
|
+
schema_dict = {
|
|
392
|
+
"name": schema.name,
|
|
393
|
+
"docstring": schema.docstring,
|
|
394
|
+
"fields": [
|
|
395
|
+
{
|
|
396
|
+
"name": f.name,
|
|
397
|
+
"type": f.type,
|
|
398
|
+
"required": f.required,
|
|
399
|
+
"label": f.label,
|
|
400
|
+
"default": f.default,
|
|
401
|
+
"description": f.description,
|
|
402
|
+
"validators": (
|
|
403
|
+
{
|
|
404
|
+
"min_length": f.validators.min_length,
|
|
405
|
+
"max_length": f.validators.max_length,
|
|
406
|
+
"pattern": f.validators.pattern,
|
|
407
|
+
"min_value": f.validators.min_value,
|
|
408
|
+
"max_value": f.validators.max_value,
|
|
409
|
+
}
|
|
410
|
+
if f.validators
|
|
411
|
+
else {}
|
|
412
|
+
),
|
|
413
|
+
}
|
|
414
|
+
for f in schema.fields
|
|
415
|
+
],
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return str(generate_doctype_source(schema_dict))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _generate_controller_code(name: str) -> str:
|
|
422
|
+
"""Generate Python code for a Controller.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
name: The DocType class name (e.g., "Invoice")
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Generated controller Python code
|
|
429
|
+
"""
|
|
430
|
+
lines = [
|
|
431
|
+
f'"""Controller for {name} DocType."""',
|
|
432
|
+
"",
|
|
433
|
+
"from __future__ import annotations",
|
|
434
|
+
"",
|
|
435
|
+
"from framework_m.core.domain.base_controller import BaseController",
|
|
436
|
+
f"from .doctype import {name}",
|
|
437
|
+
"",
|
|
438
|
+
"",
|
|
439
|
+
f"class {name}Controller(BaseController[{name}]):",
|
|
440
|
+
f' """Controller for {name} DocType.',
|
|
441
|
+
"",
|
|
442
|
+
" Implement custom business logic here.",
|
|
443
|
+
"",
|
|
444
|
+
" Lifecycle Hooks:",
|
|
445
|
+
" - validate: Called before save to validate data",
|
|
446
|
+
" - before_save: Called before persisting to database",
|
|
447
|
+
" - after_save: Called after successful save",
|
|
448
|
+
" - before_delete: Called before deleting a document",
|
|
449
|
+
' """',
|
|
450
|
+
"",
|
|
451
|
+
f" doctype = {name}",
|
|
452
|
+
"",
|
|
453
|
+
f" async def validate(self, doc: {name}) -> None:",
|
|
454
|
+
' """Validate document before saving.',
|
|
455
|
+
"",
|
|
456
|
+
" Raise ValueError for validation errors.",
|
|
457
|
+
"",
|
|
458
|
+
" Example:",
|
|
459
|
+
" if not doc.name:",
|
|
460
|
+
' raise ValueError("Name is required")',
|
|
461
|
+
' """',
|
|
462
|
+
" pass",
|
|
463
|
+
"",
|
|
464
|
+
f" async def before_save(self, doc: {name}) -> None:",
|
|
465
|
+
' """Called before saving a document.',
|
|
466
|
+
"",
|
|
467
|
+
" Use for setting computed fields or timestamps.",
|
|
468
|
+
' """',
|
|
469
|
+
" pass",
|
|
470
|
+
"",
|
|
471
|
+
f" async def after_save(self, doc: {name}) -> None:",
|
|
472
|
+
' """Called after saving a document.',
|
|
473
|
+
"",
|
|
474
|
+
" Use for side effects like sending notifications.",
|
|
475
|
+
' """',
|
|
476
|
+
" pass",
|
|
477
|
+
"",
|
|
478
|
+
f" async def before_delete(self, doc: {name}) -> None:",
|
|
479
|
+
' """Called before deleting a document.',
|
|
480
|
+
"",
|
|
481
|
+
" Use for cleanup or validation before delete.",
|
|
482
|
+
' """',
|
|
483
|
+
" pass",
|
|
484
|
+
"",
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
return "\n".join(lines)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _generate_init_code(name: str) -> str:
|
|
491
|
+
"""Generate __init__.py code for a DocType package.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
name: The DocType class name (e.g., "Invoice")
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Generated __init__.py Python code
|
|
498
|
+
"""
|
|
499
|
+
return f'''"""{{ class_name }} DocType package.
|
|
500
|
+
|
|
501
|
+
Auto-generated by Framework M Studio.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
from .controller import {name}Controller
|
|
505
|
+
from .doctype import {name}
|
|
506
|
+
|
|
507
|
+
__all__ = ["{name}", "{name}Controller"]
|
|
508
|
+
'''.replace("{{ class_name }}", name)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _generate_test_code(name: str, snake_name: str) -> str:
|
|
512
|
+
"""Generate test file code for a DocType.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
name: The DocType class name (e.g., "Invoice")
|
|
516
|
+
snake_name: Snake case name (e.g., "invoice")
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Generated test Python code
|
|
520
|
+
"""
|
|
521
|
+
return f'''"""Tests for {name} DocType.
|
|
522
|
+
|
|
523
|
+
Auto-generated by Framework M Studio.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
from __future__ import annotations
|
|
527
|
+
|
|
528
|
+
import pytest
|
|
529
|
+
|
|
530
|
+
from .doctype import {name}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class Test{name}:
|
|
534
|
+
"""Tests for {name}."""
|
|
535
|
+
|
|
536
|
+
def test_create_{snake_name}(self) -> None:
|
|
537
|
+
"""{name} should be creatable."""
|
|
538
|
+
doc = {name}(name="test-001")
|
|
539
|
+
assert doc.name == "test-001"
|
|
540
|
+
|
|
541
|
+
def test_{snake_name}_doctype_name(self) -> None:
|
|
542
|
+
"""{name} should have correct class name."""
|
|
543
|
+
assert {name}.__name__ == "{name}"
|
|
544
|
+
'''
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
__all__ = [
|
|
548
|
+
"CreateDocTypeRequest",
|
|
549
|
+
"DocTypeController",
|
|
550
|
+
"DocTypeListResponse",
|
|
551
|
+
"DocTypeResponse",
|
|
552
|
+
]
|