nene2-python 1.8.0__tar.gz → 1.8.1__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.
- {nene2_python-1.8.0 → nene2_python-1.8.1}/CHANGELOG.md +15 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/PKG-INFO +1 -1
- nene2_python-1.8.1/docs/field-trials/2026-05-field-trial-24.md +89 -0
- nene2_python-1.8.1/docs/field-trials/2026-05-field-trial-25.md +82 -0
- nene2_python-1.8.1/docs/field-trials/2026-05-field-trial-26.md +73 -0
- nene2_python-1.8.1/docs/field-trials/2026-05-field-trial-27.md +66 -0
- nene2_python-1.8.1/docs/field-trials/2026-05-field-trial-28.md +54 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/pyproject.toml +1 -1
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/config/settings.py +10 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/log/setup.py +2 -2
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/__init__.py +2 -1
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/request_id.py +18 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/throttle.py +37 -12
- nene2_python-1.8.1/tests/nene2/config/test_settings.py +54 -0
- nene2_python-1.8.1/tests/nene2/log/test_setup.py +49 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_request_id.py +27 -2
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_throttle.py +85 -0
- nene2_python-1.8.1/tests/nene2/validation/__init__.py +0 -0
- nene2_python-1.8.1/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/uv.lock +1 -1
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.env.example +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.gitignore +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/AGENTS.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/CLAUDE.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/Dockerfile +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/LICENSE +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/README.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/alembic/README +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/alembic/env.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/alembic.ini +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/compose.yaml +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/de/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/fr/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/reference/api.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/roadmap.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/todo/current.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/zh/index.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/package-lock.json +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/package.json +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/__main__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/app.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/mcp.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/schema.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.0/tests/nene2/database → nene2_python-1.8.1/tests/nene2/config}/__init__.py +0 -0
- {nene2_python-1.8.0/tests/nene2/http → nene2_python-1.8.1/tests/nene2/database}/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.0/tests/nene2/mcp → nene2_python-1.8.1/tests/nene2/http}/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.0/tests/nene2/middleware → nene2_python-1.8.1/tests/nene2/log}/__init__.py +0 -0
- {nene2_python-1.8.0/tests/nene2/use_case → nene2_python-1.8.1/tests/nene2/mcp}/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.0/tests/nene2/validation → nene2_python-1.8.1/tests/nene2/middleware}/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.0/tests/scripts → nene2_python-1.8.1/tests/nene2/use_case}/__init__.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.0 → nene2_python-1.8.1}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.1] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT25〜FT28 フィールドトライアル — RequestId ヘルパー・structlog ログレベル・ThrottleMiddleware 改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `nene2.middleware.get_request_id()` — FastAPI `Depends` で注入できる request ID ヘルパー関数 (FT25)
|
|
14
|
+
- `setup_logging()` に `log_level: str = "INFO"` パラメータを追加し `AppSettings.log_level` との統合が容易に (FT26)
|
|
15
|
+
- `ThrottleMiddleware` に `path_limits: dict[str, int] | None` パラメータを追加し、パスごとに異なるレート制限を設定可能に (FT28)
|
|
16
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-25.md` 〜 `docs/field-trials/2026-05-field-trial-28.md`
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `ThrottleMiddleware` — ウィンドウ経過後も `_counts` に古いエントリが残り続ける問題を修正(定期クリーンアップを実装)(FT27)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
8
23
|
## [1.8.0] — 2026-05-20
|
|
9
24
|
|
|
10
25
|
FT18〜FT23 フィールドトライアル — ログテスト・Problem Details・ThrottleMiddleware・ドメイン例外・HealthCheck・RequestSizeLimit の各改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: NENE2 Python — minimal API framework following NENE2's design philosophy
|
|
5
5
|
Project-URL: Homepage, https://github.com/hideyukiMORI/nene2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/hideyukiMORI/nene2-python
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# FT24: AppSettings 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `AppSettings` を使った環境変数ベースの設定管理パターンの実運用検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft24-app-settings/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.config.AppSettings` を実際のアプリに組み込み、
|
|
12
|
+
環境変数から設定を読み込んでミドルウェアを設定するパターンを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
`AppSettings` の各フィールドを使って以下を設定:
|
|
19
|
+
- `ErrorHandlerMiddleware(debug=settings.app_debug)`
|
|
20
|
+
- `RequestSizeLimitMiddleware(max_bytes=settings.max_body_size)`
|
|
21
|
+
- `ThrottleMiddleware(limit=settings.throttle_limit, window=settings.throttle_window)` (conditional)
|
|
22
|
+
- `SecurityHeadersMiddleware` (conditional)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## テスト結果
|
|
27
|
+
|
|
28
|
+
### test_app.py(正常系・機能確認)
|
|
29
|
+
| テスト | 結果 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| test_health_endpoint_returns_env | PASS |
|
|
32
|
+
| test_debug_false_by_default | PASS |
|
|
33
|
+
| test_settings_loaded_from_env_vars | PASS |
|
|
34
|
+
| test_throttle_disabled_via_settings | PASS |
|
|
35
|
+
| test_max_body_size_from_settings | PASS |
|
|
36
|
+
| test_db_url_sqlite_default | PASS |
|
|
37
|
+
| test_db_url_mysql_format | PASS |
|
|
38
|
+
|
|
39
|
+
### test_friction.py(摩擦点確認)
|
|
40
|
+
| テスト | 結果 | 摩擦 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| test_list_fields_not_parseable_from_env_string | PASS | あり(ドキュメント) |
|
|
43
|
+
| test_no_log_level_setting | PASS | あり |
|
|
44
|
+
| test_no_middleware_factory_helper | PASS | あり(設計上の判断) |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 発見した摩擦点
|
|
49
|
+
|
|
50
|
+
### FT24-F1: list[str] フィールドを環境変数で設定する方法がドキュメントにない
|
|
51
|
+
|
|
52
|
+
**概要**: `cors_origins`、`bearer_tokens`、`api_keys` は `list[str]` 型だが、
|
|
53
|
+
環境変数から設定するには JSON 形式 (`["a","b"]`) が必要。
|
|
54
|
+
単純な `"token1,token2"` ではパースされない。
|
|
55
|
+
|
|
56
|
+
**判断**: pydantic-settings の標準動作のため、リファレンスドキュメントに記載する。
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### FT24-F2: ログレベルの設定フィールドがない
|
|
61
|
+
|
|
62
|
+
**概要**: `app_debug: bool` しかなく、`INFO`/`WARNING`/`ERROR` の粒度制御ができない。
|
|
63
|
+
|
|
64
|
+
**影響**: ステージング環境で `INFO` ログを出しつつ、本番では `WARNING` に絞りたいが、
|
|
65
|
+
現在は `app_debug=true` でしか詳細ログを出す方法がない。
|
|
66
|
+
|
|
67
|
+
**期待する解決策**: `log_level: str = "INFO"` フィールドを追加。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### FT24-F3: AppSettings からミドルウェアを自動設定するヘルパーがない
|
|
72
|
+
|
|
73
|
+
**概要**: AppSettings の設定値をミドルウェアに適用するには、
|
|
74
|
+
毎回条件分岐付きのボイラープレートを手書きする必要がある。
|
|
75
|
+
|
|
76
|
+
**判断**: `configure_middleware(app, settings)` ヘルパーは「薄い HTTP 層」の設計哲学に反し、
|
|
77
|
+
フレームワークの主張が強くなりすぎる。FT アプリ側でパターンを提示する対応が適切。
|
|
78
|
+
今回はドキュメント追加のみ。
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## まとめ
|
|
83
|
+
|
|
84
|
+
`AppSettings` は環境変数から型安全に設定を読み込む機能として正しく動作する。
|
|
85
|
+
実運用での摩擦は:
|
|
86
|
+
|
|
87
|
+
1. **list[str] 環境変数の書き方がわかりにくい** → ドキュメント追加対応
|
|
88
|
+
2. **log_level フィールドがない** → Issue 化・修正対象
|
|
89
|
+
3. **ミドルウェア自動設定ヘルパーがない** → 設計上の判断で修正しない
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# FT25: RequestIdMiddleware 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `RequestIdMiddleware` のリクエスト ID 生成・伝播・ContextVar 統合の実運用検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft25-request-id/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.middleware.RequestIdMiddleware` と `request_id_var` を使って、
|
|
12
|
+
リクエスト ID を生成・伝播するパターンを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- `/api/echo` エンドポイントで `request_id_var.get()` を返す
|
|
19
|
+
- クライアント提供の有効 UUID v4 の伝播確認
|
|
20
|
+
- 無効な ID が新しい UUID に置き換えられることを確認
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## テスト結果
|
|
25
|
+
|
|
26
|
+
### test_app.py(正常系・機能確認)
|
|
27
|
+
| テスト | 結果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| test_response_includes_x_request_id_header | PASS |
|
|
30
|
+
| test_request_id_is_available_in_handler | PASS |
|
|
31
|
+
| test_client_supplied_valid_uuid_is_preserved | PASS |
|
|
32
|
+
| test_client_supplied_invalid_id_is_replaced | PASS |
|
|
33
|
+
| test_each_request_gets_unique_id | PASS |
|
|
34
|
+
|
|
35
|
+
### test_friction.py(摩擦点確認)
|
|
36
|
+
| テスト | 結果 | 摩擦 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| test_request_id_not_accessible_from_middleware_stack | PASS | あり(ドキュメント) |
|
|
39
|
+
| test_no_helper_to_get_request_id_from_header_in_handler | PASS | あり |
|
|
40
|
+
| test_request_id_contextvars_isolation_per_request | PASS | なし(正しい動作) |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 発見した摩擦点
|
|
45
|
+
|
|
46
|
+
### FT25-F1: ミドルウェア順序依存がドキュメントに明記されていない
|
|
47
|
+
|
|
48
|
+
**概要**: `request_id_var` はミドルウェアの実行順序に依存するが、
|
|
49
|
+
これがドキュメントに明記されていない。
|
|
50
|
+
`RequestIdMiddleware` より先に実行されるミドルウェアでは `request_id_var.get()` が空になる。
|
|
51
|
+
|
|
52
|
+
**判断**: ミドルウェア追加順のドキュメント化で対応(how-to に記載)。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### FT25-F2: FastAPI Depends 用の get_request_id() ヘルパーがない
|
|
57
|
+
|
|
58
|
+
**概要**: `request_id_var.get()` で直接取得できるが、
|
|
59
|
+
FastAPI の依存性注入パターンと統合するヘルパーがない。
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# 各プロジェクトで毎回書く必要がある
|
|
63
|
+
async def get_request_id() -> str:
|
|
64
|
+
return request_id_var.get()
|
|
65
|
+
|
|
66
|
+
@app.get("/")
|
|
67
|
+
async def handler(request_id: str = Depends(get_request_id)):
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**期待する解決策**: `nene2.middleware` から `get_request_id` を直接インポートできると便利。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## まとめ
|
|
76
|
+
|
|
77
|
+
`RequestIdMiddleware` の基本機能(UUID v4 生成・検証・ContextVar 伝播・レスポンスヘッダー付与)は
|
|
78
|
+
問題なく動作する。
|
|
79
|
+
|
|
80
|
+
摩擦点:
|
|
81
|
+
1. **ミドルウェア順序依存のドキュメント不備** → how-to に追記
|
|
82
|
+
2. **`get_request_id()` Depends ヘルパーがない** → Issue 化・修正対象
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# FT26: nene2.log structlog 統合検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `setup_logging()` と `AppSettings.log_level` の連携検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft26-logging/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.log.setup_logging()` を実際のアプリで使い、
|
|
12
|
+
`AppSettings.log_level`(FT24 で追加)との統合フローを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- `setup_logging()` の `local` / `production` 環境での動作確認
|
|
19
|
+
- `RequestLoggingMiddleware` と structlog の連携確認
|
|
20
|
+
- `AppSettings.log_level` を `setup_logging()` に渡す方法の検証
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## テスト結果
|
|
25
|
+
|
|
26
|
+
### test_app.py(正常系・機能確認)
|
|
27
|
+
| テスト | 結果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| test_log_test_endpoint_returns_200 | PASS |
|
|
30
|
+
| test_request_logging_middleware_logs_request | PASS |
|
|
31
|
+
| test_log_levels_endpoint_returns_200 | PASS |
|
|
32
|
+
| test_setup_logging_local_env | PASS |
|
|
33
|
+
| test_setup_logging_production_env | PASS |
|
|
34
|
+
|
|
35
|
+
### test_friction.py(摩擦点確認)
|
|
36
|
+
| テスト | 結果 | 摩擦 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| test_setup_logging_does_not_accept_log_level_param | PASS | あり |
|
|
39
|
+
| test_setup_logging_hardcodes_info_level | PASS | あり |
|
|
40
|
+
| test_no_way_to_integrate_app_settings_log_level_with_setup_logging | PASS | あり(ボイラープレート) |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 発見した摩擦点
|
|
45
|
+
|
|
46
|
+
### FT26-F1: setup_logging() が log_level を受け取れない
|
|
47
|
+
|
|
48
|
+
**概要**: FT24 で `AppSettings.log_level` が追加されたが、
|
|
49
|
+
`setup_logging()` はそれを受け取るパラメータを持たない。
|
|
50
|
+
`setup_logging()` は常に `logging.INFO` をハードコードするため、
|
|
51
|
+
`AppSettings.log_level = "DEBUG"` を設定しても反映されない。
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# 現状: ボイラープレートが必要
|
|
55
|
+
settings = AppSettings(log_level="DEBUG")
|
|
56
|
+
setup_logging(app_env=settings.app_env)
|
|
57
|
+
logging.getLogger().setLevel(settings.log_level) # ← 別途必要
|
|
58
|
+
|
|
59
|
+
# 期待する使い方
|
|
60
|
+
setup_logging(app_env=settings.app_env, log_level=settings.log_level)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**期待する解決策**: `setup_logging(app_env="local", log_level="INFO")` のように
|
|
64
|
+
`log_level` パラメータを追加して `AppSettings` と統合しやすくする。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## まとめ
|
|
69
|
+
|
|
70
|
+
`setup_logging()` の基本機能(ConsoleRenderer / JSONRenderer の切り替え)は問題なく動作する。
|
|
71
|
+
|
|
72
|
+
摩擦点:
|
|
73
|
+
1. **`setup_logging()` に `log_level` パラメータがない** → `AppSettings.log_level` との統合時にボイラープレートが必要 → Issue 化・修正対象
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# FT27: ThrottleMiddleware 長時間運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `ThrottleMiddleware` の長時間稼働時のメモリ蓄積問題を検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft27-throttle-cleanup/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`ThrottleMiddleware` の `_counts` ディクショナリに古いエントリが蓄積する問題(Issue #223)を実証し、
|
|
12
|
+
修正方針を検討する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- 複数 IP からのリクエストシミュレーション
|
|
19
|
+
- ウィンドウ経過後のエントリ残存を確認
|
|
20
|
+
- クリーンアップ機能の有無を検証
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## テスト結果
|
|
25
|
+
|
|
26
|
+
### test_app.py(正常系・機能確認)
|
|
27
|
+
| テスト | 結果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| test_ping_returns_200 | PASS |
|
|
30
|
+
| test_rate_limit_headers_present | PASS |
|
|
31
|
+
| test_rate_limit_exceeded_returns_429 | PASS |
|
|
32
|
+
| test_different_ips_have_separate_counters | PASS |
|
|
33
|
+
|
|
34
|
+
### test_friction.py(摩擦点確認)
|
|
35
|
+
| テスト | 結果 | 摩擦 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| test_stale_entries_accumulate_in_memory | PASS | あり(バグ) |
|
|
38
|
+
| test_no_cleanup_method_exists_on_throttle_middleware | PASS | あり |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 発見した摩擦点
|
|
43
|
+
|
|
44
|
+
### FT27-F1: 古いエントリが _counts から削除されない(Issue #223)
|
|
45
|
+
|
|
46
|
+
**概要**: 異なるクライアント IP からリクエストが来るたびに `_counts` にエントリが追加されるが、
|
|
47
|
+
ウィンドウ期間が過ぎても古いエントリは削除されない。
|
|
48
|
+
長時間稼働・多数の異なるクライアントが存在する環境ではメモリが際限なく増加する。
|
|
49
|
+
|
|
50
|
+
**確認した動作**:
|
|
51
|
+
```python
|
|
52
|
+
# 10 IP のエントリを作成 → window (1秒) 経過後も 10 エントリが残る
|
|
53
|
+
# 新しい IP のリクエスト後も 11 エントリ (10 古い + 1 新しい)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**修正方針**: `_check_rate()` 内で定期的にクリーンアップを実施する。
|
|
57
|
+
`_last_cleanup` タイムスタンプを持ち、`window` 秒経過したときに期限切れエントリを一括削除する。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## まとめ
|
|
62
|
+
|
|
63
|
+
`ThrottleMiddleware` の基本機能は正常に動作する。
|
|
64
|
+
|
|
65
|
+
摩擦点:
|
|
66
|
+
1. **古いエントリのメモリ蓄積** → Issue #223 として登録済み、修正対象
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# FT28: ThrottleMiddleware パスごとのレート制限検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `ThrottleMiddleware` のパスごとのレート制限(Issue #222)
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft28-path-throttle/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
重いエンドポイントだけ厳しいレート制限をかけたい実運用ニーズを検証する。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 実施内容
|
|
16
|
+
|
|
17
|
+
- `path_limits` パラメータの不在を確認
|
|
18
|
+
- 現状では全エンドポイントで同じ `limit`/`window` しか設定できないことを実証
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## テスト結果
|
|
23
|
+
|
|
24
|
+
### test_friction.py(摩擦点確認)
|
|
25
|
+
| テスト | 結果 | 摩擦 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| test_throttle_middleware_does_not_support_path_limits | PASS | あり |
|
|
28
|
+
| test_workaround_requires_multiple_middlewares | PASS | あり |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 発見した摩擦点
|
|
33
|
+
|
|
34
|
+
### FT28-F1: ThrottleMiddleware がパスごとのレート制限をサポートしない(Issue #222)
|
|
35
|
+
|
|
36
|
+
**概要**: 全エンドポイントで同じ `limit`/`window` しか設定できない。
|
|
37
|
+
`/api/expensive` だけ `limit=10` にして残りは `limit=100` にする、という設定ができない。
|
|
38
|
+
|
|
39
|
+
**期待する使い方**:
|
|
40
|
+
```python
|
|
41
|
+
app.add_middleware(
|
|
42
|
+
ThrottleMiddleware,
|
|
43
|
+
limit=100,
|
|
44
|
+
window=60,
|
|
45
|
+
path_limits={"/api/expensive": 10, "/api/search": 30},
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## まとめ
|
|
52
|
+
|
|
53
|
+
摩擦点:
|
|
54
|
+
1. **`path_limits` 未対応** → Issue #222 として登録済み、修正対象
|
|
@@ -12,6 +12,7 @@ class AppSettings(BaseSettings):
|
|
|
12
12
|
app_env: str = "local"
|
|
13
13
|
app_debug: bool = False
|
|
14
14
|
app_name: str = "nene2-python"
|
|
15
|
+
log_level: str = "INFO"
|
|
15
16
|
security_headers_enabled: bool = True
|
|
16
17
|
max_body_size: int = 1_048_576 # 1 MiB
|
|
17
18
|
throttle_enabled: bool = True
|
|
@@ -50,6 +51,15 @@ class AppSettings(BaseSettings):
|
|
|
50
51
|
raise ValueError(f"app_env must be one of {allowed}")
|
|
51
52
|
return v
|
|
52
53
|
|
|
54
|
+
@field_validator("log_level")
|
|
55
|
+
@classmethod
|
|
56
|
+
def validate_log_level(cls, v: str) -> str:
|
|
57
|
+
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
58
|
+
upper = v.upper()
|
|
59
|
+
if upper not in allowed:
|
|
60
|
+
raise ValueError(f"log_level must be one of {allowed}")
|
|
61
|
+
return upper
|
|
62
|
+
|
|
53
63
|
@field_validator("db_adapter")
|
|
54
64
|
@classmethod
|
|
55
65
|
def validate_adapter(cls, v: str) -> str:
|
|
@@ -51,7 +51,7 @@ def configure_for_testing() -> None:
|
|
|
51
51
|
root.setLevel(logging.DEBUG)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def setup_logging(app_env: str = "local") -> None:
|
|
54
|
+
def setup_logging(app_env: str = "local", log_level: str = "INFO") -> None:
|
|
55
55
|
shared_processors: list[structlog.types.Processor] = [
|
|
56
56
|
structlog.stdlib.add_log_level,
|
|
57
57
|
structlog.stdlib.add_logger_name,
|
|
@@ -86,4 +86,4 @@ def setup_logging(app_env: str = "local") -> None:
|
|
|
86
86
|
root = logging.getLogger()
|
|
87
87
|
root.handlers.clear()
|
|
88
88
|
root.addHandler(handler)
|
|
89
|
-
root.setLevel(logging.INFO)
|
|
89
|
+
root.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .domain_exception import DomainExceptionHandlerProtocol, SimpleDomainHandler
|
|
4
4
|
from .error_handler import ErrorHandlerMiddleware
|
|
5
|
-
from .request_id import RequestIdMiddleware, request_id_var
|
|
5
|
+
from .request_id import RequestIdMiddleware, get_request_id, request_id_var
|
|
6
6
|
from .request_logging import RequestLoggingMiddleware
|
|
7
7
|
from .request_size_limit import RequestSizeLimitMiddleware
|
|
8
8
|
from .security_headers import SecurityHeadersMiddleware
|
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
|
13
13
|
"SimpleDomainHandler",
|
|
14
14
|
"ErrorHandlerMiddleware",
|
|
15
15
|
"RequestIdMiddleware",
|
|
16
|
+
"get_request_id",
|
|
16
17
|
"RequestLoggingMiddleware",
|
|
17
18
|
"RequestSizeLimitMiddleware",
|
|
18
19
|
"SecurityHeadersMiddleware",
|
|
@@ -30,6 +30,24 @@ def _validated_request_id(value: str | None) -> str:
|
|
|
30
30
|
return str(uuid.uuid4())
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def get_request_id() -> str:
|
|
34
|
+
"""FastAPI ``Depends``-compatible helper that returns the current request ID.
|
|
35
|
+
|
|
36
|
+
Use this in route handlers to inject the request ID via dependency injection::
|
|
37
|
+
|
|
38
|
+
from nene2.middleware import get_request_id
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.get("/")
|
|
42
|
+
async def handler(request_id: str = Depends(get_request_id)) -> JSONResponse:
|
|
43
|
+
return JSONResponse({"request_id": request_id})
|
|
44
|
+
|
|
45
|
+
Returns an empty string when called outside of a request context
|
|
46
|
+
(e.g., in tests without :class:`RequestIdMiddleware`).
|
|
47
|
+
"""
|
|
48
|
+
return request_id_var.get()
|
|
49
|
+
|
|
50
|
+
|
|
33
51
|
class RequestIdMiddleware(BaseHTTPMiddleware):
|
|
34
52
|
"""Generate or forward X-Request-Id and expose it via contextvars.
|
|
35
53
|
|
|
@@ -42,14 +42,19 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
42
42
|
``X-RateLimit-Reset`` headers to every response so clients can monitor
|
|
43
43
|
their quota. On 429, also adds ``Retry-After``.
|
|
44
44
|
|
|
45
|
-
Use ``
|
|
45
|
+
Use ``path_limits`` to apply stricter limits on specific paths::
|
|
46
46
|
|
|
47
47
|
app.add_middleware(
|
|
48
48
|
ThrottleMiddleware,
|
|
49
|
-
limit=
|
|
49
|
+
limit=100,
|
|
50
50
|
window=60,
|
|
51
|
+
path_limits={"/api/expensive": 10, "/api/search": 30},
|
|
51
52
|
exclude_paths=["/health", "/docs", "/openapi.json"],
|
|
52
53
|
)
|
|
54
|
+
|
|
55
|
+
Path-limited endpoints are tracked independently from the global counter
|
|
56
|
+
(the key includes the path, so ``/api/expensive`` quota is separate from
|
|
57
|
+
the default quota for other paths).
|
|
53
58
|
"""
|
|
54
59
|
|
|
55
60
|
def __init__(
|
|
@@ -58,14 +63,17 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
58
63
|
*,
|
|
59
64
|
limit: int = _DEFAULT_LIMIT,
|
|
60
65
|
window: int = _DEFAULT_WINDOW,
|
|
66
|
+
path_limits: dict[str, int] | None = None,
|
|
61
67
|
exclude_paths: list[str] | None = None,
|
|
62
68
|
) -> None:
|
|
63
69
|
super().__init__(app) # type: ignore[arg-type]
|
|
64
70
|
self._limit = limit
|
|
65
71
|
self._window = window
|
|
72
|
+
self._path_limits: dict[str, int] = path_limits or {}
|
|
66
73
|
self._exclude_paths = set(exclude_paths or [])
|
|
67
74
|
self._counts: dict[str, tuple[int, float]] = {}
|
|
68
75
|
self._lock = threading.Lock()
|
|
76
|
+
self._last_cleanup: float = 0.0
|
|
69
77
|
|
|
70
78
|
def _client_key(self, request: Request) -> str:
|
|
71
79
|
forwarded = request.headers.get("X-Forwarded-For")
|
|
@@ -73,10 +81,20 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
73
81
|
return forwarded.split(",")[0].strip()
|
|
74
82
|
return request.client.host if request.client else "unknown"
|
|
75
83
|
|
|
76
|
-
def
|
|
84
|
+
def _evict_stale(self, now: float) -> None:
|
|
85
|
+
if now - self._last_cleanup < self._window:
|
|
86
|
+
return
|
|
87
|
+
self._last_cleanup = now
|
|
88
|
+
cutoff = now - self._window
|
|
89
|
+
stale = [k for k, (_, ws) in self._counts.items() if ws < cutoff]
|
|
90
|
+
for k in stale:
|
|
91
|
+
del self._counts[k]
|
|
92
|
+
|
|
93
|
+
def _check_rate(self, key: str, limit: int) -> _RateInfo:
|
|
77
94
|
now = time.monotonic()
|
|
78
95
|
wall_now = int(time.time())
|
|
79
96
|
with self._lock:
|
|
97
|
+
self._evict_stale(now)
|
|
80
98
|
count, window_start = self._counts.get(key, (0, now))
|
|
81
99
|
if now - window_start >= self._window:
|
|
82
100
|
count, window_start = 0, now
|
|
@@ -85,24 +103,31 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
85
103
|
elapsed = int(now - window_start)
|
|
86
104
|
retry_after = max(0, self._window - elapsed)
|
|
87
105
|
reset_at = wall_now + retry_after
|
|
88
|
-
remaining = max(0,
|
|
106
|
+
remaining = max(0, limit - count)
|
|
89
107
|
return _RateInfo(
|
|
90
|
-
allowed=count <=
|
|
108
|
+
allowed=count <= limit,
|
|
91
109
|
remaining=remaining,
|
|
92
110
|
retry_after=retry_after,
|
|
93
111
|
reset_at=reset_at,
|
|
94
112
|
)
|
|
95
113
|
|
|
96
|
-
def _apply_rate_headers(self, response: Response, info: _RateInfo) -> None:
|
|
97
|
-
response.headers["X-RateLimit-Limit"] = str(
|
|
114
|
+
def _apply_rate_headers(self, response: Response, info: _RateInfo, limit: int) -> None:
|
|
115
|
+
response.headers["X-RateLimit-Limit"] = str(limit)
|
|
98
116
|
response.headers["X-RateLimit-Remaining"] = str(info.remaining)
|
|
99
117
|
response.headers["X-RateLimit-Reset"] = str(info.reset_at)
|
|
100
118
|
|
|
101
119
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
102
|
-
|
|
120
|
+
path = request.url.path
|
|
121
|
+
if path in self._exclude_paths:
|
|
103
122
|
return await call_next(request)
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
client = self._client_key(request)
|
|
124
|
+
if path in self._path_limits:
|
|
125
|
+
effective_limit = self._path_limits[path]
|
|
126
|
+
key = f"{client}:{path}"
|
|
127
|
+
else:
|
|
128
|
+
effective_limit = self._limit
|
|
129
|
+
key = client
|
|
130
|
+
info = self._check_rate(key, effective_limit)
|
|
106
131
|
if not info.allowed:
|
|
107
132
|
error_response = problem_details_response(
|
|
108
133
|
"too-many-requests",
|
|
@@ -111,8 +136,8 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
111
136
|
f"Rate limit exceeded. Retry after {info.retry_after} seconds.",
|
|
112
137
|
)
|
|
113
138
|
error_response.headers["Retry-After"] = str(info.retry_after)
|
|
114
|
-
self._apply_rate_headers(error_response, info)
|
|
139
|
+
self._apply_rate_headers(error_response, info, effective_limit)
|
|
115
140
|
return error_response
|
|
116
141
|
response = await call_next(request)
|
|
117
|
-
self._apply_rate_headers(response, info)
|
|
142
|
+
self._apply_rate_headers(response, info, effective_limit)
|
|
118
143
|
return response
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Tests for AppSettings."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from nene2.config import AppSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_default_log_level_is_info() -> None:
|
|
10
|
+
settings = AppSettings()
|
|
11
|
+
assert settings.log_level == "INFO"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_log_level_is_normalized_to_uppercase() -> None:
|
|
15
|
+
settings = AppSettings(log_level="debug")
|
|
16
|
+
assert settings.log_level == "DEBUG"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_log_level_accepts_all_standard_levels() -> None:
|
|
20
|
+
for level in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
21
|
+
settings = AppSettings(log_level=level)
|
|
22
|
+
assert settings.log_level == level
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_invalid_log_level_raises_validation_error() -> None:
|
|
26
|
+
with pytest.raises(ValidationError):
|
|
27
|
+
AppSettings(log_level="TRACE")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_default_app_env_is_local() -> None:
|
|
31
|
+
settings = AppSettings()
|
|
32
|
+
assert settings.app_env == "local"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_invalid_app_env_raises_validation_error() -> None:
|
|
36
|
+
with pytest.raises(ValidationError):
|
|
37
|
+
AppSettings(app_env="staging")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_db_url_sqlite_by_default() -> None:
|
|
41
|
+
settings = AppSettings()
|
|
42
|
+
assert settings.db_url.startswith("sqlite:///")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_db_url_mysql_format() -> None:
|
|
46
|
+
settings = AppSettings(
|
|
47
|
+
db_adapter="mysql",
|
|
48
|
+
db_user="root",
|
|
49
|
+
db_name="mydb",
|
|
50
|
+
db_host="localhost",
|
|
51
|
+
db_port=3306,
|
|
52
|
+
)
|
|
53
|
+
assert "mysql+pymysql://" in settings.db_url
|
|
54
|
+
assert "mydb" in settings.db_url
|