model-generator-kit 0.1.1__tar.gz → 0.1.2__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.2}/PKG-INFO +2 -2
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/README.md +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/pyproject.toml +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/__init__.py +1 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/api/route.py.j2 +7 -1
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/src/model_generator_kit.egg-info/PKG-INFO +2 -2
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_generators.py +166 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/LICENSE +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/setup.cfg +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/api.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/constraints.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/database.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/enums.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/infrastructure.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/migrations.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/py.typed +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/schema/model.schema.json +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/config.yaml +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/_shared/_base.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/_shared/_entity.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/_shared/_examples.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/_shared/_fields.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/_shared/_tests.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/api/init.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/api/pagination.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/api/request.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/api/response.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/database/constraints.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/database/enums.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/database/factory.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/database/init.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/database/model.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/base.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/csrf.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/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.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/engine.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/errors.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/gitignore.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/main.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/pyproject.toml.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/types.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/utils.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/infrastructure/validators.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/migrations/env.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/stacks/python-fastapi/templates/migrations/ini.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/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.2}/src/model_generator/stacks/python-fastapi/templates/tests/contract.py.j2 +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/conftest_generator.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/constants.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/loaders.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/parser.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/quality.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/templates.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/validate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/actions/__init__.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/actions/clean.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/actions/generate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/actions/project_setup.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/actions/test_runner.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/menu.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/prompts.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator_kit.egg-info/SOURCES.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator_kit.egg-info/dependency_links.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator_kit.egg-info/entry_points.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator_kit.egg-info/requires.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator_kit.egg-info/top_level.txt +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_cleanup.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_cli.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_edge_cases.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_enum_examples.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_full_generation.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_integration.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_template_utils.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_utils.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_validate.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/tests/test_validation.py +0 -0
- {model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2
|
|
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.2
|
|
@@ -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.2"
|
|
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"}
|
|
@@ -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
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2
|
|
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.2
|
|
@@ -1652,6 +1652,58 @@ class TestApiRoutesFilterCoercion:
|
|
|
1652
1652
|
assert "from datetime import datetime" in content
|
|
1653
1653
|
|
|
1654
1654
|
|
|
1655
|
+
class TestApiRoutesDatetimeFilterTzAware:
|
|
1656
|
+
"""Naive datetime filter values are localized to UTC before comparison.
|
|
1657
|
+
|
|
1658
|
+
A ``datetime | None`` filter parses input without a tz offset (e.g.
|
|
1659
|
+
``2026-06-11T12:00:00``) as a naive datetime; compared against a tz-aware
|
|
1660
|
+
``DateTime(timezone=True)`` column it raises ``TypeError``/``DataError`` on
|
|
1661
|
+
strict drivers (asyncpg/psycopg2). The handler localizes a naive value to
|
|
1662
|
+
UTC first. Latent (SQLite suites don't enforce tz-awareness), not a
|
|
1663
|
+
regression — fixed uniformly for both ``_after`` and ``_before``.
|
|
1664
|
+
"""
|
|
1665
|
+
|
|
1666
|
+
def _render(self, model: dict[str, Any], project_env: Any) -> str:
|
|
1667
|
+
project_root, config, env = project_env
|
|
1668
|
+
result = generate_api_routes(
|
|
1669
|
+
model, config, env, project_root, enums={}, constraints={}
|
|
1670
|
+
)
|
|
1671
|
+
assert isinstance(result, dict)
|
|
1672
|
+
return str(result["content"])
|
|
1673
|
+
|
|
1674
|
+
def test_imports_timezone(
|
|
1675
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1676
|
+
) -> None:
|
|
1677
|
+
content = self._render(filter_model, project_env)
|
|
1678
|
+
assert "from datetime import datetime, timezone" in content
|
|
1679
|
+
|
|
1680
|
+
def test_naive_after_localized_to_utc(
|
|
1681
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1682
|
+
) -> None:
|
|
1683
|
+
content = self._render(filter_model, project_env)
|
|
1684
|
+
assert "if observed_at_after.tzinfo is None:" in content
|
|
1685
|
+
assert (
|
|
1686
|
+
"observed_at_after = observed_at_after.replace(tzinfo=timezone.utc)"
|
|
1687
|
+
in content
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
def test_naive_before_localized_to_utc(
|
|
1691
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1692
|
+
) -> None:
|
|
1693
|
+
content = self._render(filter_model, project_env)
|
|
1694
|
+
assert "if observed_at_before.tzinfo is None:" in content
|
|
1695
|
+
assert (
|
|
1696
|
+
"observed_at_before = observed_at_before.replace(tzinfo=timezone.utc)"
|
|
1697
|
+
in content
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
def test_route_still_compiles(
|
|
1701
|
+
self, filter_model: dict[str, Any], project_env: Any
|
|
1702
|
+
) -> None:
|
|
1703
|
+
content = self._render(filter_model, project_env)
|
|
1704
|
+
compile(content, "<metric_route>", "exec")
|
|
1705
|
+
|
|
1706
|
+
|
|
1655
1707
|
class TestValidateAuthConfig:
|
|
1656
1708
|
"""Test the _validate_auth_config helper."""
|
|
1657
1709
|
|
|
@@ -3738,6 +3790,120 @@ class TestRequestLimitGenerator:
|
|
|
3738
3790
|
# Default cap (>0) still applies -> middleware still emitted, no crash.
|
|
3739
3791
|
assert isinstance(generate_request_limit(config, env, project_root), dict)
|
|
3740
3792
|
|
|
3793
|
+
def test_negative_content_length_treated_as_invalid(
|
|
3794
|
+
self, project_env: Any, monkeypatch: pytest.MonkeyPatch
|
|
3795
|
+
) -> None:
|
|
3796
|
+
"""Defense-in-depth: a negative Content-Length must not bypass the cap.
|
|
3797
|
+
|
|
3798
|
+
``int(b"-100")`` used to be returned verbatim, so ``-100 > max`` was
|
|
3799
|
+
False and the request streamed through uncounted. Now a negative length
|
|
3800
|
+
is invalid (``None``), so the request falls through to the chunked
|
|
3801
|
+
byte-counting path and is still rejected on overflow.
|
|
3802
|
+
"""
|
|
3803
|
+
import asyncio
|
|
3804
|
+
import sys
|
|
3805
|
+
import types as types_module
|
|
3806
|
+
|
|
3807
|
+
project_root, config, env = project_env
|
|
3808
|
+
result = generate_request_limit(config, env, project_root)
|
|
3809
|
+
assert isinstance(result, dict)
|
|
3810
|
+
content = result["content"]
|
|
3811
|
+
# Template-level guard: a negative declared length is invalid.
|
|
3812
|
+
assert "if n < 0:" in content
|
|
3813
|
+
|
|
3814
|
+
# Runtime probe: drive the middleware with a lying Content-Length: -100
|
|
3815
|
+
# and an oversized body; the cap must still produce a 413. starlette is
|
|
3816
|
+
# not a generator dependency, so stub its type-only import (auto-undone
|
|
3817
|
+
# by the monkeypatch fixture).
|
|
3818
|
+
starlette = types_module.ModuleType("starlette")
|
|
3819
|
+
starlette_types = types_module.ModuleType("starlette.types")
|
|
3820
|
+
for name in ("ASGIApp", "Message", "Receive", "Scope", "Send"):
|
|
3821
|
+
setattr(starlette_types, name, Any)
|
|
3822
|
+
starlette.types = starlette_types # type: ignore[attr-defined]
|
|
3823
|
+
monkeypatch.setitem(sys.modules, "starlette", starlette)
|
|
3824
|
+
monkeypatch.setitem(sys.modules, "starlette.types", starlette_types)
|
|
3825
|
+
|
|
3826
|
+
ns: dict[str, Any] = {}
|
|
3827
|
+
exec(content, ns)
|
|
3828
|
+
middleware_cls = ns["RequestBodySizeLimitMiddleware"]
|
|
3829
|
+
|
|
3830
|
+
async def app(scope: Any, receive: Any, send: Any) -> None:
|
|
3831
|
+
raise AssertionError("oversized body reached the app")
|
|
3832
|
+
|
|
3833
|
+
mw = middleware_cls(app, max_body_bytes=10)
|
|
3834
|
+
scope = {"type": "http", "headers": [(b"content-length", b"-100")]}
|
|
3835
|
+
|
|
3836
|
+
async def receive() -> dict[str, Any]:
|
|
3837
|
+
return {"type": "http.request", "body": b"x" * 100, "more_body": False}
|
|
3838
|
+
|
|
3839
|
+
sent: list[dict[str, Any]] = []
|
|
3840
|
+
|
|
3841
|
+
async def send(message: dict[str, Any]) -> None:
|
|
3842
|
+
sent.append(message)
|
|
3843
|
+
|
|
3844
|
+
asyncio.run(mw(scope, receive, send))
|
|
3845
|
+
|
|
3846
|
+
start = next(m for m in sent if m["type"] == "http.response.start")
|
|
3847
|
+
assert start["status"] == 413
|
|
3848
|
+
|
|
3849
|
+
def test_duplicate_content_length_treated_as_invalid(
|
|
3850
|
+
self, project_env: Any, monkeypatch: pytest.MonkeyPatch
|
|
3851
|
+
) -> None:
|
|
3852
|
+
"""Defense-in-depth: duplicate Content-Length headers must not bypass the cap.
|
|
3853
|
+
|
|
3854
|
+
Returning the first header's value lets a smuggling pair (small + large)
|
|
3855
|
+
slip an oversized body past a guard keyed on the small one if a
|
|
3856
|
+
downstream server honors the other. Two Content-Length headers are now
|
|
3857
|
+
treated as invalid, forcing the chunked byte-counting path → 413.
|
|
3858
|
+
"""
|
|
3859
|
+
import asyncio
|
|
3860
|
+
import sys
|
|
3861
|
+
import types as types_module
|
|
3862
|
+
|
|
3863
|
+
project_root, config, env = project_env
|
|
3864
|
+
result = generate_request_limit(config, env, project_root)
|
|
3865
|
+
assert isinstance(result, dict)
|
|
3866
|
+
content = result["content"]
|
|
3867
|
+
|
|
3868
|
+
starlette = types_module.ModuleType("starlette")
|
|
3869
|
+
starlette_types = types_module.ModuleType("starlette.types")
|
|
3870
|
+
for name in ("ASGIApp", "Message", "Receive", "Scope", "Send"):
|
|
3871
|
+
setattr(starlette_types, name, Any)
|
|
3872
|
+
starlette.types = starlette_types # type: ignore[attr-defined]
|
|
3873
|
+
monkeypatch.setitem(sys.modules, "starlette", starlette)
|
|
3874
|
+
monkeypatch.setitem(sys.modules, "starlette.types", starlette_types)
|
|
3875
|
+
|
|
3876
|
+
ns: dict[str, Any] = {}
|
|
3877
|
+
exec(content, ns)
|
|
3878
|
+
middleware_cls = ns["RequestBodySizeLimitMiddleware"]
|
|
3879
|
+
|
|
3880
|
+
async def app(scope: Any, receive: Any, send: Any) -> None:
|
|
3881
|
+
raise AssertionError("oversized body reached the app")
|
|
3882
|
+
|
|
3883
|
+
mw = middleware_cls(app, max_body_bytes=10)
|
|
3884
|
+
# Smuggling pair: a small declared length the guard would accept, plus a
|
|
3885
|
+
# second header. The middleware must distrust both and count bytes.
|
|
3886
|
+
scope = {
|
|
3887
|
+
"type": "http",
|
|
3888
|
+
"headers": [
|
|
3889
|
+
(b"content-length", b"5"),
|
|
3890
|
+
(b"content-length", b"100"),
|
|
3891
|
+
],
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
async def receive() -> dict[str, Any]:
|
|
3895
|
+
return {"type": "http.request", "body": b"x" * 100, "more_body": False}
|
|
3896
|
+
|
|
3897
|
+
sent: list[dict[str, Any]] = []
|
|
3898
|
+
|
|
3899
|
+
async def send(message: dict[str, Any]) -> None:
|
|
3900
|
+
sent.append(message)
|
|
3901
|
+
|
|
3902
|
+
asyncio.run(mw(scope, receive, send))
|
|
3903
|
+
|
|
3904
|
+
start = next(m for m in sent if m["type"] == "http.response.start")
|
|
3905
|
+
assert start["status"] == 413
|
|
3906
|
+
|
|
3741
3907
|
|
|
3742
3908
|
class TestImmutableEntityGeneration:
|
|
3743
3909
|
"""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.2}/src/model_generator/generators/__init__.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/api.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/database.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/enums.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/generators/migrations.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/constants.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/loaders.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/quality.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/utils/templates.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/src/model_generator/wizard/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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.2}/src/model_generator/wizard/prompts.py
RENAMED
|
File without changes
|
{model_generator_kit-0.1.1 → model_generator_kit-0.1.2}/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
|