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,403 @@
|
|
|
1
|
+
# Python FastAPI Stack Configuration
|
|
2
|
+
# Maps abstract types to SQLAlchemy/Pydantic implementations
|
|
3
|
+
#
|
|
4
|
+
# NOTE: Project-specific config (name, description) should be in:
|
|
5
|
+
# <project-root>/.model-generator.yaml
|
|
6
|
+
#
|
|
7
|
+
# See .model-generator.example.yaml in the skill directory for template.
|
|
8
|
+
|
|
9
|
+
stack:
|
|
10
|
+
name: python-fastapi
|
|
11
|
+
description: Python with FastAPI, SQLAlchemy, Pydantic
|
|
12
|
+
version: "1.0"
|
|
13
|
+
|
|
14
|
+
# Required Python packages for generated code
|
|
15
|
+
dependencies:
|
|
16
|
+
runtime:
|
|
17
|
+
- "fastapi>=0.109.0"
|
|
18
|
+
- "uvicorn>=0.27.0"
|
|
19
|
+
- "sqlalchemy>=2.0.0"
|
|
20
|
+
- "alembic>=1.13.0"
|
|
21
|
+
- "pydantic>=2.0.0"
|
|
22
|
+
- "aiosqlite>=0.19.0"
|
|
23
|
+
# Conditional: only needed when model has password fields
|
|
24
|
+
conditional:
|
|
25
|
+
password_field:
|
|
26
|
+
- "bcrypt>=4.0.0"
|
|
27
|
+
encrypted_binary:
|
|
28
|
+
- "cryptography>=42.0.0"
|
|
29
|
+
dev:
|
|
30
|
+
- "pytest>=7.0.0"
|
|
31
|
+
- "pytest-xdist>=3.0.0"
|
|
32
|
+
- "httpx>=0.26.0"
|
|
33
|
+
- "factory-boy>=3.3.0"
|
|
34
|
+
- "faker>=22.0.0"
|
|
35
|
+
- "mutmut>=3.0.0,<3.5.0"
|
|
36
|
+
|
|
37
|
+
# Output paths relative to project root
|
|
38
|
+
paths:
|
|
39
|
+
# Infrastructure (generated once)
|
|
40
|
+
base: backend/src/database/models/base.py
|
|
41
|
+
engine: backend/src/database/engine.py
|
|
42
|
+
main: backend/src/main.py
|
|
43
|
+
errors: backend/src/api/errors.py
|
|
44
|
+
validators: backend/src/api/validators.py
|
|
45
|
+
test_conftest_root: tests/conftest.py
|
|
46
|
+
# Domain-specific (generated per domain)
|
|
47
|
+
database_models: backend/src/database/models
|
|
48
|
+
factories: backend/src/database/models/factories
|
|
49
|
+
api_models: backend/src/api/models
|
|
50
|
+
api_routes: backend/src/api/routes
|
|
51
|
+
api_tests: tests/contract/api
|
|
52
|
+
enums: backend/src/database/models/enums.py
|
|
53
|
+
constraints: backend/src/database/models/constraints.py
|
|
54
|
+
migrations: alembic
|
|
55
|
+
|
|
56
|
+
# Type mappings: abstract -> concrete
|
|
57
|
+
types:
|
|
58
|
+
uuid:
|
|
59
|
+
database:
|
|
60
|
+
column: "Column(Uuid, primary_key={primary_key}, default=uuid.uuid4)"
|
|
61
|
+
imports:
|
|
62
|
+
- "from sqlalchemy import Column, Uuid"
|
|
63
|
+
- "import uuid"
|
|
64
|
+
api_response: "str | None"
|
|
65
|
+
api_request: "str"
|
|
66
|
+
|
|
67
|
+
financial:
|
|
68
|
+
database:
|
|
69
|
+
column: "Column(Numeric({precision}, {scale}), nullable={nullable}, default=Decimal(\"{default}\"))"
|
|
70
|
+
imports:
|
|
71
|
+
- "from sqlalchemy import Column, Numeric"
|
|
72
|
+
- "from decimal import Decimal"
|
|
73
|
+
api_response: "str | None"
|
|
74
|
+
api_request: "str"
|
|
75
|
+
validators:
|
|
76
|
+
- validate_non_negative_decimal
|
|
77
|
+
- validate_positive_decimal
|
|
78
|
+
|
|
79
|
+
percentage:
|
|
80
|
+
database:
|
|
81
|
+
column: "Column(Numeric(5, 4), nullable={nullable}, default=Decimal(\"{default}\"))"
|
|
82
|
+
imports:
|
|
83
|
+
- "from sqlalchemy import Column, Numeric"
|
|
84
|
+
- "from decimal import Decimal"
|
|
85
|
+
api_response: "str | None"
|
|
86
|
+
api_request: "str"
|
|
87
|
+
validators:
|
|
88
|
+
- validate_percentage
|
|
89
|
+
|
|
90
|
+
counter:
|
|
91
|
+
database:
|
|
92
|
+
column: "Column(Integer, nullable={nullable}, default={default})"
|
|
93
|
+
imports:
|
|
94
|
+
- "from sqlalchemy import Column, Integer"
|
|
95
|
+
api_response: "int | None"
|
|
96
|
+
api_request: "int"
|
|
97
|
+
validators:
|
|
98
|
+
- validate_non_negative_int
|
|
99
|
+
- validate_positive_int
|
|
100
|
+
|
|
101
|
+
# "integer" is an alias for "counter" — normalized at load time.
|
|
102
|
+
# Use "integer" for generic integers (e.g., HTTP status, nanosecond timestamps).
|
|
103
|
+
# Use "counter" for monotonically increasing values (e.g., retry counts).
|
|
104
|
+
integer:
|
|
105
|
+
database:
|
|
106
|
+
column: "Column(Integer, nullable={nullable}, default={default})"
|
|
107
|
+
imports:
|
|
108
|
+
- "from sqlalchemy import Column, Integer"
|
|
109
|
+
api_response: "int | None"
|
|
110
|
+
api_request: "int"
|
|
111
|
+
validators:
|
|
112
|
+
- validate_non_negative_int
|
|
113
|
+
- validate_positive_int
|
|
114
|
+
|
|
115
|
+
text:
|
|
116
|
+
database:
|
|
117
|
+
column: "Column(String({max_length}), nullable={nullable}, unique={unique})"
|
|
118
|
+
imports:
|
|
119
|
+
- "from sqlalchemy import Column, String"
|
|
120
|
+
api_response: "str | None"
|
|
121
|
+
api_request: "str"
|
|
122
|
+
field_args: "max_length={max_length}"
|
|
123
|
+
|
|
124
|
+
longtext:
|
|
125
|
+
database:
|
|
126
|
+
column: "Column(Text, nullable={nullable})"
|
|
127
|
+
imports:
|
|
128
|
+
- "from sqlalchemy import Column, Text"
|
|
129
|
+
api_response: "str | None"
|
|
130
|
+
api_request: "str"
|
|
131
|
+
|
|
132
|
+
boolean:
|
|
133
|
+
database:
|
|
134
|
+
column: "Column(Boolean, nullable={nullable}, default={default})"
|
|
135
|
+
imports:
|
|
136
|
+
- "from sqlalchemy import Column, Boolean"
|
|
137
|
+
api_response: "bool | None"
|
|
138
|
+
api_request: "bool"
|
|
139
|
+
|
|
140
|
+
binary:
|
|
141
|
+
database:
|
|
142
|
+
column: "Column(LargeBinary, nullable={nullable})"
|
|
143
|
+
imports:
|
|
144
|
+
- "from sqlalchemy import Column, LargeBinary"
|
|
145
|
+
api_response: "bytes | None"
|
|
146
|
+
api_request: "bytes"
|
|
147
|
+
|
|
148
|
+
datetime:
|
|
149
|
+
database:
|
|
150
|
+
column: "Column(DateTime(timezone=True), nullable={nullable})"
|
|
151
|
+
column_with_default: "Column(DateTime(timezone=True), server_default=func.now(), nullable={nullable})"
|
|
152
|
+
column_with_update: "Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable={nullable})"
|
|
153
|
+
imports:
|
|
154
|
+
- "from sqlalchemy import Column, DateTime"
|
|
155
|
+
- "from sqlalchemy.sql import func"
|
|
156
|
+
api_response: "str | None"
|
|
157
|
+
api_request: "str"
|
|
158
|
+
conversion: ".isoformat() + 'Z'"
|
|
159
|
+
|
|
160
|
+
enum:
|
|
161
|
+
database:
|
|
162
|
+
column: 'Column(SQLEnum({enum_name}, native_enum=False), nullable={nullable}, default={enum_name}.{default})'
|
|
163
|
+
imports:
|
|
164
|
+
- "from sqlalchemy import Column, Enum as SQLEnum"
|
|
165
|
+
- "from .enums import {enum_name}"
|
|
166
|
+
api_response: "{enum_name} | None"
|
|
167
|
+
api_request: "{enum_name}"
|
|
168
|
+
api_imports:
|
|
169
|
+
- "from {{{{ config.paths.database_models | path_to_import }}}}.enums import {enum_name}"
|
|
170
|
+
|
|
171
|
+
json_object:
|
|
172
|
+
database:
|
|
173
|
+
column: "Column(JSON, nullable={nullable}, default=dict)"
|
|
174
|
+
imports:
|
|
175
|
+
- "from sqlalchemy import Column, JSON"
|
|
176
|
+
api_response: "dict[str, Any] | None"
|
|
177
|
+
api_request: "dict[str, Any]"
|
|
178
|
+
api_imports:
|
|
179
|
+
- "from typing import Any"
|
|
180
|
+
|
|
181
|
+
json_array:
|
|
182
|
+
database:
|
|
183
|
+
column: "Column(JSON, nullable={nullable}, default=list)"
|
|
184
|
+
imports:
|
|
185
|
+
- "from sqlalchemy import Column, JSON"
|
|
186
|
+
api_response: "list[Any] | None"
|
|
187
|
+
api_request: "list[Any]"
|
|
188
|
+
api_imports:
|
|
189
|
+
- "from typing import Any"
|
|
190
|
+
|
|
191
|
+
reference:
|
|
192
|
+
database:
|
|
193
|
+
column: 'Column(Uuid, ForeignKey("{reference_table}.{reference_column}", ondelete="{on_delete}"), nullable={nullable}, index=True)'
|
|
194
|
+
imports:
|
|
195
|
+
- "from sqlalchemy import Column, Uuid, ForeignKey"
|
|
196
|
+
api_response: "str | None"
|
|
197
|
+
api_request: "str"
|
|
198
|
+
|
|
199
|
+
# Timestamp patterns
|
|
200
|
+
timestamps:
|
|
201
|
+
created_at:
|
|
202
|
+
column: "created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)"
|
|
203
|
+
imports:
|
|
204
|
+
- "from sqlalchemy import Column, DateTime"
|
|
205
|
+
- "from sqlalchemy.sql import func"
|
|
206
|
+
|
|
207
|
+
updated_at:
|
|
208
|
+
column: "updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)"
|
|
209
|
+
imports:
|
|
210
|
+
- "from sqlalchemy import Column, DateTime"
|
|
211
|
+
- "from sqlalchemy.sql import func"
|
|
212
|
+
|
|
213
|
+
# Constraint patterns
|
|
214
|
+
constraints:
|
|
215
|
+
non_negative:
|
|
216
|
+
check: 'CheckConstraint("{field} >= 0", name="ck_{table}_{field}_non_negative")'
|
|
217
|
+
imports:
|
|
218
|
+
- "from sqlalchemy import CheckConstraint"
|
|
219
|
+
|
|
220
|
+
positive:
|
|
221
|
+
check: 'CheckConstraint("{field} > 0", name="ck_{table}_{field}_positive")'
|
|
222
|
+
imports:
|
|
223
|
+
- "from sqlalchemy import CheckConstraint"
|
|
224
|
+
|
|
225
|
+
range:
|
|
226
|
+
check: 'CheckConstraint("{field} >= {min} AND {field} <= {max}", name="ck_{table}_{field}_range")'
|
|
227
|
+
imports:
|
|
228
|
+
- "from sqlalchemy import CheckConstraint"
|
|
229
|
+
|
|
230
|
+
length:
|
|
231
|
+
check: 'CheckConstraint("length({field}) >= {min}", name="ck_{table}_{field}_length")'
|
|
232
|
+
imports:
|
|
233
|
+
- "from sqlalchemy import CheckConstraint"
|
|
234
|
+
|
|
235
|
+
pattern:
|
|
236
|
+
# Pattern validation typically done at API level, not database
|
|
237
|
+
api_validator: true
|
|
238
|
+
|
|
239
|
+
depends:
|
|
240
|
+
check: 'CheckConstraint("{field} {operator} {other_field}", name="ck_{table}_{field}_{other_field}")'
|
|
241
|
+
imports:
|
|
242
|
+
- "from sqlalchemy import CheckConstraint"
|
|
243
|
+
|
|
244
|
+
# Index patterns
|
|
245
|
+
indexes:
|
|
246
|
+
single:
|
|
247
|
+
pattern: 'Index("ix_{table}_{field}", "{field}")'
|
|
248
|
+
imports:
|
|
249
|
+
- "from sqlalchemy import Index"
|
|
250
|
+
|
|
251
|
+
composite:
|
|
252
|
+
pattern: 'Index("ix_{table}_{fields_joined}", {fields_quoted})'
|
|
253
|
+
imports:
|
|
254
|
+
- "from sqlalchemy import Index"
|
|
255
|
+
|
|
256
|
+
unique:
|
|
257
|
+
pattern: 'UniqueConstraint({fields_quoted}, name="uq_{table}_{fields_joined}")'
|
|
258
|
+
imports:
|
|
259
|
+
- "from sqlalchemy import UniqueConstraint"
|
|
260
|
+
|
|
261
|
+
# Relationship patterns
|
|
262
|
+
relationships:
|
|
263
|
+
one_to_many:
|
|
264
|
+
parent: 'relationship("{target}", back_populates="{back_populates}", cascade="{cascade}")'
|
|
265
|
+
child_fk: 'Column(Uuid, ForeignKey("{parent_table}.id", ondelete="{on_delete}"), nullable={nullable}, index=True)'
|
|
266
|
+
child_rel: 'relationship("{parent}", back_populates="{back_populates}")'
|
|
267
|
+
|
|
268
|
+
many_to_one:
|
|
269
|
+
fk: 'Column(Uuid, ForeignKey("{target_table}.id", ondelete="{on_delete}"), nullable={nullable}, index=True)'
|
|
270
|
+
rel: 'relationship("{target}", back_populates="{back_populates}")'
|
|
271
|
+
|
|
272
|
+
one_to_one:
|
|
273
|
+
parent: 'relationship("{target}", back_populates="{back_populates}", uselist=False, cascade="{cascade}")'
|
|
274
|
+
child_fk: 'Column(Uuid, ForeignKey("{parent_table}.id", ondelete="{on_delete}"), nullable={nullable}, unique=True)'
|
|
275
|
+
child_rel: 'relationship("{parent}", back_populates="{back_populates}")'
|
|
276
|
+
|
|
277
|
+
# API patterns
|
|
278
|
+
api:
|
|
279
|
+
response_model:
|
|
280
|
+
suffix: Response
|
|
281
|
+
base_class: BaseModel
|
|
282
|
+
config: "model_config = ConfigDict(from_attributes=True)"
|
|
283
|
+
imports:
|
|
284
|
+
- "from pydantic import BaseModel, ConfigDict"
|
|
285
|
+
|
|
286
|
+
create_request:
|
|
287
|
+
prefix: Create
|
|
288
|
+
suffix: Request
|
|
289
|
+
base_class: BaseModel
|
|
290
|
+
imports:
|
|
291
|
+
- "from pydantic import BaseModel, Field"
|
|
292
|
+
|
|
293
|
+
update_request:
|
|
294
|
+
prefix: Update
|
|
295
|
+
suffix: Request
|
|
296
|
+
base_class: BaseModel
|
|
297
|
+
imports:
|
|
298
|
+
- "from pydantic import BaseModel, Field"
|
|
299
|
+
|
|
300
|
+
validators:
|
|
301
|
+
non_negative_decimal:
|
|
302
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_non_negative_decimal"
|
|
303
|
+
usage: "_validate_{field} = field_validator('{field}')(validate_non_negative_decimal)"
|
|
304
|
+
|
|
305
|
+
positive_decimal:
|
|
306
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_positive_decimal"
|
|
307
|
+
usage: "_validate_{field} = field_validator('{field}')(validate_positive_decimal)"
|
|
308
|
+
|
|
309
|
+
percentage:
|
|
310
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_percentage"
|
|
311
|
+
usage: "_validate_{field} = field_validator('{field}')(validate_percentage)"
|
|
312
|
+
|
|
313
|
+
non_negative_int:
|
|
314
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_non_negative_int"
|
|
315
|
+
usage: "_validate_{field} = field_validator('{field}')(validate_non_negative_int)"
|
|
316
|
+
|
|
317
|
+
positive_int:
|
|
318
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_positive_int"
|
|
319
|
+
usage: "_validate_{field} = field_validator('{field}')(validate_positive_int)"
|
|
320
|
+
|
|
321
|
+
timestamp_after:
|
|
322
|
+
import: "from {{{{ config.paths.api_models.rsplit('/', 1)[0] | path_to_import }}}}.validators import validate_timestamp_after"
|
|
323
|
+
usage: "validate_timestamp_after(self.{later}, self.{earlier}, '{later}', '{earlier}')"
|
|
324
|
+
|
|
325
|
+
# Naming conventions
|
|
326
|
+
naming:
|
|
327
|
+
table: snake_case
|
|
328
|
+
model: PascalCase
|
|
329
|
+
field: snake_case
|
|
330
|
+
enum: PascalCase
|
|
331
|
+
enum_value: snake_case
|
|
332
|
+
constraint: "ck_{table}_{description}"
|
|
333
|
+
index: "ix_{table}_{fields}"
|
|
334
|
+
unique: "uq_{table}_{fields}"
|
|
335
|
+
fk: "fk_{table}_{field}"
|
|
336
|
+
|
|
337
|
+
# Code style (opt-in overrides — absent keys yield tool-native defaults)
|
|
338
|
+
# Surfaced via .model-generator.yaml:
|
|
339
|
+
# style:
|
|
340
|
+
# python_version: "3.12" # drives requires-python + mypy python_version
|
|
341
|
+
# line_length: 100 # emitted as [tool.ruff] line-length
|
|
342
|
+
# quote_style: "single" # emitted as [tool.ruff.format] quote-style
|
|
343
|
+
# indent_style: "tab" # emitted as [tool.ruff.format] indent-style
|
|
344
|
+
style:
|
|
345
|
+
quote_style: double
|
|
346
|
+
indent_style: space
|
|
347
|
+
type_hints: true
|
|
348
|
+
docstrings: true
|
|
349
|
+
section_headers: true
|
|
350
|
+
section_header_format: "# ====== {section} ======"
|
|
351
|
+
|
|
352
|
+
# Quality tools
|
|
353
|
+
quality:
|
|
354
|
+
linter: "ruff check --fix ."
|
|
355
|
+
formatter: "ruff format ."
|
|
356
|
+
type_checker: "mypy . --explicit-package-bases"
|
|
357
|
+
|
|
358
|
+
# Factory configuration for test data generation
|
|
359
|
+
# Maps field name patterns to Faker methods
|
|
360
|
+
factory:
|
|
361
|
+
# Field name inference: if field name contains key, use the faker method
|
|
362
|
+
# Format: pattern: "faker_method" or pattern: {method: "faker_method", args: {...}}
|
|
363
|
+
field_name_inference:
|
|
364
|
+
email: "email()"
|
|
365
|
+
first_name: "first_name()"
|
|
366
|
+
last_name: "last_name()"
|
|
367
|
+
username: "user_name()"
|
|
368
|
+
phone: "phone_number()"
|
|
369
|
+
address: "address()"
|
|
370
|
+
company: "company()"
|
|
371
|
+
url: "url()"
|
|
372
|
+
password: "password()"
|
|
373
|
+
description: "text(max_nb_chars=200)"
|
|
374
|
+
notes: "text(max_nb_chars=200)"
|
|
375
|
+
title: "sentence(nb_words=4)"
|
|
376
|
+
name: "name()"
|
|
377
|
+
city: "city()"
|
|
378
|
+
country: "country()"
|
|
379
|
+
state: "state()"
|
|
380
|
+
zip_code: "postcode()"
|
|
381
|
+
postal_code: "postcode()"
|
|
382
|
+
credit_card: "credit_card_number()"
|
|
383
|
+
ssn: "ssn()"
|
|
384
|
+
ip_address: "ipv4()"
|
|
385
|
+
mac_address: "mac_address()"
|
|
386
|
+
user_agent: "user_agent()"
|
|
387
|
+
file_name: "file_name()"
|
|
388
|
+
file_path: "file_path()"
|
|
389
|
+
mime_type: "mime_type()"
|
|
390
|
+
slug: "slug()"
|
|
391
|
+
color: "color_name()"
|
|
392
|
+
hex_color: "hex_color()"
|
|
393
|
+
latitude: "latitude()"
|
|
394
|
+
longitude: "longitude()"
|
|
395
|
+
currency_code: "currency_code()"
|
|
396
|
+
locale: "locale()"
|
|
397
|
+
language_code: "language_code()"
|
|
398
|
+
job: "job()"
|
|
399
|
+
job_title: "job()"
|
|
400
|
+
|
|
401
|
+
# Default text length when no max_length specified
|
|
402
|
+
default_text_length: 50
|
|
403
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
Base Macros
|
|
3
|
+
Common header and formatting macros used across all templates.
|
|
4
|
+
-#}
|
|
5
|
+
|
|
6
|
+
{#- Generated file marker - consistent warning across all templates -#}
|
|
7
|
+
{% macro generated_marker() -%}
|
|
8
|
+
# THIS CODE WAS INITIALLY GENERATED BY model-generator
|
|
9
|
+
{%- endmacro %}
|
|
10
|
+
|
|
11
|
+
{#- Full-width section divider (for major file sections) -#}
|
|
12
|
+
{% macro section_divider(title) -%}
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# {{ title | upper }}
|
|
15
|
+
# ============================================================================
|
|
16
|
+
{%- endmacro %}
|
|
17
|
+
|
|
18
|
+
{#- Inline section header (for class subsections, indented) -#}
|
|
19
|
+
{% macro section_header(title) -%}
|
|
20
|
+
# ====== {{ title }} ======
|
|
21
|
+
{%- endmacro %}
|
|
22
|
+
|
|
23
|
+
{#- Banner-style section (for routes/tests, centered title) -#}
|
|
24
|
+
{% macro section_banner(title) -%}
|
|
25
|
+
# ==================== {{ title | upper }} ====================
|
|
26
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
Entity Utility Macros
|
|
3
|
+
Primary key extraction, name transformations, and entity utilities.
|
|
4
|
+
-#}
|
|
5
|
+
|
|
6
|
+
{#- Get the primary key field name for an entity -#}
|
|
7
|
+
{% macro get_primary_key_field(entity) -%}
|
|
8
|
+
{%- for field_name, field in entity.fields.items() -%}
|
|
9
|
+
{%- if field.primary_key | default(false) -%}
|
|
10
|
+
{{ field.api_field_name | default(field_name) }}
|
|
11
|
+
{%- endif -%}
|
|
12
|
+
{%- endfor -%}
|
|
13
|
+
{%- endmacro %}
|
|
14
|
+
|
|
15
|
+
{#- Pluralize a name (simple English rules) -#}
|
|
16
|
+
{#- Handles: y -> ies, s/x/ch/sh -> es, otherwise -> s -#}
|
|
17
|
+
{% macro pluralize(name) -%}
|
|
18
|
+
{%- if name.endswith('y') and name[-2:-1] not in 'aeiou' -%}
|
|
19
|
+
{{ name[:-1] }}ies
|
|
20
|
+
{%- elif name.endswith(('s', 'x', 'ch', 'sh')) -%}
|
|
21
|
+
{{ name }}es
|
|
22
|
+
{%- else -%}
|
|
23
|
+
{{ name }}s
|
|
24
|
+
{%- endif -%}
|
|
25
|
+
{%- endmacro %}
|
|
26
|
+
|
|
27
|
+
{#- Convert snake_case to kebab-case for URL paths -#}
|
|
28
|
+
{% macro to_kebab_case(name) -%}
|
|
29
|
+
{{ name | replace('_', '-') }}
|
|
30
|
+
{%- endmacro %}
|
|
31
|
+
|
|
32
|
+
{#- Convert CamelCase to snake_case -#}
|
|
33
|
+
{% macro to_snake_case(name) -%}
|
|
34
|
+
{%- set ns = namespace(result='') -%}
|
|
35
|
+
{%- for char in name -%}
|
|
36
|
+
{%- if char.isupper() and loop.index0 > 0 -%}
|
|
37
|
+
{%- set ns.result = ns.result ~ '_' ~ char.lower() -%}
|
|
38
|
+
{%- else -%}
|
|
39
|
+
{%- set ns.result = ns.result ~ char.lower() -%}
|
|
40
|
+
{%- endif -%}
|
|
41
|
+
{%- endfor -%}
|
|
42
|
+
{{ ns.result }}
|
|
43
|
+
{%- endmacro %}
|
|
44
|
+
|
|
45
|
+
{#- Get entity display name (snake_case to Title Case) -#}
|
|
46
|
+
{% macro entity_display_name(entity_name) -%}
|
|
47
|
+
{{ entity_name.replace('_', ' ').title() }}
|
|
48
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
OpenAPI Example Generation Macros
|
|
3
|
+
Generates sample values for API documentation.
|
|
4
|
+
-#}
|
|
5
|
+
|
|
6
|
+
{#- Generate an example value for a field -#}
|
|
7
|
+
{#- for_update: use alternative values for update requests -#}
|
|
8
|
+
{#- enums: shared enums dict for resolving enum field values -#}
|
|
9
|
+
{% macro example_value(field, field_name, for_update=false, enums=none) -%}
|
|
10
|
+
{%- set api_name = field.api_field_name | default(field_name) -%}
|
|
11
|
+
{%- if field.type == 'uuid' -%}
|
|
12
|
+
"123e4567-e89b-12d3-a456-426614174000"
|
|
13
|
+
{%- elif field.type == 'text' and 'email' in field_name.lower() -%}
|
|
14
|
+
"{% if for_update %}updated.{% endif %}user@example.com"
|
|
15
|
+
{%- elif field.type == 'text' and field_name == 'username' -%}
|
|
16
|
+
"{% if for_update %}updated_{% endif %}john_doe"
|
|
17
|
+
{%- elif field.type == 'text' and 'name' in field_name.lower() -%}
|
|
18
|
+
"{% if for_update %}Updated {% endif %}{{ field_name.replace('_', ' ').title() }}"
|
|
19
|
+
{%- elif field.type == 'text' -%}
|
|
20
|
+
"{% if for_update %}updated_{% endif %}{{ field_name }}_example"
|
|
21
|
+
{%- elif field.type == 'longtext' -%}
|
|
22
|
+
"This is a {% if for_update %}updated {% endif %}longer text field with more content."
|
|
23
|
+
{%- elif field.type == 'reference' -%}
|
|
24
|
+
"ref-123e4567"
|
|
25
|
+
{%- elif field.type == 'financial' -%}
|
|
26
|
+
"{% if for_update %}2000{% else %}1000{% endif %}"
|
|
27
|
+
{%- elif field.type == 'percentage' -%}
|
|
28
|
+
"{% if for_update %}0.2{% else %}0.15{% endif %}"
|
|
29
|
+
{%- elif field.type == 'counter' -%}
|
|
30
|
+
{% if for_update %}84{% else %}42{% endif %}
|
|
31
|
+
{%- elif field.type == 'boolean' -%}
|
|
32
|
+
{% if field.default == false %}false{% else %}true{% endif %}
|
|
33
|
+
{%- elif field.type == 'datetime' -%}
|
|
34
|
+
"2025-12-31T23:59:59Z"
|
|
35
|
+
{%- elif field.type == 'enum' -%}
|
|
36
|
+
{%- set enum_val = field.default | default('') -%}
|
|
37
|
+
{%- if not enum_val and field.enum_name is defined and enums is mapping and field.enum_name in enums -%}
|
|
38
|
+
{%- set ev = enums[field.enum_name].get('values', []) -%}
|
|
39
|
+
{%- if ev -%}
|
|
40
|
+
{%- set first = ev[1] if for_update and ev | length > 1 else ev[0] -%}
|
|
41
|
+
{%- set enum_val = first.value if first is mapping else first -%}
|
|
42
|
+
{%- endif -%}
|
|
43
|
+
{%- endif -%}
|
|
44
|
+
"{{ (enum_val or 'UNKNOWN') | upper }}"
|
|
45
|
+
{%- elif field.type == 'json_object' -%}
|
|
46
|
+
{"key": "value", "nested": {"data": 123}}
|
|
47
|
+
{%- elif field.type == 'json_array' -%}
|
|
48
|
+
["item1", "item2", "item3"]
|
|
49
|
+
{%- endif -%}
|
|
50
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
Field Utility Macros
|
|
3
|
+
Type hint generation and field detection utilities.
|
|
4
|
+
-#}
|
|
5
|
+
|
|
6
|
+
{#- Check if a field is required (either explicitly or as non-auto-generated PK) -#}
|
|
7
|
+
{% macro is_field_required(field) -%}
|
|
8
|
+
{%- if field.required or (field.primary_key | default(false) and not field.auto_generate | default(true)) -%}
|
|
9
|
+
true
|
|
10
|
+
{%- endif -%}
|
|
11
|
+
{%- endmacro %}
|
|
12
|
+
|
|
13
|
+
{#- Generate Python type hint for a field -#}
|
|
14
|
+
{#- for_response: response models use | None, request models use Optional or default -#}
|
|
15
|
+
{% macro python_type_hint(field, field_name, for_response=false) -%}
|
|
16
|
+
{%- if field.type == 'uuid' -%}
|
|
17
|
+
str{{ ' | None' if for_response else '' }}
|
|
18
|
+
{%- elif field.type in ['text', 'longtext'] -%}
|
|
19
|
+
str{{ ' | None' if for_response else '' }}
|
|
20
|
+
{%- elif field.type == 'reference' -%}
|
|
21
|
+
str{{ ' | None' if for_response else '' }}
|
|
22
|
+
{%- elif field.type in ['financial', 'percentage'] -%}
|
|
23
|
+
str{{ ' | None' if for_response else '' }}
|
|
24
|
+
{%- elif field.type == 'counter' -%}
|
|
25
|
+
int{{ ' | None' if for_response else '' }}
|
|
26
|
+
{%- elif field.type == 'boolean' -%}
|
|
27
|
+
bool{{ ' | None' if for_response else '' }}
|
|
28
|
+
{%- elif field.type == 'datetime' -%}
|
|
29
|
+
str{{ ' | None' if for_response else '' }}
|
|
30
|
+
{%- elif field.type == 'enum' -%}
|
|
31
|
+
{{ field.enum_name }}{{ ' | None' if for_response else '' }}
|
|
32
|
+
{%- elif field.type == 'json_object' -%}
|
|
33
|
+
dict[str, Any]{{ ' | None' if for_response else '' }}
|
|
34
|
+
{%- elif field.type == 'json_array' -%}
|
|
35
|
+
list[{{ field.list_type | default('Any') }}]{{ ' | None' if for_response else '' }}
|
|
36
|
+
{%- else -%}
|
|
37
|
+
Any
|
|
38
|
+
{%- endif -%}
|
|
39
|
+
{%- endmacro %}
|
|
40
|
+
|
|
41
|
+
{#- Check if any fields need the Any type import -#}
|
|
42
|
+
{% macro needs_any_import(entity) -%}
|
|
43
|
+
{%- for field_name, field in entity.fields.items() -%}
|
|
44
|
+
{%- if field.type in ['json_object', 'json_array'] -%}
|
|
45
|
+
true
|
|
46
|
+
{%- endif -%}
|
|
47
|
+
{%- endfor -%}
|
|
48
|
+
{%- endmacro %}
|