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.
Files changed (68) hide show
  1. model_generator/__init__.py +6 -0
  2. model_generator/generate.py +1030 -0
  3. model_generator/generators/__init__.py +38 -0
  4. model_generator/generators/api.py +287 -0
  5. model_generator/generators/constraints.py +176 -0
  6. model_generator/generators/database.py +147 -0
  7. model_generator/generators/enums.py +88 -0
  8. model_generator/generators/infrastructure.py +679 -0
  9. model_generator/generators/migrations.py +146 -0
  10. model_generator/py.typed +0 -0
  11. model_generator/schema/model.schema.json +758 -0
  12. model_generator/stacks/python-fastapi/config.yaml +403 -0
  13. model_generator/stacks/python-fastapi/templates/_shared/_base.j2 +26 -0
  14. model_generator/stacks/python-fastapi/templates/_shared/_entity.j2 +48 -0
  15. model_generator/stacks/python-fastapi/templates/_shared/_examples.j2 +50 -0
  16. model_generator/stacks/python-fastapi/templates/_shared/_fields.j2 +48 -0
  17. model_generator/stacks/python-fastapi/templates/_shared/_tests.j2 +143 -0
  18. model_generator/stacks/python-fastapi/templates/api/init.py.j2 +55 -0
  19. model_generator/stacks/python-fastapi/templates/api/pagination.py.j2 +79 -0
  20. model_generator/stacks/python-fastapi/templates/api/request.py.j2 +448 -0
  21. model_generator/stacks/python-fastapi/templates/api/response.py.j2 +222 -0
  22. model_generator/stacks/python-fastapi/templates/api/route.py.j2 +507 -0
  23. model_generator/stacks/python-fastapi/templates/database/constraints.py.j2 +439 -0
  24. model_generator/stacks/python-fastapi/templates/database/enums.py.j2 +55 -0
  25. model_generator/stacks/python-fastapi/templates/database/factory.py.j2 +265 -0
  26. model_generator/stacks/python-fastapi/templates/database/init.py.j2 +37 -0
  27. model_generator/stacks/python-fastapi/templates/database/model.py.j2 +476 -0
  28. model_generator/stacks/python-fastapi/templates/infrastructure/auth_router.py.j2 +434 -0
  29. model_generator/stacks/python-fastapi/templates/infrastructure/base.py.j2 +16 -0
  30. model_generator/stacks/python-fastapi/templates/infrastructure/csrf.py.j2 +121 -0
  31. model_generator/stacks/python-fastapi/templates/infrastructure/database_init.py.j2 +12 -0
  32. model_generator/stacks/python-fastapi/templates/infrastructure/encrypted_bytes.py.j2 +62 -0
  33. model_generator/stacks/python-fastapi/templates/infrastructure/engine.py.j2 +51 -0
  34. model_generator/stacks/python-fastapi/templates/infrastructure/errors.py.j2 +74 -0
  35. model_generator/stacks/python-fastapi/templates/infrastructure/gitignore.j2 +48 -0
  36. model_generator/stacks/python-fastapi/templates/infrastructure/main.py.j2 +94 -0
  37. model_generator/stacks/python-fastapi/templates/infrastructure/pyproject.toml.j2 +92 -0
  38. model_generator/stacks/python-fastapi/templates/infrastructure/rate_limit.py.j2 +41 -0
  39. model_generator/stacks/python-fastapi/templates/infrastructure/types.py.j2 +94 -0
  40. model_generator/stacks/python-fastapi/templates/infrastructure/utils.py.j2 +50 -0
  41. model_generator/stacks/python-fastapi/templates/infrastructure/validators.py.j2 +126 -0
  42. model_generator/stacks/python-fastapi/templates/migrations/env.py.j2 +125 -0
  43. model_generator/stacks/python-fastapi/templates/migrations/ini.j2 +109 -0
  44. model_generator/stacks/python-fastapi/templates/migrations/script.py.mako.j2 +35 -0
  45. model_generator/stacks/python-fastapi/templates/tests/conftest_root.py.j2 +122 -0
  46. model_generator/stacks/python-fastapi/templates/tests/contract.py.j2 +1860 -0
  47. model_generator/utils/__init__.py +31 -0
  48. model_generator/utils/conftest_generator.py +683 -0
  49. model_generator/utils/constants.py +6 -0
  50. model_generator/utils/loaders.py +292 -0
  51. model_generator/utils/parser.py +129 -0
  52. model_generator/utils/quality.py +43 -0
  53. model_generator/utils/templates.py +128 -0
  54. model_generator/validate.py +219 -0
  55. model_generator/wizard/__init__.py +10 -0
  56. model_generator/wizard/actions/__init__.py +1 -0
  57. model_generator/wizard/actions/clean.py +55 -0
  58. model_generator/wizard/actions/generate.py +166 -0
  59. model_generator/wizard/actions/project_setup.py +142 -0
  60. model_generator/wizard/actions/test_runner.py +60 -0
  61. model_generator/wizard/menu.py +43 -0
  62. model_generator/wizard/prompts.py +80 -0
  63. model_generator_kit-0.1.0.dist-info/METADATA +143 -0
  64. model_generator_kit-0.1.0.dist-info/RECORD +68 -0
  65. model_generator_kit-0.1.0.dist-info/WHEEL +5 -0
  66. model_generator_kit-0.1.0.dist-info/entry_points.txt +3 -0
  67. model_generator_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
  68. 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
+ )