model-generator-kit 0.1.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.
- model_generator/__init__.py +6 -0
- model_generator/generate.py +1030 -0
- model_generator/generators/__init__.py +38 -0
- model_generator/generators/api.py +287 -0
- model_generator/generators/constraints.py +176 -0
- model_generator/generators/database.py +147 -0
- model_generator/generators/enums.py +88 -0
- model_generator/generators/infrastructure.py +679 -0
- model_generator/generators/migrations.py +146 -0
- model_generator/py.typed +0 -0
- model_generator/schema/model.schema.json +758 -0
- model_generator/stacks/python-fastapi/config.yaml +403 -0
- model_generator/stacks/python-fastapi/templates/_shared/_base.j2 +26 -0
- model_generator/stacks/python-fastapi/templates/_shared/_entity.j2 +48 -0
- model_generator/stacks/python-fastapi/templates/_shared/_examples.j2 +50 -0
- model_generator/stacks/python-fastapi/templates/_shared/_fields.j2 +48 -0
- model_generator/stacks/python-fastapi/templates/_shared/_tests.j2 +143 -0
- model_generator/stacks/python-fastapi/templates/api/init.py.j2 +55 -0
- model_generator/stacks/python-fastapi/templates/api/pagination.py.j2 +79 -0
- model_generator/stacks/python-fastapi/templates/api/request.py.j2 +448 -0
- model_generator/stacks/python-fastapi/templates/api/response.py.j2 +222 -0
- model_generator/stacks/python-fastapi/templates/api/route.py.j2 +507 -0
- model_generator/stacks/python-fastapi/templates/database/constraints.py.j2 +439 -0
- model_generator/stacks/python-fastapi/templates/database/enums.py.j2 +55 -0
- model_generator/stacks/python-fastapi/templates/database/factory.py.j2 +265 -0
- model_generator/stacks/python-fastapi/templates/database/init.py.j2 +37 -0
- model_generator/stacks/python-fastapi/templates/database/model.py.j2 +476 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/auth_router.py.j2 +434 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/base.py.j2 +16 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/csrf.py.j2 +121 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/database_init.py.j2 +12 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/encrypted_bytes.py.j2 +62 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/engine.py.j2 +51 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/errors.py.j2 +74 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/gitignore.j2 +48 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/main.py.j2 +94 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/pyproject.toml.j2 +92 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/rate_limit.py.j2 +41 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/types.py.j2 +94 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/utils.py.j2 +50 -0
- model_generator/stacks/python-fastapi/templates/infrastructure/validators.py.j2 +126 -0
- model_generator/stacks/python-fastapi/templates/migrations/env.py.j2 +125 -0
- model_generator/stacks/python-fastapi/templates/migrations/ini.j2 +109 -0
- model_generator/stacks/python-fastapi/templates/migrations/script.py.mako.j2 +35 -0
- model_generator/stacks/python-fastapi/templates/tests/conftest_root.py.j2 +122 -0
- model_generator/stacks/python-fastapi/templates/tests/contract.py.j2 +1860 -0
- model_generator/utils/__init__.py +31 -0
- model_generator/utils/conftest_generator.py +683 -0
- model_generator/utils/constants.py +6 -0
- model_generator/utils/loaders.py +292 -0
- model_generator/utils/parser.py +129 -0
- model_generator/utils/quality.py +43 -0
- model_generator/utils/templates.py +128 -0
- model_generator/validate.py +219 -0
- model_generator/wizard/__init__.py +10 -0
- model_generator/wizard/actions/__init__.py +1 -0
- model_generator/wizard/actions/clean.py +55 -0
- model_generator/wizard/actions/generate.py +166 -0
- model_generator/wizard/actions/project_setup.py +142 -0
- model_generator/wizard/actions/test_runner.py +60 -0
- model_generator/wizard/menu.py +43 -0
- model_generator/wizard/prompts.py +80 -0
- model_generator_kit-0.1.0.dist-info/METADATA +143 -0
- model_generator_kit-0.1.0.dist-info/RECORD +68 -0
- model_generator_kit-0.1.0.dist-info/WHEEL +5 -0
- model_generator_kit-0.1.0.dist-info/entry_points.txt +3 -0
- model_generator_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
- model_generator_kit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Model JSON Validator.
|
|
4
|
+
|
|
5
|
+
Validates model definition JSON files against the multi-entity schema.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python validate.py <model.json>
|
|
9
|
+
python validate.py models/users.model.json
|
|
10
|
+
python validate.py --all models/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, cast
|
|
18
|
+
|
|
19
|
+
from jsonschema import Draft7Validator
|
|
20
|
+
|
|
21
|
+
from . import __version__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_schema() -> dict[str, Any]:
|
|
25
|
+
"""Load the model schema from the skill directory."""
|
|
26
|
+
script_dir = Path(__file__).parent
|
|
27
|
+
schema_path = script_dir / "schema" / "model.schema.json"
|
|
28
|
+
|
|
29
|
+
if not schema_path.exists():
|
|
30
|
+
print(f"Error: Schema not found at {schema_path}")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
with open(schema_path) as f:
|
|
34
|
+
return cast(dict[str, Any], json.load(f))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_model(model_path: Path, schema: dict[str, Any]) -> list[str]:
|
|
38
|
+
"""
|
|
39
|
+
Validate a model JSON file against the schema.
|
|
40
|
+
|
|
41
|
+
Returns list of error messages (empty if valid).
|
|
42
|
+
"""
|
|
43
|
+
errors = []
|
|
44
|
+
|
|
45
|
+
if not model_path.exists():
|
|
46
|
+
return [f"File not found: {model_path}"]
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with open(model_path) as f:
|
|
50
|
+
model = json.load(f)
|
|
51
|
+
except json.JSONDecodeError as e:
|
|
52
|
+
return [f"Invalid JSON: {e}"]
|
|
53
|
+
|
|
54
|
+
# Validate against schema
|
|
55
|
+
validator = Draft7Validator(schema)
|
|
56
|
+
for error in sorted(validator.iter_errors(model), key=lambda e: e.path):
|
|
57
|
+
path = " -> ".join(str(p) for p in error.path) if error.path else "(root)"
|
|
58
|
+
errors.append(f" {path}: {error.message}")
|
|
59
|
+
|
|
60
|
+
# Additional semantic validations
|
|
61
|
+
if not errors:
|
|
62
|
+
errors.extend(validate_semantics(model))
|
|
63
|
+
|
|
64
|
+
return errors
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_semantics(model: dict[str, Any]) -> list[str]:
|
|
68
|
+
"""
|
|
69
|
+
Perform semantic validations beyond schema.
|
|
70
|
+
|
|
71
|
+
Validates each entity for:
|
|
72
|
+
- Primary key field exists
|
|
73
|
+
- Enum fields have values or reference existing enum
|
|
74
|
+
- Reference fields point to valid tables
|
|
75
|
+
- Constraint field references exist
|
|
76
|
+
- Relationship targets exist within the domain
|
|
77
|
+
- Immutable entity constraints
|
|
78
|
+
"""
|
|
79
|
+
errors = []
|
|
80
|
+
entities = model.get("entities", {})
|
|
81
|
+
|
|
82
|
+
for entity_name, entity in entities.items():
|
|
83
|
+
prefix = f" [{entity_name}]"
|
|
84
|
+
fields = entity.get("fields", {})
|
|
85
|
+
|
|
86
|
+
# Check for primary key
|
|
87
|
+
has_pk = any(f.get("primary_key", False) for f in fields.values())
|
|
88
|
+
if not has_pk:
|
|
89
|
+
errors.append(f"{prefix} No primary key field defined")
|
|
90
|
+
|
|
91
|
+
# Check enum fields
|
|
92
|
+
for field_name, field in fields.items():
|
|
93
|
+
if field.get("type") == "enum":
|
|
94
|
+
if not field.get("enum_name"):
|
|
95
|
+
errors.append(
|
|
96
|
+
f"{prefix} Enum field '{field_name}' missing enum_name"
|
|
97
|
+
)
|
|
98
|
+
if not field.get("enum_values") and not field.get("enum_existing"):
|
|
99
|
+
errors.append(
|
|
100
|
+
f"{prefix} Enum field '{field_name}' "
|
|
101
|
+
"needs enum_values or enum_existing=true"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Check reference fields
|
|
105
|
+
for field_name, field in fields.items():
|
|
106
|
+
if field.get("type") == "reference":
|
|
107
|
+
if not field.get("reference_table"):
|
|
108
|
+
errors.append(
|
|
109
|
+
f"{prefix} Reference field '{field_name}' "
|
|
110
|
+
"missing reference_table"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Check field constraints reference valid fields
|
|
114
|
+
for field_name, field in fields.items():
|
|
115
|
+
for constraint in field.get("constraints", []):
|
|
116
|
+
if constraint.get("type") == "depends":
|
|
117
|
+
other_field = constraint.get("other_field")
|
|
118
|
+
if other_field and other_field not in fields:
|
|
119
|
+
errors.append(
|
|
120
|
+
f"{prefix} Constraint on '{field_name}' references "
|
|
121
|
+
f"non-existent field '{other_field}'"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Check table constraints reference valid fields
|
|
125
|
+
for constraint in entity.get("constraints", []):
|
|
126
|
+
for field_ref in constraint.get("fields", []):
|
|
127
|
+
if field_ref not in fields:
|
|
128
|
+
errors.append(
|
|
129
|
+
f"{prefix} Table constraint references "
|
|
130
|
+
f"non-existent field '{field_ref}'"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Collect all known field names (explicit + timestamp-generated)
|
|
134
|
+
all_field_names = set(fields.keys())
|
|
135
|
+
timestamps = entity.get("timestamps", {})
|
|
136
|
+
if timestamps.get("created", True):
|
|
137
|
+
all_field_names.add("created_at")
|
|
138
|
+
if timestamps.get("updated", True):
|
|
139
|
+
all_field_names.add("updated_at")
|
|
140
|
+
|
|
141
|
+
# Check index fields exist
|
|
142
|
+
for index in entity.get("indexes", []):
|
|
143
|
+
for field_ref in index.get("fields", []):
|
|
144
|
+
if field_ref not in all_field_names:
|
|
145
|
+
errors.append(
|
|
146
|
+
f"{prefix} Index references non-existent field '{field_ref}'"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Check immutable entities
|
|
150
|
+
if entity.get("mutability") == "immutable":
|
|
151
|
+
if entity.get("timestamps", {}).get("updated"):
|
|
152
|
+
errors.append(
|
|
153
|
+
f"{prefix} Immutable entities should not have updated_at timestamp"
|
|
154
|
+
)
|
|
155
|
+
api_endpoints = entity.get("api", {}).get("endpoints", [])
|
|
156
|
+
if "update" in api_endpoints or "delete" in api_endpoints:
|
|
157
|
+
errors.append(
|
|
158
|
+
f"{prefix} Immutable entities should not have "
|
|
159
|
+
"'update' or 'delete' endpoints"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Relationship targets: cross-domain refs are allowed, no validation here.
|
|
163
|
+
|
|
164
|
+
return errors
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> None:
|
|
168
|
+
parser = argparse.ArgumentParser(description="Validate model JSON files")
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--version",
|
|
171
|
+
action="version",
|
|
172
|
+
version=f"%(prog)s {__version__}",
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"path",
|
|
176
|
+
type=Path,
|
|
177
|
+
help="Model JSON file or directory containing model files",
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"--all",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="Validate all *.model.json files in directory",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
schema = load_schema()
|
|
187
|
+
|
|
188
|
+
if args.path.is_dir() or args.all:
|
|
189
|
+
directory = args.path if args.path.is_dir() else args.path.parent
|
|
190
|
+
model_files = list(directory.glob("*.model.json"))
|
|
191
|
+
if not model_files:
|
|
192
|
+
print(f"No *.model.json files found in {directory}")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
else:
|
|
195
|
+
model_files = [args.path]
|
|
196
|
+
|
|
197
|
+
all_valid = True
|
|
198
|
+
for model_path in sorted(model_files):
|
|
199
|
+
errors = validate_model(model_path, schema)
|
|
200
|
+
if errors:
|
|
201
|
+
all_valid = False
|
|
202
|
+
print(f"\n❌ {model_path.name}:")
|
|
203
|
+
for error in errors:
|
|
204
|
+
print(error)
|
|
205
|
+
else:
|
|
206
|
+
# Show entity count
|
|
207
|
+
with open(model_path) as f:
|
|
208
|
+
model = json.load(f)
|
|
209
|
+
entity_count = len(model.get("entities", {}))
|
|
210
|
+
print(f"✅ {model_path.name}: Valid ({entity_count} entities)")
|
|
211
|
+
|
|
212
|
+
if not all_valid:
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
print(f"\n✅ All {len(model_files)} model(s) valid")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Wizard actions package."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action: Clean generated files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..prompts import confirm, select
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_project_root() -> Path:
|
|
13
|
+
"""Find project root by looking for .model-generator.yaml."""
|
|
14
|
+
cwd = Path.cwd()
|
|
15
|
+
if (cwd / ".model-generator.yaml").exists():
|
|
16
|
+
return cwd
|
|
17
|
+
parent = cwd.parent
|
|
18
|
+
if (parent / ".model-generator.yaml").exists():
|
|
19
|
+
return parent
|
|
20
|
+
return cwd
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_clean() -> None:
|
|
24
|
+
"""Select scope, preview, confirm, and clean generated files."""
|
|
25
|
+
project_root = _find_project_root()
|
|
26
|
+
|
|
27
|
+
if not (project_root / ".model-generator.yaml").exists():
|
|
28
|
+
print("\nNo .model-generator.yaml found.")
|
|
29
|
+
print("Run 'Setup/update project settings' first.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
scope = select(
|
|
33
|
+
"Cleanup scope:",
|
|
34
|
+
choices=["selective", "full"],
|
|
35
|
+
default="selective",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
print(f"\nScope: {scope}")
|
|
39
|
+
if scope == "selective":
|
|
40
|
+
print(" Will remove only files that would be regenerated.")
|
|
41
|
+
else:
|
|
42
|
+
print(" Will remove all generated directories, caches, and venvs.")
|
|
43
|
+
|
|
44
|
+
# Dry run first
|
|
45
|
+
from ...generate import cleanup_generated
|
|
46
|
+
|
|
47
|
+
print("\nPreview (what would be deleted):")
|
|
48
|
+
cleanup_generated(project_root, scope=scope, dry_run=True)
|
|
49
|
+
|
|
50
|
+
if not confirm("\nProceed with deletion?", default=False):
|
|
51
|
+
print("Cancelled.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
cleanup_generated(project_root, scope=scope, dry_run=False)
|
|
55
|
+
print("\nCleanup complete.")
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action: Generate code from model specifications.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..prompts import checkbox, confirm, select
|
|
11
|
+
|
|
12
|
+
INFRASTRUCTURE_TARGETS = {"all", "infrastructure"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _find_project_root() -> Path:
|
|
16
|
+
"""Find project root by looking for .model-generator.yaml."""
|
|
17
|
+
cwd = Path.cwd()
|
|
18
|
+
if (cwd / ".model-generator.yaml").exists():
|
|
19
|
+
return cwd
|
|
20
|
+
parent = cwd.parent
|
|
21
|
+
if (parent / ".model-generator.yaml").exists():
|
|
22
|
+
return parent
|
|
23
|
+
return cwd
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_models_dir(project_root: Path) -> Path | None:
|
|
27
|
+
"""Find the models directory."""
|
|
28
|
+
models_dir = project_root / "models"
|
|
29
|
+
if models_dir.exists():
|
|
30
|
+
return models_dir
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_generate() -> None:
|
|
35
|
+
"""Select domains and targets, then run generation."""
|
|
36
|
+
project_root = _find_project_root()
|
|
37
|
+
|
|
38
|
+
if not (project_root / ".model-generator.yaml").exists():
|
|
39
|
+
print("\nNo .model-generator.yaml found.")
|
|
40
|
+
print("Run 'Setup/update project settings' first.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
models_dir = _find_models_dir(project_root)
|
|
44
|
+
if models_dir is None:
|
|
45
|
+
print(f"\nNo models/ directory found in {project_root}")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Scan for model files
|
|
49
|
+
model_files = sorted(models_dir.glob("*.model.json"))
|
|
50
|
+
if not model_files:
|
|
51
|
+
print(f"\nNo *.model.json files found in {models_dir}")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
domain_names = [f.stem.replace(".model", "") for f in model_files]
|
|
55
|
+
print(f"\nFound {len(model_files)} domain(s): {', '.join(domain_names)}")
|
|
56
|
+
|
|
57
|
+
# Select domains
|
|
58
|
+
if len(model_files) > 1:
|
|
59
|
+
selected = checkbox(
|
|
60
|
+
"Select domains to generate:",
|
|
61
|
+
choices=["all"] + domain_names,
|
|
62
|
+
)
|
|
63
|
+
if "all" in selected:
|
|
64
|
+
selected_files = model_files
|
|
65
|
+
else:
|
|
66
|
+
selected_files = [
|
|
67
|
+
f for f in model_files if f.stem.replace(".model", "") in selected
|
|
68
|
+
]
|
|
69
|
+
else:
|
|
70
|
+
selected_files = model_files
|
|
71
|
+
|
|
72
|
+
if not selected_files:
|
|
73
|
+
print("No domains selected.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Select target
|
|
77
|
+
targets = [
|
|
78
|
+
"all",
|
|
79
|
+
"infrastructure",
|
|
80
|
+
"database",
|
|
81
|
+
"factories",
|
|
82
|
+
"api-models",
|
|
83
|
+
"api-routes",
|
|
84
|
+
"api-tests",
|
|
85
|
+
"enums",
|
|
86
|
+
"constraints",
|
|
87
|
+
"migration-init",
|
|
88
|
+
]
|
|
89
|
+
target = select("Generation target:", choices=targets, default="all")
|
|
90
|
+
|
|
91
|
+
# Confirm
|
|
92
|
+
file_names = [f.name for f in selected_files]
|
|
93
|
+
print(f"\nWill generate '{target}' for: {', '.join(file_names)}")
|
|
94
|
+
if not confirm("Proceed?"):
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Generate infrastructure if needed (same logic as main())
|
|
98
|
+
if target in INFRASTRUCTURE_TARGETS:
|
|
99
|
+
from ...generate import _has_encrypted_binary_field
|
|
100
|
+
from ...generators.infrastructure import generate_infrastructure
|
|
101
|
+
from ...utils import (
|
|
102
|
+
get_layout,
|
|
103
|
+
get_template_env,
|
|
104
|
+
load_config,
|
|
105
|
+
load_model,
|
|
106
|
+
run_quality_tools,
|
|
107
|
+
)
|
|
108
|
+
from ...utils.templates import snake_case
|
|
109
|
+
|
|
110
|
+
stack = "python-fastapi"
|
|
111
|
+
config = load_config(stack)
|
|
112
|
+
env = get_template_env(stack, config)
|
|
113
|
+
layout = get_layout(config)
|
|
114
|
+
|
|
115
|
+
domains: list[str] = []
|
|
116
|
+
route_modules: list[str] = []
|
|
117
|
+
factory_modules: list[str] = []
|
|
118
|
+
loaded_models: list[dict[str, Any]] = []
|
|
119
|
+
for model_file in selected_files:
|
|
120
|
+
model = load_model(model_file)
|
|
121
|
+
loaded_models.append(model)
|
|
122
|
+
domain = model.get("domain", "unknown")
|
|
123
|
+
has_api = any(
|
|
124
|
+
e.get("api", {}).get("enabled", True)
|
|
125
|
+
for e in model.get("entities", {}).values()
|
|
126
|
+
)
|
|
127
|
+
if domain not in domains and has_api:
|
|
128
|
+
domains.append(domain)
|
|
129
|
+
if layout == "per-entity":
|
|
130
|
+
for name, entity in model.get("entities", {}).items():
|
|
131
|
+
stem = snake_case(name)
|
|
132
|
+
if (
|
|
133
|
+
entity.get("api", {}).get("enabled", True)
|
|
134
|
+
and stem not in route_modules
|
|
135
|
+
):
|
|
136
|
+
route_modules.append(stem)
|
|
137
|
+
if has_api and stem not in factory_modules:
|
|
138
|
+
factory_modules.append(stem)
|
|
139
|
+
if layout != "per-entity":
|
|
140
|
+
route_modules = list(domains)
|
|
141
|
+
factory_modules = list(domains)
|
|
142
|
+
|
|
143
|
+
infra_files = generate_infrastructure(
|
|
144
|
+
config=config,
|
|
145
|
+
env=env,
|
|
146
|
+
project_root=project_root,
|
|
147
|
+
domains=domains,
|
|
148
|
+
route_modules=route_modules,
|
|
149
|
+
factory_modules=factory_modules,
|
|
150
|
+
project_config=config,
|
|
151
|
+
has_encrypted_binary=_has_encrypted_binary_field(loaded_models),
|
|
152
|
+
)
|
|
153
|
+
if infra_files:
|
|
154
|
+
run_quality_tools(config, project_root, infra_files)
|
|
155
|
+
|
|
156
|
+
if target == "infrastructure":
|
|
157
|
+
print("\nInfrastructure generation complete.")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Generate per domain using the existing generate() function
|
|
161
|
+
from ...generate import generate
|
|
162
|
+
|
|
163
|
+
for model_file in selected_files:
|
|
164
|
+
generate(model_path=model_file, target=target)
|
|
165
|
+
|
|
166
|
+
print("\nGeneration complete.")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action: Setup or update project settings (.model-generator.yaml).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from ..prompts import confirm, select, text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _find_project_root() -> Path:
|
|
16
|
+
"""Find project root by looking for .model-generator.yaml."""
|
|
17
|
+
cwd = Path.cwd()
|
|
18
|
+
if (cwd / ".model-generator.yaml").exists():
|
|
19
|
+
return cwd
|
|
20
|
+
parent = cwd.parent
|
|
21
|
+
if (parent / ".model-generator.yaml").exists():
|
|
22
|
+
return parent
|
|
23
|
+
return cwd
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _scan_stacks() -> list[str]:
|
|
27
|
+
"""Scan available stacks from the stacks directory."""
|
|
28
|
+
stacks_dir = Path(__file__).parent.parent / "stacks"
|
|
29
|
+
if not stacks_dir.exists():
|
|
30
|
+
return ["python-fastapi"]
|
|
31
|
+
return sorted(
|
|
32
|
+
d.name
|
|
33
|
+
for d in stacks_dir.iterdir()
|
|
34
|
+
if d.is_dir() and not d.name.startswith(".")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_PATH_LAYOUTS: dict[str, dict[str, str]] = {
|
|
39
|
+
"full-stack (backend/src/)": {
|
|
40
|
+
"database_models": "backend/src/database/models",
|
|
41
|
+
"factories": "backend/src/database/models/factories",
|
|
42
|
+
"api_models": "backend/src/api/models",
|
|
43
|
+
"api_routes": "backend/src/api/routes",
|
|
44
|
+
"api_tests": "tests/contract/api",
|
|
45
|
+
"base": "backend/src/database/models/base.py",
|
|
46
|
+
"engine": "backend/src/database/engine.py",
|
|
47
|
+
"main": "backend/src/main.py",
|
|
48
|
+
"errors": "backend/src/api/errors.py",
|
|
49
|
+
"validators": "backend/src/api/validators.py",
|
|
50
|
+
"test_conftest_root": "tests/conftest.py",
|
|
51
|
+
"enums": "backend/src/database/models/enums.py",
|
|
52
|
+
"constraints": "backend/src/database/models/constraints.py",
|
|
53
|
+
},
|
|
54
|
+
"backend-only (src/)": {
|
|
55
|
+
"database_models": "src/database/models",
|
|
56
|
+
"factories": "src/database/models/factories",
|
|
57
|
+
"api_models": "src/api/models",
|
|
58
|
+
"api_routes": "src/api/routes",
|
|
59
|
+
"api_tests": "tests/api",
|
|
60
|
+
"base": "src/database/models/base.py",
|
|
61
|
+
"engine": "src/database/engine.py",
|
|
62
|
+
"main": "src/main.py",
|
|
63
|
+
"errors": "src/api/errors.py",
|
|
64
|
+
"validators": "src/api/validators.py",
|
|
65
|
+
"test_conftest_root": "tests/conftest.py",
|
|
66
|
+
"enums": "src/database/models/enums.py",
|
|
67
|
+
"constraints": "src/database/models/constraints.py",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_setup() -> None:
|
|
73
|
+
"""Create or update .model-generator.yaml interactively."""
|
|
74
|
+
project_root = _find_project_root()
|
|
75
|
+
config_path = project_root / ".model-generator.yaml"
|
|
76
|
+
|
|
77
|
+
if config_path.exists():
|
|
78
|
+
_update_config(config_path)
|
|
79
|
+
else:
|
|
80
|
+
_create_config(config_path, project_root)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _create_config(config_path: Path, project_root: Path) -> None:
|
|
84
|
+
"""Guide user through creating a new config."""
|
|
85
|
+
print("\n--- Create Project Settings ---\n")
|
|
86
|
+
|
|
87
|
+
name = text("Project name:", default=project_root.name)
|
|
88
|
+
description = text("Description (optional):", default="")
|
|
89
|
+
version = text("Version:", default="0.1.0")
|
|
90
|
+
|
|
91
|
+
stacks = _scan_stacks()
|
|
92
|
+
stack = select("Stack:", choices=stacks, default="python-fastapi")
|
|
93
|
+
|
|
94
|
+
layout_choices = list(_PATH_LAYOUTS.keys()) + ["custom"]
|
|
95
|
+
layout = select("Path layout:", choices=layout_choices, default=layout_choices[0])
|
|
96
|
+
|
|
97
|
+
config: dict[str, Any] = {
|
|
98
|
+
"project": {"name": name, "version": version},
|
|
99
|
+
"stack": stack,
|
|
100
|
+
}
|
|
101
|
+
if description:
|
|
102
|
+
config["project"]["description"] = description
|
|
103
|
+
|
|
104
|
+
if layout != "custom" and layout in _PATH_LAYOUTS:
|
|
105
|
+
config["paths"] = _PATH_LAYOUTS[layout]
|
|
106
|
+
|
|
107
|
+
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
108
|
+
print(f"\nCreated: {config_path}")
|
|
109
|
+
|
|
110
|
+
# Create models directory
|
|
111
|
+
models_dir = project_root / "models"
|
|
112
|
+
if not models_dir.exists():
|
|
113
|
+
if confirm("Create models/ directory?"):
|
|
114
|
+
models_dir.mkdir(parents=True)
|
|
115
|
+
print(f"Created: {models_dir}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _update_config(config_path: Path) -> None:
|
|
119
|
+
"""Show current config and offer updates."""
|
|
120
|
+
print("\n--- Update Project Settings ---\n")
|
|
121
|
+
|
|
122
|
+
with open(config_path) as f:
|
|
123
|
+
config = yaml.safe_load(f) or {}
|
|
124
|
+
|
|
125
|
+
print("Current configuration:")
|
|
126
|
+
print(yaml.dump(config, default_flow_style=False))
|
|
127
|
+
|
|
128
|
+
if not confirm("Update settings?", default=False):
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
project = config.get("project", {})
|
|
132
|
+
project["name"] = text("Project name:", default=project.get("name", ""))
|
|
133
|
+
project["version"] = text("Version:", default=project.get("version", "0.1.0"))
|
|
134
|
+
config["project"] = project
|
|
135
|
+
|
|
136
|
+
stacks = _scan_stacks()
|
|
137
|
+
config["stack"] = select(
|
|
138
|
+
"Stack:", choices=stacks, default=config.get("stack", "python-fastapi")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
config_path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
142
|
+
print(f"\nUpdated: {config_path}")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action: Run tests via pytest.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..prompts import confirm, select
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _find_project_root() -> Path:
|
|
15
|
+
"""Find project root by looking for .model-generator.yaml."""
|
|
16
|
+
cwd = Path.cwd()
|
|
17
|
+
if (cwd / ".model-generator.yaml").exists():
|
|
18
|
+
return cwd
|
|
19
|
+
parent = cwd.parent
|
|
20
|
+
if (parent / ".model-generator.yaml").exists():
|
|
21
|
+
return parent
|
|
22
|
+
return cwd
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _find_test_dir(project_root: Path) -> Path | None:
|
|
26
|
+
"""Find the test directory."""
|
|
27
|
+
for candidate in ["tests", "test"]:
|
|
28
|
+
test_dir = project_root / candidate
|
|
29
|
+
if test_dir.exists():
|
|
30
|
+
return test_dir
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_tests() -> None:
|
|
35
|
+
"""Run pytest with selected options."""
|
|
36
|
+
project_root = _find_project_root()
|
|
37
|
+
test_dir = _find_test_dir(project_root)
|
|
38
|
+
|
|
39
|
+
if test_dir is None:
|
|
40
|
+
print(f"\nNo tests/ directory found in {project_root}")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
mode = select(
|
|
44
|
+
"Test mode:",
|
|
45
|
+
choices=["all tests", "verbose (-v)", "with coverage"],
|
|
46
|
+
default="all tests",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
cmd = [sys.executable, "-m", "pytest", str(test_dir)]
|
|
50
|
+
|
|
51
|
+
if mode == "verbose (-v)":
|
|
52
|
+
cmd.append("-v")
|
|
53
|
+
elif mode == "with coverage":
|
|
54
|
+
cmd.extend(["--cov", "--cov-report=term-missing"])
|
|
55
|
+
|
|
56
|
+
print(f"\nRunning: {' '.join(cmd)}")
|
|
57
|
+
if not confirm("Proceed?"):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
subprocess.run(cmd, cwd=str(project_root))
|