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,143 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
Test Utility Macros
|
|
3
|
+
Shared macros for contract test generation.
|
|
4
|
+
-#}
|
|
5
|
+
|
|
6
|
+
{#- Macro to check if entity needs unique_suffix -#}
|
|
7
|
+
{% macro needs_unique_suffix(entity) -%}
|
|
8
|
+
{% for field_name, field in entity.fields.items() %}
|
|
9
|
+
{% if not field.api_exclude_request | default(false) %}
|
|
10
|
+
{% if field.required or (field.primary_key | default(false) and not field.auto_generate | default(true)) %}
|
|
11
|
+
{% if field.type == 'text' and (field.unique | default(false) or 'email' in field_name.lower()) %}
|
|
12
|
+
true
|
|
13
|
+
{%- elif field.primary_key | default(false) and not field.auto_generate | default(true) %}
|
|
14
|
+
true
|
|
15
|
+
{%- endif %}
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% endif %}
|
|
18
|
+
{% endfor %}
|
|
19
|
+
{%- endmacro %}
|
|
20
|
+
|
|
21
|
+
{#- Macro to get proper enum value -#}
|
|
22
|
+
{% macro get_enum_value(enum_name, field_default='', alternative=false, enums=none) -%}
|
|
23
|
+
{%- if field_default and not alternative -%}
|
|
24
|
+
{{ field_default | upper }}
|
|
25
|
+
{%- elif enums and enum_name in enums -%}
|
|
26
|
+
{%- set enum_def = enums[enum_name] -%}
|
|
27
|
+
{%- set enum_values = enum_def.get('values', []) -%}
|
|
28
|
+
{%- if enum_values and enum_values|length > 0 -%}
|
|
29
|
+
{%- if alternative and enum_values|length > 1 -%}
|
|
30
|
+
{%- set selected_value = enum_values[1] -%}
|
|
31
|
+
{%- else -%}
|
|
32
|
+
{%- set selected_value = enum_values[0] -%}
|
|
33
|
+
{%- endif -%}
|
|
34
|
+
{%- if selected_value is string -%}
|
|
35
|
+
{{ selected_value | upper }}
|
|
36
|
+
{%- elif selected_value is mapping and 'value' in selected_value -%}
|
|
37
|
+
{{ selected_value.value | upper }}
|
|
38
|
+
{%- else -%}
|
|
39
|
+
active
|
|
40
|
+
{%- endif -%}
|
|
41
|
+
{%- else -%}
|
|
42
|
+
active
|
|
43
|
+
{%- endif -%}
|
|
44
|
+
{%- else -%}
|
|
45
|
+
active
|
|
46
|
+
{%- endif -%}
|
|
47
|
+
{%- endmacro %}
|
|
48
|
+
|
|
49
|
+
{#- Macro to format financial test values -#}
|
|
50
|
+
{% macro format_test_financial(value, scale=8) -%}
|
|
51
|
+
{{- value | normalize_decimal -}}
|
|
52
|
+
{%- endmacro %}
|
|
53
|
+
|
|
54
|
+
{#- Macro to check if a test scenario should be generated -#}
|
|
55
|
+
{% macro should_generate_test(entity, scenario_keywords) -%}
|
|
56
|
+
{%- if entity.tests and entity.tests.scenarios -%}
|
|
57
|
+
{%- set keywords_list = scenario_keywords if scenario_keywords is iterable and scenario_keywords is not string else [scenario_keywords] -%}
|
|
58
|
+
{%- for scenario in entity.tests.scenarios -%}
|
|
59
|
+
{%- for keyword in keywords_list -%}
|
|
60
|
+
{%- if keyword in scenario -%}true{% endif -%}
|
|
61
|
+
{%- endfor -%}
|
|
62
|
+
{%- endfor -%}
|
|
63
|
+
{%- else -%}
|
|
64
|
+
true
|
|
65
|
+
{%- endif -%}
|
|
66
|
+
{%- endmacro %}
|
|
67
|
+
|
|
68
|
+
{#- ============================================================================
|
|
69
|
+
MACRO: build_test_data_fields
|
|
70
|
+
============================================================================
|
|
71
|
+
Generates test data field assignments for an entity.
|
|
72
|
+
|
|
73
|
+
This macro consolidates the repetitive test data generation pattern used
|
|
74
|
+
across many test methods (POST, PUT, filtering, validation tests, etc.).
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
- entity: The entity definition
|
|
78
|
+
- field_to_fixture: Mapping of FK field names to fixture names
|
|
79
|
+
- prefix: Prefix for unique field values (e.g., 'filter', 'minimal', 'update')
|
|
80
|
+
- skip_field: Field name to skip (for missing required field tests)
|
|
81
|
+
- only_required: If true, only include required fields (default: true)
|
|
82
|
+
- constraints: Global constraints dict for range validation
|
|
83
|
+
- enums: Global enums dict for enum values
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
{{ entity_name.lower() }}_data: dict[str, Any] = {
|
|
87
|
+
{{ build_test_data_fields(entity, field_to_fixture, 'test', '', true, constraints, enums) }}
|
|
88
|
+
}
|
|
89
|
+
-#}
|
|
90
|
+
{% macro build_test_data_fields(entity, field_to_fixture, prefix='test', skip_field='', only_required=true, constraints=none, enums=none) -%}
|
|
91
|
+
{% for field_name, field in entity.fields.items() %}
|
|
92
|
+
{% if field_name != skip_field and not field.api_exclude_request | default(false) %}
|
|
93
|
+
{% set is_required = field.required or (field.primary_key | default(false) and not field.auto_generate | default(false)) %}
|
|
94
|
+
{% if is_required or not only_required %}
|
|
95
|
+
{% if field.type == 'reference' %}
|
|
96
|
+
"{{ field.api_field_name | default(field_name) }}": {{ field_to_fixture[field_name] }},
|
|
97
|
+
{% elif field.type == 'text' and 'email' in field_name.lower() %}
|
|
98
|
+
"{{ field.api_field_name | default(field_name) }}": f"{{ prefix }}.{unique_suffix}@example.com",
|
|
99
|
+
{% elif field.type == 'text' and (field.unique | default(false) or (field.primary_key | default(false) and not field.auto_generate | default(false))) %}
|
|
100
|
+
"{{ field.api_field_name | default(field_name) }}": f"{{ prefix }}_{unique_suffix}",
|
|
101
|
+
{% elif field.type == 'text' or field.type == 'longtext' %}
|
|
102
|
+
"{{ field.api_field_name | default(field_name) }}": "{% if field.max_length is defined and field.max_length <= 3 %}{{ prefix[:3] }}{% elif field.max_length is defined and field.max_length <= 10 %}{{ prefix[:10] }}{% else %}{{ prefix }}_value{% endif %}",
|
|
103
|
+
{% elif field.type == 'financial' %}
|
|
104
|
+
{% set has_range = namespace(found=false) %}{% if field.constraints %}{% for constraint in field.constraints %}{% if constraint.type in ['range', 'range_or_null'] %}{% set has_range.found = true %}{% if constraint.max is defined and constraint.max | float <= 1.0 %} "{{ field.api_field_name | default(field_name) }}": "{{ (constraint.max | float / 2) | normalize_decimal }}",
|
|
105
|
+
{% elif constraint.max is defined and constraint.max | float < 100.0 %} "{{ field.api_field_name | default(field_name) }}": "{{ (constraint.max | float / 2) | normalize_decimal }}",
|
|
106
|
+
{% elif constraint.max_ref is defined and constraints and constraint.max_ref in constraints %}{% set max_val = constraints[constraint.max_ref]['value'] | float %}{% if max_val <= 1.0 %} "{{ field.api_field_name | default(field_name) }}": "{{ 0.5 | normalize_decimal }}",
|
|
107
|
+
{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 100 | normalize_decimal }}",
|
|
108
|
+
{% endif %}{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 100 | normalize_decimal }}",
|
|
109
|
+
{% endif %}{% endif %}{% endfor %}{% if not has_range.found %} "{{ field.api_field_name | default(field_name) }}": "{{ 100 | normalize_decimal }}",
|
|
110
|
+
{% endif %}{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 100 | normalize_decimal }}",
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% elif field.type == 'percentage' %}
|
|
113
|
+
{% set has_range = namespace(found=false) %}{% if field.constraints %}{% for constraint in field.constraints %}{% if constraint.type in ['range', 'range_or_null'] %}{% set has_range.found = true %}{% if constraint.max is defined and constraint.max | float <= 0.05 %} "{{ field.api_field_name | default(field_name) }}": "{{ (constraint.max | float / 2) | normalize_decimal }}",
|
|
114
|
+
{% elif constraint.max_ref is defined and constraints and constraint.max_ref in constraints %}{% set max_val = constraints[constraint.max_ref]['value'] | float %}{% if max_val <= 0.05 %} "{{ field.api_field_name | default(field_name) }}": "{{ (max_val / 2) | normalize_decimal }}",
|
|
115
|
+
{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 0.15 | normalize_decimal }}",
|
|
116
|
+
{% endif %}{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 0.15 | normalize_decimal }}",
|
|
117
|
+
{% endif %}{% endif %}{% endfor %}{% if not has_range.found %} "{{ field.api_field_name | default(field_name) }}": "{{ 0.15 | normalize_decimal }}",
|
|
118
|
+
{% endif %}{% else %} "{{ field.api_field_name | default(field_name) }}": "{{ 0.15 | normalize_decimal }}",
|
|
119
|
+
{% endif %}
|
|
120
|
+
{% elif field.type == 'counter' %}
|
|
121
|
+
{% set cn = namespace(found=false, min=none, max=none) %}{% if field.constraints %}{% for constraint in field.constraints %}{% if constraint.type == 'range' %}{% set cn.found = true %}{% if constraint.min is defined %}{% set cn.min = constraint.min | int %}{% elif constraint.min_ref is defined and constraints and constraint.min_ref in constraints %}{% set cn.min = constraints[constraint.min_ref]['value'] | int %}{% endif %}{% if constraint.max is defined %}{% set cn.max = constraint.max | int %}{% elif constraint.max_ref is defined and constraints and constraint.max_ref in constraints %}{% set cn.max = constraints[constraint.max_ref]['value'] | int %}{% endif %}{% endif %}{% endfor %}{% if cn.found %}{% if cn.min is not none and cn.max is not none %}{% set mid_val = (cn.min + cn.max) // 2 %}{% elif cn.min is not none %}{% set mid_val = cn.min %}{% elif cn.max is not none %}{% set mid_val = cn.max // 2 %}{% else %}{% set mid_val = 10 %}{% endif %} "{{ field.api_field_name | default(field_name) }}": {{ mid_val }},
|
|
122
|
+
{% else %} "{{ field.api_field_name | default(field_name) }}": 10,
|
|
123
|
+
{% endif %}{% else %} "{{ field.api_field_name | default(field_name) }}": 10,
|
|
124
|
+
{% endif %}
|
|
125
|
+
{% elif field.type == 'boolean' %}
|
|
126
|
+
"{{ field.api_field_name | default(field_name) }}": True,
|
|
127
|
+
{% elif field.type == 'enum' %}
|
|
128
|
+
"{{ field.api_field_name | default(field_name) }}": "{{ get_enum_value(field.enum_name, field.default, false, enums) }}",
|
|
129
|
+
{% elif field.type == 'datetime' %}
|
|
130
|
+
{% if 'end' in field_name or '_to' in field_name %}
|
|
131
|
+
"{{ field.api_field_name | default(field_name) }}": "2100-01-02T00:00:00Z",
|
|
132
|
+
{% else %}
|
|
133
|
+
"{{ field.api_field_name | default(field_name) }}": "2099-12-31T23:59:59Z",
|
|
134
|
+
{% endif %}
|
|
135
|
+
{% elif field.type == 'json_array' %}
|
|
136
|
+
"{{ field.api_field_name | default(field_name) }}": ["item1", "item2"],
|
|
137
|
+
{% elif field.type == 'json_object' %}
|
|
138
|
+
"{{ field.api_field_name | default(field_name) }}": {"key": "value"},
|
|
139
|
+
{% endif %}
|
|
140
|
+
{% endif %}
|
|
141
|
+
{% endif %}
|
|
142
|
+
{% endfor %}
|
|
143
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
__init__.py Template for API Models Package
|
|
3
|
+
Generates exports for all response and request models across domain files.
|
|
4
|
+
|
|
5
|
+
This template receives:
|
|
6
|
+
- domains: List of domain files with their entities and model types
|
|
7
|
+
- config: Stack configuration
|
|
8
|
+
-#}
|
|
9
|
+
{% from "_shared/_base.j2" import generated_marker, section_divider %}
|
|
10
|
+
{{ generated_marker() }}
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
Application API Models.
|
|
14
|
+
|
|
15
|
+
This package contains all Pydantic models for API requests and responses.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
{% for domain in domains %}
|
|
19
|
+
{% if domain.section %}
|
|
20
|
+
# {{ domain.section }}
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if domain.response_models %}
|
|
23
|
+
from .{{ domain.name }}_response import (
|
|
24
|
+
{% for model in domain.response_models | sort %}
|
|
25
|
+
{{ model }},
|
|
26
|
+
{% endfor %}
|
|
27
|
+
)
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% if domain.request_models %}
|
|
30
|
+
from .{{ domain.name }}_requests import (
|
|
31
|
+
{% for model in domain.request_models | sort %}
|
|
32
|
+
{{ model }},
|
|
33
|
+
{% endfor %}
|
|
34
|
+
)
|
|
35
|
+
{% endif %}
|
|
36
|
+
|
|
37
|
+
{% endfor %}
|
|
38
|
+
{% set all_models = [] %}
|
|
39
|
+
{% for domain in domains %}
|
|
40
|
+
{% if domain.response_models %}
|
|
41
|
+
{% for model in domain.response_models %}
|
|
42
|
+
{% set _ = all_models.append(model) %}
|
|
43
|
+
{% endfor %}
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% if domain.request_models %}
|
|
46
|
+
{% for model in domain.request_models %}
|
|
47
|
+
{% set _ = all_models.append(model) %}
|
|
48
|
+
{% endfor %}
|
|
49
|
+
{% endif %}
|
|
50
|
+
{% endfor %}
|
|
51
|
+
__all__ = [
|
|
52
|
+
{% for model in all_models | sort %}
|
|
53
|
+
"{{ model }}",
|
|
54
|
+
{% endfor %}
|
|
55
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
API Pagination Models Template
|
|
3
|
+
Generates reusable pagination models for all API endpoints
|
|
4
|
+
-#}
|
|
5
|
+
{% from "_shared/_base.j2" import generated_marker %}
|
|
6
|
+
{{ generated_marker() }}
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Pagination models for API responses.
|
|
10
|
+
|
|
11
|
+
Provides standardized pagination support for all list endpoints:
|
|
12
|
+
- Page-based pagination (page, page_size)
|
|
13
|
+
- Total counts and navigation helpers (has_next, has_prev)
|
|
14
|
+
- Generic PaginatedResponse[T] for type safety
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Generic, TypeVar
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Generic type variable for paginated items
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PageInfo(BaseModel):
|
|
26
|
+
"""Pagination metadata."""
|
|
27
|
+
|
|
28
|
+
page: int = Field(..., description="Current page number (1-indexed)", ge=1)
|
|
29
|
+
page_size: int = Field(..., description="Number of items per page", ge=1, le=1000)
|
|
30
|
+
total_items: int = Field(..., description="Total number of items across all pages", ge=0)
|
|
31
|
+
total_pages: int = Field(..., description="Total number of pages", ge=0)
|
|
32
|
+
has_next: bool = Field(..., description="Whether there is a next page")
|
|
33
|
+
has_prev: bool = Field(..., description="Whether there is a previous page")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PaginatedResponse[T](BaseModel): # noqa: UP046
|
|
37
|
+
"""
|
|
38
|
+
Generic paginated response wrapper.
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
PaginatedResponse[UserResponse](
|
|
42
|
+
items=[user1, user2],
|
|
43
|
+
page_info=PageInfo(...)
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
items: list[T] = Field(..., description="List of items for current page")
|
|
48
|
+
page_info: PageInfo = Field(..., description="Pagination metadata")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Helper function for calculating pagination metadata
|
|
52
|
+
def calculate_page_info(
|
|
53
|
+
page: int,
|
|
54
|
+
page_size: int,
|
|
55
|
+
total_items: int,
|
|
56
|
+
) -> PageInfo:
|
|
57
|
+
"""
|
|
58
|
+
Calculate pagination metadata.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
page: Current page number (1-indexed)
|
|
62
|
+
page_size: Number of items per page
|
|
63
|
+
total_items: Total number of items available
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
PageInfo with calculated values
|
|
67
|
+
"""
|
|
68
|
+
total_pages = (total_items + page_size - 1) // page_size if total_items > 0 else 0
|
|
69
|
+
has_next = page < total_pages
|
|
70
|
+
has_prev = page > 1
|
|
71
|
+
|
|
72
|
+
return PageInfo(
|
|
73
|
+
page=page,
|
|
74
|
+
page_size=page_size,
|
|
75
|
+
total_items=total_items,
|
|
76
|
+
total_pages=total_pages,
|
|
77
|
+
has_next=has_next,
|
|
78
|
+
has_prev=has_prev,
|
|
79
|
+
)
|