model-generator-kit 0.1.1__tar.gz → 0.1.3__tar.gz
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_kit-0.1.1 → model_generator_kit-0.1.3}/PKG-INFO +2 -2
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/README.md +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/pyproject.toml +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/__init__.py +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/request.py.j2 +21 -21
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/response.py.j2 +14 -2
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/route.py.j2 +7 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/request_limit.py.j2 +20 -3
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/validators.py.j2 +17 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/PKG-INFO +2 -2
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_generators.py +353 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/LICENSE +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/setup.cfg +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/api.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/constraints.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/database.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/enums.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/infrastructure.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/migrations.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/py.typed +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/schema/model.schema.json +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/config.yaml +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_base.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_entity.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_examples.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_fields.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_tests.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/init.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/pagination.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/constraints.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/enums.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/factory.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/init.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/model.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/auth_router.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/base.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/csrf.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/database_init.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/encrypted_bytes.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/engine.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/errors.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/gitignore.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/main.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/pyproject.toml.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/rate_limit.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/types.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/utils.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/env.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/ini.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/script.py.mako.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/tests/conftest_root.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/tests/contract.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/conftest_generator.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/constants.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/loaders.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/parser.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/quality.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/templates.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/validate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/clean.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/generate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/project_setup.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/test_runner.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/menu.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/prompts.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/SOURCES.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/dependency_links.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/entry_points.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/requires.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/top_level.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_cleanup.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_cli.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_edge_cases.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_enum_examples.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_full_generation.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_integration.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_template_utils.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_utils.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_validate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_validation.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model-generator-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: One-shot bootstrap generator for database models, API models, routes, and tests
|
|
5
5
|
Author-email: nuncaeslupus <imarcos@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -140,4 +140,4 @@ Define specifications once in JSON. Generate production-ready scaffolds. Then ma
|
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
-
**Model Generator** | Bootstrap Tool for API Backends | v0.1.
|
|
143
|
+
**Model Generator** | Bootstrap Tool for API Backends | v0.1.3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "model-generator-kit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "One-shot bootstrap generator for database models, API models, routes, and tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -57,11 +57,13 @@ This module contains:
|
|
|
57
57
|
{% endif %}
|
|
58
58
|
{#- Check for financial/counter fields with constraints needing validators -#}
|
|
59
59
|
{% if not field.api_exclude_create | default(false) or not field.api_exclude_update | default(false) %}
|
|
60
|
-
{% if field.type == 'financial'
|
|
61
|
-
{
|
|
62
|
-
{%
|
|
63
|
-
{% if
|
|
64
|
-
{%
|
|
60
|
+
{% if field.type == 'financial' %}
|
|
61
|
+
{#- Choose validator by declared constraint: non-negative when constrained, signed otherwise -#}
|
|
62
|
+
{% set ctypes = (field.constraints | map(attribute='type') | list) if (field.constraints is defined and field.constraints) else [] %}
|
|
63
|
+
{% if 'non_negative' in ctypes or 'non_negative_or_null' in ctypes %}{% set fin_validator = 'validate_non_negative_decimal' %}
|
|
64
|
+
{% elif 'positive' in ctypes or 'positive_or_null' in ctypes %}{% set fin_validator = 'validate_positive_decimal' %}
|
|
65
|
+
{% else %}{% set fin_validator = 'validate_decimal' %}{% endif %}
|
|
66
|
+
{% if fin_validator not in ns.needs_validators %}{% set _ = ns.needs_validators.append(fin_validator) %}{% endif %}
|
|
65
67
|
{% elif field.type == 'percentage' %}
|
|
66
68
|
{% if 'validate_percentage' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_percentage') %}{% endif %}
|
|
67
69
|
{% elif field.type == 'counter' and field.constraints is defined %}
|
|
@@ -271,14 +273,13 @@ class Create{{ entity_name }}Request(BaseModel):
|
|
|
271
273
|
{#- Collect constraint-driven validators (financial, percentage, counter) -#}
|
|
272
274
|
{% for item in create_fields %}
|
|
273
275
|
{% set field = item.field %}
|
|
274
|
-
{% if field.type == 'financial'
|
|
275
|
-
{
|
|
276
|
-
{%
|
|
277
|
-
{%
|
|
278
|
-
{% elif
|
|
279
|
-
{% set
|
|
280
|
-
{%
|
|
281
|
-
{% endfor %}
|
|
276
|
+
{% if field.type == 'financial' %}
|
|
277
|
+
{#- Choose validator by declared constraint: non-negative when constrained, signed otherwise -#}
|
|
278
|
+
{% set ctypes = (field.constraints | map(attribute='type') | list) if (field.constraints is defined and field.constraints) else [] %}
|
|
279
|
+
{% if 'non_negative' in ctypes or 'non_negative_or_null' in ctypes %}{% set fin_validator = 'validate_non_negative_decimal' %}
|
|
280
|
+
{% elif 'positive' in ctypes or 'positive_or_null' in ctypes %}{% set fin_validator = 'validate_positive_decimal' %}
|
|
281
|
+
{% else %}{% set fin_validator = 'validate_decimal' %}{% endif %}
|
|
282
|
+
{% set _ = field_validators.append({'name': item.name, 'validator': fin_validator}) %}
|
|
282
283
|
{% elif field.type == 'percentage' %}
|
|
283
284
|
{% set _ = field_validators.append({'name': item.name, 'validator': 'validate_percentage'}) %}
|
|
284
285
|
{% elif field.type == 'counter' and field.constraints is defined %}
|
|
@@ -416,14 +417,13 @@ class Update{{ entity_name }}Request(BaseModel):
|
|
|
416
417
|
{% set update_validators = [] %}
|
|
417
418
|
{% for item in update_fields %}
|
|
418
419
|
{% set field = item.field %}
|
|
419
|
-
{% if field.type == 'financial'
|
|
420
|
-
{
|
|
421
|
-
{%
|
|
422
|
-
{%
|
|
423
|
-
{% elif
|
|
424
|
-
{% set
|
|
425
|
-
{%
|
|
426
|
-
{% endfor %}
|
|
420
|
+
{% if field.type == 'financial' %}
|
|
421
|
+
{#- Choose validator by declared constraint: non-negative when constrained, signed otherwise -#}
|
|
422
|
+
{% set ctypes = (field.constraints | map(attribute='type') | list) if (field.constraints is defined and field.constraints) else [] %}
|
|
423
|
+
{% if 'non_negative' in ctypes or 'non_negative_or_null' in ctypes %}{% set fin_validator = 'validate_non_negative_decimal' %}
|
|
424
|
+
{% elif 'positive' in ctypes or 'positive_or_null' in ctypes %}{% set fin_validator = 'validate_positive_decimal' %}
|
|
425
|
+
{% else %}{% set fin_validator = 'validate_decimal' %}{% endif %}
|
|
426
|
+
{% set _ = update_validators.append({'name': item.name, 'validator': fin_validator}) %}
|
|
427
427
|
{% elif field.type == 'percentage' %}
|
|
428
428
|
{% set _ = update_validators.append({'name': item.name, 'validator': 'validate_percentage'}) %}
|
|
429
429
|
{% elif field.type == 'counter' and field.constraints is defined %}
|
|
@@ -26,7 +26,14 @@ This module contains:
|
|
|
26
26
|
{% if not field.api_exclude_response | default(false) %}
|
|
27
27
|
{% if field.type in ['json_object', 'json_array'] %}{% set ns.has_any = true %}{% endif %}
|
|
28
28
|
{% if field.type == 'enum' and field.enum_name not in ns.enum_imports %}{% set _ = ns.enum_imports.append(field.enum_name) %}{% endif %}
|
|
29
|
-
{% if field.type == 'financial'
|
|
29
|
+
{% if field.type == 'financial' %}
|
|
30
|
+
{#- Choose validator by declared constraint: non-negative when constrained, signed otherwise -#}
|
|
31
|
+
{% set ctypes = (field.constraints | map(attribute='type') | list) if (field.constraints is defined and field.constraints) else [] %}
|
|
32
|
+
{% if 'non_negative' in ctypes or 'non_negative_or_null' in ctypes %}{% set fin_validator = 'validate_non_negative_decimal' %}
|
|
33
|
+
{% elif 'positive' in ctypes or 'positive_or_null' in ctypes %}{% set fin_validator = 'validate_positive_decimal' %}
|
|
34
|
+
{% else %}{% set fin_validator = 'validate_decimal' %}{% endif %}
|
|
35
|
+
{% if fin_validator not in ns.needs_validators %}{% set _ = ns.needs_validators.append(fin_validator) %}{% endif %}
|
|
36
|
+
{% endif %}
|
|
30
37
|
{% if field.type == 'percentage' and 'validate_percentage' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_percentage') %}{% endif %}
|
|
31
38
|
{% if field.type == 'counter' and 'validate_non_negative_int' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_non_negative_int') %}{% endif %}
|
|
32
39
|
{% endif %}
|
|
@@ -173,7 +180,12 @@ class {{ entity_name }}Response(BaseModel):
|
|
|
173
180
|
{% for field_name, field in entity.fields.items() %}
|
|
174
181
|
{% if not field.api_exclude_response | default(false) %}
|
|
175
182
|
{% if field.type == 'financial' %}
|
|
176
|
-
{
|
|
183
|
+
{#- Choose validator by declared constraint: non-negative when constrained, signed otherwise -#}
|
|
184
|
+
{% set ctypes = (field.constraints | map(attribute='type') | list) if (field.constraints is defined and field.constraints) else [] %}
|
|
185
|
+
{% if 'non_negative' in ctypes or 'non_negative_or_null' in ctypes %}{% set fin_validator = 'validate_non_negative_decimal' %}
|
|
186
|
+
{% elif 'positive' in ctypes or 'positive_or_null' in ctypes %}{% set fin_validator = 'validate_positive_decimal' %}
|
|
187
|
+
{% else %}{% set fin_validator = 'validate_decimal' %}{% endif %}
|
|
188
|
+
{% set _ = validated_fields.append({'name': field_name, 'validator': fin_validator}) %}
|
|
177
189
|
{% elif field.type == 'percentage' %}
|
|
178
190
|
{% set _ = validated_fields.append({'name': field_name, 'validator': 'validate_percentage'}) %}
|
|
179
191
|
{% elif field.type == 'counter' %}
|
|
@@ -75,7 +75,7 @@ All endpoints follow RESTful conventions and return standardized error responses
|
|
|
75
75
|
import bcrypt
|
|
76
76
|
{% endif %}
|
|
77
77
|
{% if ns.has_datetime %}
|
|
78
|
-
from datetime import datetime
|
|
78
|
+
from datetime import datetime, timezone
|
|
79
79
|
{% endif %}
|
|
80
80
|
{% if ns.has_financial or ns.has_percentage %}
|
|
81
81
|
from decimal import Decimal
|
|
@@ -225,9 +225,15 @@ async def list_{{ entity_plural }}(
|
|
|
225
225
|
count_stmt = count_stmt.where({{ entity_name }}.{{ field_name }} == {{ field_name }})
|
|
226
226
|
{% elif field.type == 'datetime' %}
|
|
227
227
|
if {{ field_name }}_after is not None:
|
|
228
|
+
# Localize a naive value to UTC: the column is tz-aware, and comparing
|
|
229
|
+
# it against a naive datetime raises on strict drivers (asyncpg/psycopg2).
|
|
230
|
+
if {{ field_name }}_after.tzinfo is None:
|
|
231
|
+
{{ field_name }}_after = {{ field_name }}_after.replace(tzinfo=timezone.utc)
|
|
228
232
|
stmt = stmt.where({{ entity_name }}.{{ field_name }} >= {{ field_name }}_after)
|
|
229
233
|
count_stmt = count_stmt.where({{ entity_name }}.{{ field_name }} >= {{ field_name }}_after)
|
|
230
234
|
if {{ field_name }}_before is not None:
|
|
235
|
+
if {{ field_name }}_before.tzinfo is None:
|
|
236
|
+
{{ field_name }}_before = {{ field_name }}_before.replace(tzinfo=timezone.utc)
|
|
231
237
|
stmt = stmt.where({{ entity_name }}.{{ field_name }} <= {{ field_name }}_before)
|
|
232
238
|
count_stmt = count_stmt.where({{ entity_name }}.{{ field_name }} <= {{ field_name }}_before)
|
|
233
239
|
{% elif field.type in ['financial', 'percentage'] %}
|
|
@@ -97,11 +97,28 @@ class RequestBodySizeLimitMiddleware:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def _content_length(scope: Scope) -> int | None:
|
|
100
|
-
"""Return the request's Content-Length as an int, or None if absent/invalid.
|
|
100
|
+
"""Return the request's Content-Length as an int, or None if absent/invalid.
|
|
101
|
+
|
|
102
|
+
A declared length is treated as invalid (returns None) — forcing the caller
|
|
103
|
+
onto the chunked byte-counting path, which is safe regardless of any header
|
|
104
|
+
— when it is negative or when *more than one* Content-Length header is
|
|
105
|
+
present. Multiple Content-Length headers are a request-smuggling signal: a
|
|
106
|
+
downstream server or proxy might honor a different one than this middleware,
|
|
107
|
+
so an oversized request could slip past a guard keyed on the first value.
|
|
108
|
+
Compliant servers reject both cases at the protocol layer, but the
|
|
109
|
+
middleware must not rely on that pre-filtering — defense-in-depth.
|
|
110
|
+
"""
|
|
111
|
+
found: int | None = None
|
|
101
112
|
for name, value in scope.get("headers", []):
|
|
102
113
|
if name == b"content-length":
|
|
114
|
+
if found is not None:
|
|
115
|
+
# Duplicate Content-Length headers: force the chunked path.
|
|
116
|
+
return None
|
|
103
117
|
try:
|
|
104
|
-
|
|
118
|
+
n = int(value)
|
|
105
119
|
except ValueError:
|
|
106
120
|
return None
|
|
107
|
-
|
|
121
|
+
if n < 0:
|
|
122
|
+
return None
|
|
123
|
+
found = n
|
|
124
|
+
return found
|
|
@@ -23,6 +23,23 @@ from typing import Any
|
|
|
23
23
|
{{ section_divider('DECIMAL VALIDATORS') }}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def validate_decimal(value: Any) -> str | None:
|
|
27
|
+
"""Validate that a value is a well-formed decimal (sign-agnostic).
|
|
28
|
+
|
|
29
|
+
Accepts negative, zero, and positive values. Use this for signed
|
|
30
|
+
financial fields (e.g. PnL, returns) that may legitimately be negative.
|
|
31
|
+
"""
|
|
32
|
+
if value is None:
|
|
33
|
+
return value
|
|
34
|
+
try:
|
|
35
|
+
decimal_value = Decimal(str(value))
|
|
36
|
+
except InvalidOperation:
|
|
37
|
+
raise ValueError("Invalid decimal value") from None
|
|
38
|
+
if not decimal_value.is_finite():
|
|
39
|
+
raise ValueError("Value must be a finite decimal")
|
|
40
|
+
return str(value)
|
|
41
|
+
|
|
42
|
+
|
|
26
43
|
def validate_non_negative_decimal(value: Any) -> str | None:
|
|
27
44
|
"""Validate that a decimal value is non-negative (>= 0)."""
|
|
28
45
|
if value is None:
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model-generator-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: One-shot bootstrap generator for database models, API models, routes, and tests
|
|
5
5
|
Author-email: nuncaeslupus <imarcos@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -140,4 +140,4 @@ Define specifications once in JSON. Generate production-ready scaffolds. Then ma
|
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
-
**Model Generator** | Bootstrap Tool for API Backends | v0.1.
|
|
143
|
+
**Model Generator** | Bootstrap Tool for API Backends | v0.1.3
|
|
@@ -1106,6 +1106,193 @@ class TestApiModelsGeneratorPerEntity:
|
|
|
1106
1106
|
assert "CreatePostRequest" not in author_req
|
|
1107
1107
|
|
|
1108
1108
|
|
|
1109
|
+
@pytest.fixture
|
|
1110
|
+
def financial_model() -> dict[str, Any]:
|
|
1111
|
+
"""Single entity exercising signed vs constrained financial fields.
|
|
1112
|
+
|
|
1113
|
+
- unrealized_pnl: no constraint -> signed -> validate_decimal
|
|
1114
|
+
- balance: non_negative -> validate_non_negative_decimal
|
|
1115
|
+
- deposit: positive -> validate_positive_decimal
|
|
1116
|
+
"""
|
|
1117
|
+
return {
|
|
1118
|
+
"domain": "ledger",
|
|
1119
|
+
"description": "Ledger domain with signed and constrained money",
|
|
1120
|
+
"entities": {
|
|
1121
|
+
"Account": {
|
|
1122
|
+
"table": "accounts",
|
|
1123
|
+
"description": "Trading account",
|
|
1124
|
+
"fields": {
|
|
1125
|
+
"id": {
|
|
1126
|
+
"type": "uuid",
|
|
1127
|
+
"primary_key": True,
|
|
1128
|
+
"auto_generate": True,
|
|
1129
|
+
},
|
|
1130
|
+
"unrealized_pnl": {
|
|
1131
|
+
"type": "financial",
|
|
1132
|
+
"required": True,
|
|
1133
|
+
},
|
|
1134
|
+
"balance": {
|
|
1135
|
+
"type": "financial",
|
|
1136
|
+
"required": True,
|
|
1137
|
+
"constraints": [{"type": "non_negative"}],
|
|
1138
|
+
},
|
|
1139
|
+
"deposit": {
|
|
1140
|
+
"type": "financial",
|
|
1141
|
+
"required": True,
|
|
1142
|
+
"constraints": [{"type": "positive"}],
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
"timestamps": {"created": True, "updated": True},
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
class TestFinancialValidatorSelection:
|
|
1152
|
+
"""Signed financial fields must not be forced non-negative.
|
|
1153
|
+
|
|
1154
|
+
Regression: the response/request templates hardcoded
|
|
1155
|
+
validate_non_negative_decimal for every financial field, so any
|
|
1156
|
+
legitimately negative value (PnL, returns) failed validation.
|
|
1157
|
+
"""
|
|
1158
|
+
|
|
1159
|
+
def _content(self, model: dict[str, Any], env_fixture: Any, suffix: str) -> str:
|
|
1160
|
+
project_root, config, env = env_fixture
|
|
1161
|
+
result = generate_api_models(model, config, env, project_root)
|
|
1162
|
+
assert isinstance(result, list)
|
|
1163
|
+
content = next(r["content"] for r in result if r["path"].name.endswith(suffix))
|
|
1164
|
+
assert isinstance(content, str)
|
|
1165
|
+
return content
|
|
1166
|
+
|
|
1167
|
+
def test_response_signed_financial_uses_validate_decimal(
|
|
1168
|
+
self, financial_model: dict[str, Any], project_env_per_entity: Any
|
|
1169
|
+
) -> None:
|
|
1170
|
+
content = self._content(
|
|
1171
|
+
financial_model, project_env_per_entity, "account_response.py"
|
|
1172
|
+
)
|
|
1173
|
+
# Unconstrained financial -> signed validator
|
|
1174
|
+
assert (
|
|
1175
|
+
'_validate_unrealized_pnl = field_validator("unrealized_pnl")'
|
|
1176
|
+
"(validate_decimal)" in content
|
|
1177
|
+
)
|
|
1178
|
+
# Constrained financial -> stays non-negative / positive
|
|
1179
|
+
assert (
|
|
1180
|
+
'_validate_balance = field_validator("balance")'
|
|
1181
|
+
"(validate_non_negative_decimal)" in content
|
|
1182
|
+
)
|
|
1183
|
+
assert (
|
|
1184
|
+
'_validate_deposit = field_validator("deposit")'
|
|
1185
|
+
"(validate_positive_decimal)" in content
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
def test_response_imports_match_validators_used(
|
|
1189
|
+
self, financial_model: dict[str, Any], project_env_per_entity: Any
|
|
1190
|
+
) -> None:
|
|
1191
|
+
content = self._content(
|
|
1192
|
+
financial_model, project_env_per_entity, "account_response.py"
|
|
1193
|
+
)
|
|
1194
|
+
import_line = next(
|
|
1195
|
+
line
|
|
1196
|
+
for line in content.splitlines()
|
|
1197
|
+
if line.startswith("from") and "validators import" in line
|
|
1198
|
+
)
|
|
1199
|
+
assert "validate_decimal" in import_line
|
|
1200
|
+
assert "validate_non_negative_decimal" in import_line
|
|
1201
|
+
assert "validate_positive_decimal" in import_line
|
|
1202
|
+
|
|
1203
|
+
def test_request_signed_financial_uses_validate_decimal(
|
|
1204
|
+
self, financial_model: dict[str, Any], project_env_per_entity: Any
|
|
1205
|
+
) -> None:
|
|
1206
|
+
content = self._content(
|
|
1207
|
+
financial_model, project_env_per_entity, "account_requests.py"
|
|
1208
|
+
)
|
|
1209
|
+
assert (
|
|
1210
|
+
'_validate_unrealized_pnl = field_validator("unrealized_pnl")'
|
|
1211
|
+
"(validate_decimal)" in content
|
|
1212
|
+
)
|
|
1213
|
+
assert (
|
|
1214
|
+
'_validate_balance = field_validator("balance")'
|
|
1215
|
+
"(validate_non_negative_decimal)" in content
|
|
1216
|
+
)
|
|
1217
|
+
assert (
|
|
1218
|
+
'_validate_deposit = field_validator("deposit")'
|
|
1219
|
+
"(validate_positive_decimal)" in content
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
def test_request_imports_match_validators_used(
|
|
1223
|
+
self, financial_model: dict[str, Any], project_env_per_entity: Any
|
|
1224
|
+
) -> None:
|
|
1225
|
+
content = self._content(
|
|
1226
|
+
financial_model, project_env_per_entity, "account_requests.py"
|
|
1227
|
+
)
|
|
1228
|
+
import_line = next(
|
|
1229
|
+
line
|
|
1230
|
+
for line in content.splitlines()
|
|
1231
|
+
if line.startswith("from") and "validators import" in line
|
|
1232
|
+
)
|
|
1233
|
+
assert "validate_decimal" in import_line
|
|
1234
|
+
assert "validate_non_negative_decimal" in import_line
|
|
1235
|
+
assert "validate_positive_decimal" in import_line
|
|
1236
|
+
|
|
1237
|
+
def test_validators_module_defines_validate_decimal(
|
|
1238
|
+
self, project_env_per_entity: Any
|
|
1239
|
+
) -> None:
|
|
1240
|
+
project_root, config, env = project_env_per_entity
|
|
1241
|
+
result = generate_validators(config, env, project_root)
|
|
1242
|
+
assert result is not None
|
|
1243
|
+
assert "def validate_decimal(value: Any) -> str | None:" in result["content"]
|
|
1244
|
+
|
|
1245
|
+
def test_validate_decimal_rejects_non_finite(
|
|
1246
|
+
self, project_env_per_entity: Any
|
|
1247
|
+
) -> None:
|
|
1248
|
+
"""The emitted validate_decimal must reject NaN/Infinity, not just
|
|
1249
|
+
malformed strings (financial fields cannot store non-finite values)."""
|
|
1250
|
+
project_root, config, env = project_env_per_entity
|
|
1251
|
+
result = generate_validators(config, env, project_root)
|
|
1252
|
+
assert result is not None
|
|
1253
|
+
ns: dict[str, Any] = {}
|
|
1254
|
+
exec(result["content"], ns)
|
|
1255
|
+
validate_decimal = ns["validate_decimal"]
|
|
1256
|
+
# Signed finite values pass.
|
|
1257
|
+
assert validate_decimal("-12.5") == "-12.5"
|
|
1258
|
+
assert validate_decimal(None) is None
|
|
1259
|
+
# Non-finite values are rejected.
|
|
1260
|
+
for bad in ("NaN", "Infinity", "-Infinity", "sNaN"):
|
|
1261
|
+
with pytest.raises(ValueError):
|
|
1262
|
+
validate_decimal(bad)
|
|
1263
|
+
with pytest.raises(ValueError):
|
|
1264
|
+
validate_decimal("not-a-number")
|
|
1265
|
+
|
|
1266
|
+
def test_empty_constraints_fall_back_to_signed(
|
|
1267
|
+
self, project_env_per_entity: Any
|
|
1268
|
+
) -> None:
|
|
1269
|
+
"""A financial field with an empty (falsy) `constraints` list falls
|
|
1270
|
+
back to the signed default rather than crashing on the `map` filter."""
|
|
1271
|
+
model = {
|
|
1272
|
+
"domain": "ledger",
|
|
1273
|
+
"entities": {
|
|
1274
|
+
"Account": {
|
|
1275
|
+
"table": "accounts",
|
|
1276
|
+
"fields": {
|
|
1277
|
+
"id": {
|
|
1278
|
+
"type": "uuid",
|
|
1279
|
+
"primary_key": True,
|
|
1280
|
+
"auto_generate": True,
|
|
1281
|
+
},
|
|
1282
|
+
"pnl": {
|
|
1283
|
+
"type": "financial",
|
|
1284
|
+
"required": True,
|
|
1285
|
+
"constraints": [],
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
}
|
|
1291
|
+
for suffix in ("account_response.py", "account_requests.py"):
|
|
1292
|
+
content = self._content(model, project_env_per_entity, suffix)
|
|
1293
|
+
assert '_validate_pnl = field_validator("pnl")(validate_decimal)' in content
|
|
1294
|
+
|
|
1295
|
+
|
|
1109
1296
|
class TestGenerateApiInitPerEntity:
|
|
1110
1297
|
"""Per-entity api __init__.py emission."""
|
|
1111
1298
|
|
|
@@ -1652,6 +1839,58 @@ class TestApiRoutesFilterCoercion:
|
|
|
1652
1839
|
assert "from datetime import datetime" in content
|
|
1653
1840
|
|
|
1654
1841
|
|
|
1842
|
+
class TestApiRoutesDatetimeFilterTzAware:
|
|
1843
|
+
"""Naive datetime filter values are localized to UTC before comparison.
|
|
1844
|
+
|
|
1845
|
+
A ``datetime | None`` filter parses input without a tz offset (e.g.
|
|
1846
|
+
``2026-06-11T12:00:00``) as a naive datetime; compared against a tz-aware
|
|
1847
|
+
``DateTime(timezone=True)`` column it raises ``TypeError``/``DataError`` on
|
|
1848
|
+
strict drivers (asyncpg/psycopg2). The handler localizes a naive value to
|
|
1849
|
+
UTC first. Latent (SQLite suites don't enforce tz-awareness), not a
|
|
1850
|
+
regression — fixed uniformly for both ``_after`` and ``_before``.
|
|
1851
|
+
"""
|
|
1852
|
+
|
|
1853
|
+
def _render(self, model: dict[str, Any], project_env: Any) -> str:
|
|
1854
|
+
project_root, config, env = project_env
|
|
1855
|
+
result = generate_api_routes(
|
|
1856
|
+
model, config, env, project_root, enums={}, constraints={}
|
|
1857
|
+
)
|
|
1858
|
+
assert isinstance(result, dict)
|
|
1859
|
+
return str(result["content"])
|
|
1860
|
+
|
|
1861
|
+
def test_imports_timezone(
|
|
1862
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1863
|
+
) -> None:
|
|
1864
|
+
content = self._render(filter_model, project_env)
|
|
1865
|
+
assert "from datetime import datetime, timezone" in content
|
|
1866
|
+
|
|
1867
|
+
def test_naive_after_localized_to_utc(
|
|
1868
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1869
|
+
) -> None:
|
|
1870
|
+
content = self._render(filter_model, project_env)
|
|
1871
|
+
assert "if observed_at_after.tzinfo is None:" in content
|
|
1872
|
+
assert (
|
|
1873
|
+
"observed_at_after = observed_at_after.replace(tzinfo=timezone.utc)"
|
|
1874
|
+
in content
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
def test_naive_before_localized_to_utc(
|
|
1878
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1879
|
+
) -> None:
|
|
1880
|
+
content = self._render(filter_model, project_env)
|
|
1881
|
+
assert "if observed_at_before.tzinfo is None:" in content
|
|
1882
|
+
assert (
|
|
1883
|
+
"observed_at_before = observed_at_before.replace(tzinfo=timezone.utc)"
|
|
1884
|
+
in content
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
def test_route_still_compiles(
|
|
1888
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1889
|
+
) -> None:
|
|
1890
|
+
content = self._render(filter_model, project_env)
|
|
1891
|
+
compile(content, "<metric_route>", "exec")
|
|
1892
|
+
|
|
1893
|
+
|
|
1655
1894
|
class TestValidateAuthConfig:
|
|
1656
1895
|
"""Test the _validate_auth_config helper."""
|
|
1657
1896
|
|
|
@@ -3738,6 +3977,120 @@ class TestRequestLimitGenerator:
|
|
|
3738
3977
|
# Default cap (>0) still applies -> middleware still emitted, no crash.
|
|
3739
3978
|
assert isinstance(generate_request_limit(config, env, project_root), dict)
|
|
3740
3979
|
|
|
3980
|
+
def test_negative_content_length_treated_as_invalid(
|
|
3981
|
+
self, project_env: Any, monkeypatch: pytest.MonkeyPatch
|
|
3982
|
+
) -> None:
|
|
3983
|
+
"""Defense-in-depth: a negative Content-Length must not bypass the cap.
|
|
3984
|
+
|
|
3985
|
+
``int(b"-100")`` used to be returned verbatim, so ``-100 > max`` was
|
|
3986
|
+
False and the request streamed through uncounted. Now a negative length
|
|
3987
|
+
is invalid (``None``), so the request falls through to the chunked
|
|
3988
|
+
byte-counting path and is still rejected on overflow.
|
|
3989
|
+
"""
|
|
3990
|
+
import asyncio
|
|
3991
|
+
import sys
|
|
3992
|
+
import types as types_module
|
|
3993
|
+
|
|
3994
|
+
project_root, config, env = project_env
|
|
3995
|
+
result = generate_request_limit(config, env, project_root)
|
|
3996
|
+
assert isinstance(result, dict)
|
|
3997
|
+
content = result["content"]
|
|
3998
|
+
# Template-level guard: a negative declared length is invalid.
|
|
3999
|
+
assert "if n < 0:" in content
|
|
4000
|
+
|
|
4001
|
+
# Runtime probe: drive the middleware with a lying Content-Length: -100
|
|
4002
|
+
# and an oversized body; the cap must still produce a 413. starlette is
|
|
4003
|
+
# not a generator dependency, so stub its type-only import (auto-undone
|
|
4004
|
+
# by the monkeypatch fixture).
|
|
4005
|
+
starlette = types_module.ModuleType("starlette")
|
|
4006
|
+
starlette_types = types_module.ModuleType("starlette.types")
|
|
4007
|
+
for name in ("ASGIApp", "Message", "Receive", "Scope", "Send"):
|
|
4008
|
+
setattr(starlette_types, name, Any)
|
|
4009
|
+
starlette.types = starlette_types # type: ignore[attr-defined]
|
|
4010
|
+
monkeypatch.setitem(sys.modules, "starlette", starlette)
|
|
4011
|
+
monkeypatch.setitem(sys.modules, "starlette.types", starlette_types)
|
|
4012
|
+
|
|
4013
|
+
ns: dict[str, Any] = {}
|
|
4014
|
+
exec(content, ns)
|
|
4015
|
+
middleware_cls = ns["RequestBodySizeLimitMiddleware"]
|
|
4016
|
+
|
|
4017
|
+
async def app(scope: Any, receive: Any, send: Any) -> None:
|
|
4018
|
+
raise AssertionError("oversized body reached the app")
|
|
4019
|
+
|
|
4020
|
+
mw = middleware_cls(app, max_body_bytes=10)
|
|
4021
|
+
scope = {"type": "http", "headers": [(b"content-length", b"-100")]}
|
|
4022
|
+
|
|
4023
|
+
async def receive() -> dict[str, Any]:
|
|
4024
|
+
return {"type": "http.request", "body": b"x" * 100, "more_body": False}
|
|
4025
|
+
|
|
4026
|
+
sent: list[dict[str, Any]] = []
|
|
4027
|
+
|
|
4028
|
+
async def send(message: dict[str, Any]) -> None:
|
|
4029
|
+
sent.append(message)
|
|
4030
|
+
|
|
4031
|
+
asyncio.run(mw(scope, receive, send))
|
|
4032
|
+
|
|
4033
|
+
start = next(m for m in sent if m["type"] == "http.response.start")
|
|
4034
|
+
assert start["status"] == 413
|
|
4035
|
+
|
|
4036
|
+
def test_duplicate_content_length_treated_as_invalid(
|
|
4037
|
+
self, project_env: Any, monkeypatch: pytest.MonkeyPatch
|
|
4038
|
+
) -> None:
|
|
4039
|
+
"""Defense-in-depth: duplicate Content-Length headers must not bypass the cap.
|
|
4040
|
+
|
|
4041
|
+
Returning the first header's value lets a smuggling pair (small + large)
|
|
4042
|
+
slip an oversized body past a guard keyed on the small one if a
|
|
4043
|
+
downstream server honors the other. Two Content-Length headers are now
|
|
4044
|
+
treated as invalid, forcing the chunked byte-counting path → 413.
|
|
4045
|
+
"""
|
|
4046
|
+
import asyncio
|
|
4047
|
+
import sys
|
|
4048
|
+
import types as types_module
|
|
4049
|
+
|
|
4050
|
+
project_root, config, env = project_env
|
|
4051
|
+
result = generate_request_limit(config, env, project_root)
|
|
4052
|
+
assert isinstance(result, dict)
|
|
4053
|
+
content = result["content"]
|
|
4054
|
+
|
|
4055
|
+
starlette = types_module.ModuleType("starlette")
|
|
4056
|
+
starlette_types = types_module.ModuleType("starlette.types")
|
|
4057
|
+
for name in ("ASGIApp", "Message", "Receive", "Scope", "Send"):
|
|
4058
|
+
setattr(starlette_types, name, Any)
|
|
4059
|
+
starlette.types = starlette_types # type: ignore[attr-defined]
|
|
4060
|
+
monkeypatch.setitem(sys.modules, "starlette", starlette)
|
|
4061
|
+
monkeypatch.setitem(sys.modules, "starlette.types", starlette_types)
|
|
4062
|
+
|
|
4063
|
+
ns: dict[str, Any] = {}
|
|
4064
|
+
exec(content, ns)
|
|
4065
|
+
middleware_cls = ns["RequestBodySizeLimitMiddleware"]
|
|
4066
|
+
|
|
4067
|
+
async def app(scope: Any, receive: Any, send: Any) -> None:
|
|
4068
|
+
raise AssertionError("oversized body reached the app")
|
|
4069
|
+
|
|
4070
|
+
mw = middleware_cls(app, max_body_bytes=10)
|
|
4071
|
+
# Smuggling pair: a small declared length the guard would accept, plus a
|
|
4072
|
+
# second header. The middleware must distrust both and count bytes.
|
|
4073
|
+
scope = {
|
|
4074
|
+
"type": "http",
|
|
4075
|
+
"headers": [
|
|
4076
|
+
(b"content-length", b"5"),
|
|
4077
|
+
(b"content-length", b"100"),
|
|
4078
|
+
],
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
async def receive() -> dict[str, Any]:
|
|
4082
|
+
return {"type": "http.request", "body": b"x" * 100, "more_body": False}
|
|
4083
|
+
|
|
4084
|
+
sent: list[dict[str, Any]] = []
|
|
4085
|
+
|
|
4086
|
+
async def send(message: dict[str, Any]) -> None:
|
|
4087
|
+
sent.append(message)
|
|
4088
|
+
|
|
4089
|
+
asyncio.run(mw(scope, receive, send))
|
|
4090
|
+
|
|
4091
|
+
start = next(m for m in sent if m["type"] == "http.response.start")
|
|
4092
|
+
assert start["status"] == 413
|
|
4093
|
+
|
|
3741
4094
|
|
|
3742
4095
|
class TestImmutableEntityGeneration:
|
|
3743
4096
|
"""Test generation for immutable entities (no update endpoint)."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/__init__.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/api.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/database.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/enums.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/generators/migrations.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/schema/model.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/constants.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/loaders.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/quality.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/utils/templates.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/clean.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator/wizard/prompts.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|