model-generator-kit 0.1.2__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.
Files changed (86) hide show
  1. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/PKG-INFO +2 -2
  2. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/README.md +1 -1
  3. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/pyproject.toml +1 -1
  4. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/__init__.py +1 -1
  5. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/request.py.j2 +21 -21
  6. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/response.py.j2 +14 -2
  7. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/validators.py.j2 +17 -0
  8. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/PKG-INFO +2 -2
  9. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_generators.py +187 -0
  10. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/LICENSE +0 -0
  11. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/setup.cfg +0 -0
  12. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generate.py +0 -0
  13. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/__init__.py +0 -0
  14. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/api.py +0 -0
  15. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/constraints.py +0 -0
  16. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/database.py +0 -0
  17. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/enums.py +0 -0
  18. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/infrastructure.py +0 -0
  19. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/generators/migrations.py +0 -0
  20. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/py.typed +0 -0
  21. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/schema/model.schema.json +0 -0
  22. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/config.yaml +0 -0
  23. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_base.j2 +0 -0
  24. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_entity.j2 +0 -0
  25. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_examples.j2 +0 -0
  26. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_fields.j2 +0 -0
  27. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/_shared/_tests.j2 +0 -0
  28. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/init.py.j2 +0 -0
  29. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/pagination.py.j2 +0 -0
  30. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/api/route.py.j2 +0 -0
  31. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/constraints.py.j2 +0 -0
  32. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/enums.py.j2 +0 -0
  33. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/factory.py.j2 +0 -0
  34. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/init.py.j2 +0 -0
  35. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/database/model.py.j2 +0 -0
  36. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/auth_router.py.j2 +0 -0
  37. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/base.py.j2 +0 -0
  38. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/csrf.py.j2 +0 -0
  39. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/database_init.py.j2 +0 -0
  40. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/encrypted_bytes.py.j2 +0 -0
  41. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/engine.py.j2 +0 -0
  42. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/errors.py.j2 +0 -0
  43. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/gitignore.j2 +0 -0
  44. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/main.py.j2 +0 -0
  45. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/pyproject.toml.j2 +0 -0
  46. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/rate_limit.py.j2 +0 -0
  47. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/request_limit.py.j2 +0 -0
  48. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/types.py.j2 +0 -0
  49. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/infrastructure/utils.py.j2 +0 -0
  50. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/env.py.j2 +0 -0
  51. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/ini.j2 +0 -0
  52. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/migrations/script.py.mako.j2 +0 -0
  53. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/tests/conftest_root.py.j2 +0 -0
  54. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/stacks/python-fastapi/templates/tests/contract.py.j2 +0 -0
  55. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/__init__.py +0 -0
  56. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/conftest_generator.py +0 -0
  57. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/constants.py +0 -0
  58. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/loaders.py +0 -0
  59. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/parser.py +0 -0
  60. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/quality.py +0 -0
  61. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/utils/templates.py +0 -0
  62. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/validate.py +0 -0
  63. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/__init__.py +0 -0
  64. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/__init__.py +0 -0
  65. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/clean.py +0 -0
  66. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/generate.py +0 -0
  67. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/project_setup.py +0 -0
  68. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/actions/test_runner.py +0 -0
  69. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/menu.py +0 -0
  70. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator/wizard/prompts.py +0 -0
  71. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/SOURCES.txt +0 -0
  72. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/dependency_links.txt +0 -0
  73. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/entry_points.txt +0 -0
  74. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/requires.txt +0 -0
  75. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/src/model_generator_kit.egg-info/top_level.txt +0 -0
  76. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_cleanup.py +0 -0
  77. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_cli.py +0 -0
  78. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_edge_cases.py +0 -0
  79. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_enum_examples.py +0 -0
  80. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_full_generation.py +0 -0
  81. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_integration.py +0 -0
  82. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_template_utils.py +0 -0
  83. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_utils.py +0 -0
  84. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_validate.py +0 -0
  85. {model_generator_kit-0.1.2 → model_generator_kit-0.1.3}/tests/test_validation.py +0 -0
  86. {model_generator_kit-0.1.2 → 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.2
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.2
143
+ **Model Generator** | Bootstrap Tool for API Backends | v0.1.3
@@ -104,4 +104,4 @@ Define specifications once in JSON. Generate production-ready scaffolds. Then ma
104
104
 
105
105
  ---
106
106
 
107
- **Model Generator** | Bootstrap Tool for API Backends | v0.1.2
107
+ **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.2"
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"}
@@ -3,4 +3,4 @@
3
3
  # Single source of truth is pyproject.toml's [project].version. `make
4
4
  # version-sync` propagates that value here; `make check-version-sync` (run in
5
5
  # CI) fails the build if they drift. Do not edit by hand.
6
- __version__ = "0.1.2"
6
+ __version__ = "0.1.3"
@@ -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' and field.constraints is defined %}
61
- {% for c in field.constraints %}
62
- {% if c.type in ['non_negative', 'non_negative_or_null'] and 'validate_non_negative_decimal' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_non_negative_decimal') %}{% endif %}
63
- {% if c.type in ['positive', 'positive_or_null'] and 'validate_positive_decimal' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_positive_decimal') %}{% endif %}
64
- {% endfor %}
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' and field.constraints is defined %}
275
- {% for c in field.constraints %}
276
- {% if c.type in ['non_negative', 'non_negative_or_null'] %}
277
- {% set _ = field_validators.append({'name': item.name, 'validator': 'validate_non_negative_decimal'}) %}
278
- {% elif c.type in ['positive', 'positive_or_null'] %}
279
- {% set _ = field_validators.append({'name': item.name, 'validator': 'validate_positive_decimal'}) %}
280
- {% endif %}
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' and field.constraints is defined %}
420
- {% for c in field.constraints %}
421
- {% if c.type in ['non_negative', 'non_negative_or_null'] %}
422
- {% set _ = update_validators.append({'name': item.name, 'validator': 'validate_non_negative_decimal'}) %}
423
- {% elif c.type in ['positive', 'positive_or_null'] %}
424
- {% set _ = update_validators.append({'name': item.name, 'validator': 'validate_positive_decimal'}) %}
425
- {% endif %}
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' and 'validate_non_negative_decimal' not in ns.needs_validators %}{% set _ = ns.needs_validators.append('validate_non_negative_decimal') %}{% endif %}
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
- {% set _ = validated_fields.append({'name': field_name, 'validator': 'validate_non_negative_decimal'}) %}
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' %}
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-generator-kit
3
- Version: 0.1.2
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.2
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