nene2-python 1.8.1__tar.gz → 1.8.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.
- {nene2_python-1.8.1 → nene2_python-1.8.3}/CHANGELOG.md +23 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/PKG-INFO +1 -1
- nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-29.md +74 -0
- nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-30.md +66 -0
- nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-31.md +66 -0
- nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-32.md +78 -0
- nene2_python-1.8.3/docs/how-to/async-use-case.md +121 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/pyproject.toml +1 -1
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/health.py +4 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_logging.py +10 -1
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/security_headers.py +17 -3
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_health.py +8 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_logging.py +37 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_security_headers.py +38 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/uv.lock +1 -1
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.env.example +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.gitignore +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/AGENTS.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/CLAUDE.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/Dockerfile +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/LICENSE +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/README.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/README +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/env.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic.ini +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/compose.yaml +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/de/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/fr/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/api.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/roadmap.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/todo/current.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/zh/index.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/package-lock.json +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/package.json +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/__main__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/app.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/mcp.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/schema.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.3] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT31〜FT32 フィールドトライアル — HealthCheck・SecurityHeaders 改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `HealthStatus.http_status_code` プロパティ — `is_healthy` → 200、それ以外 → 503 のマッピングを提供 (FT31)
|
|
14
|
+
- `SecurityHeadersMiddleware` に `permissions_policy: str | None = None` パラメータを追加 (FT32)
|
|
15
|
+
- `SecurityHeadersMiddleware` に `hsts: str | None = None` パラメータを追加 (FT32)
|
|
16
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-31.md`、`docs/field-trials/2026-05-field-trial-32.md`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [1.8.2] — 2026-05-20
|
|
21
|
+
|
|
22
|
+
FT29〜FT30 フィールドトライアル — AsyncUseCase ドキュメント・RequestLoggingMiddleware 改善。
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `docs/how-to/async-use-case.md` — `AsyncUseCaseProtocol` + FastAPI `Depends` の DI パターンガイドを追加 (FT29)
|
|
26
|
+
- `RequestLoggingMiddleware` に `extra_context: dict[str, str] | None = None` パラメータを追加し、全ログに静的フィールドを付加できるように (FT30)
|
|
27
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-29.md`、`docs/field-trials/2026-05-field-trial-30.md`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
8
31
|
## [1.8.1] — 2026-05-20
|
|
9
32
|
|
|
10
33
|
FT25〜FT28 フィールドトライアル — RequestId ヘルパー・structlog ログレベル・ThrottleMiddleware 改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.3
|
|
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,74 @@
|
|
|
1
|
+
# FT29: AsyncUseCaseProtocol 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `AsyncUseCaseProtocol` を使った非同期 UseCase パターンの実運用検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft29-async-usecase/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`AsyncUseCaseProtocol` の実装・FastAPI ハンドラーへの統合・並行処理の動作を検証する。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 実施内容
|
|
16
|
+
|
|
17
|
+
- 外部 API 呼び出しを模した `FetchDataUseCase` を実装
|
|
18
|
+
- `asyncio.gather()` で並行実行を確認
|
|
19
|
+
- Protocol 適合性と isinstance() の既知制限を検証
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## テスト結果
|
|
24
|
+
|
|
25
|
+
### test_app.py(正常系・機能確認)
|
|
26
|
+
| テスト | 結果 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| test_get_item_returns_200 | PASS |
|
|
29
|
+
| test_slow_endpoint_returns_200 | PASS |
|
|
30
|
+
| test_async_use_case_executes_correctly | PASS |
|
|
31
|
+
| test_async_use_case_satisfies_protocol | PASS |
|
|
32
|
+
| test_multiple_async_calls_are_independent | PASS |
|
|
33
|
+
|
|
34
|
+
### test_friction.py(摩擦点確認)
|
|
35
|
+
| テスト | 結果 | 摩擦 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| test_isinstance_cannot_distinguish_sync_vs_async_protocol | PASS | 既知(ADR-0010) |
|
|
38
|
+
| test_no_async_usecase_base_class_provided | PASS | 軽微(設計通り) |
|
|
39
|
+
| test_no_generic_di_container_for_async_use_cases | PASS | あり(ドキュメント不備) |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 発見した摩擦点
|
|
44
|
+
|
|
45
|
+
### FT29-F1: FastAPI Depends を使った AsyncUseCase DI パターンがドキュメント化されていない
|
|
46
|
+
|
|
47
|
+
**概要**: `AsyncUseCaseProtocol` を FastAPI の依存性注入と統合する標準的なパターンがない。
|
|
48
|
+
ユーザーは毎回自分でパターンを決める必要がある。
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# ユーザーが毎回書く必要があるボイラープレート
|
|
52
|
+
def get_fetch_use_case() -> FetchDataUseCase:
|
|
53
|
+
return FetchDataUseCase()
|
|
54
|
+
|
|
55
|
+
@app.get("/items/{item_id}")
|
|
56
|
+
async def get_item(
|
|
57
|
+
item_id: int,
|
|
58
|
+
use_case: FetchDataUseCase = Depends(get_fetch_use_case),
|
|
59
|
+
) -> JSONResponse:
|
|
60
|
+
result = await use_case.execute(FetchDataInput(item_id=item_id))
|
|
61
|
+
...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**判断**: how-to ドキュメントに DI パターンを追記する(Issue 化)。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## まとめ
|
|
69
|
+
|
|
70
|
+
`AsyncUseCaseProtocol` の基本機能(実装・FastAPI 統合・並行処理)は問題なく動作する。
|
|
71
|
+
|
|
72
|
+
摩擦点:
|
|
73
|
+
1. **AsyncUseCase + FastAPI DI パターンがドキュメント化されていない** → Issue 化・how-to に追記
|
|
74
|
+
2. **isinstance() の sync/async 区別不可** → ADR-0010 記載の既知制限、修正不要
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# FT30: RequestLoggingMiddleware + structlog バインディング実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `RequestLoggingMiddleware` と structlog contextvars の実運用パターン検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft30-request-logging/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`RequestLoggingMiddleware` と structlog の contextvars 統合を実際のアプリで検証し、
|
|
12
|
+
カスタムフィールドの付加方法を確認する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- `RequestLoggingMiddleware` が自動バインドするコンテキスト(request_id, method, path)を確認
|
|
19
|
+
- 追加のコンテキストフィールドを渡す方法を検証
|
|
20
|
+
- `clear_contextvars()` の挙動確認
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## テスト結果
|
|
25
|
+
|
|
26
|
+
### test_app.py(正常系・機能確認)
|
|
27
|
+
| テスト | 結果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| test_log_test_endpoint_returns_200 | PASS |
|
|
30
|
+
| test_with_user_context_returns_200 | PASS |
|
|
31
|
+
| test_request_id_is_in_response_header | PASS |
|
|
32
|
+
|
|
33
|
+
### test_friction.py(摩擦点確認)
|
|
34
|
+
| テスト | 結果 | 摩擦 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| test_clear_contextvars_wipes_pre_bound_context | PASS | 軽微(設計上の制限) |
|
|
37
|
+
| test_no_way_to_add_extra_fields_to_request_log | PASS | あり |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 発見した摩擦点
|
|
42
|
+
|
|
43
|
+
### FT30-F1: RequestLoggingMiddleware に extra_context パラメータがない
|
|
44
|
+
|
|
45
|
+
**概要**: `service_name` や `version` などの静的フィールドを全リクエストログに含めたい場合、
|
|
46
|
+
`RequestLoggingMiddleware` にパラメータとして渡す方法がない。
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# 期待する使い方
|
|
50
|
+
app.add_middleware(
|
|
51
|
+
RequestLoggingMiddleware,
|
|
52
|
+
extra_context={"service": "my-api", "version": "1.0.0"},
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**期待する解決策**: `extra_context: dict[str, str] | None = None` パラメータを追加し、
|
|
57
|
+
`bind_contextvars()` に追加フィールドとして渡す。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## まとめ
|
|
62
|
+
|
|
63
|
+
`RequestLoggingMiddleware` の基本機能は問題なく動作する。
|
|
64
|
+
|
|
65
|
+
摩擦点:
|
|
66
|
+
1. **`extra_context` パラメータがない** → Issue 化・修正対象
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# FT31: DatabaseHealthCheck + ヘルスエンドポイント統合検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `DatabaseHealthCheck` と `/health` エンドポイントの統合パターン検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft31-health-check/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`DatabaseHealthCheck` + `CompositeHealthCheck` を使った `/health` エンドポイントの実装パターンを検証する。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 実施内容
|
|
16
|
+
|
|
17
|
+
- `DatabaseHealthCheck` + `CompositeHealthCheck` で `/health` を実装
|
|
18
|
+
- DB 接続成功時に 200、失敗時に 503 を返すパターンを確認
|
|
19
|
+
- フレームワークが提供すべき機能の不足を記録
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## テスト結果
|
|
24
|
+
|
|
25
|
+
### test_app.py(正常系・機能確認)
|
|
26
|
+
| テスト | 結果 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| test_health_endpoint_returns_200_when_db_healthy | PASS |
|
|
29
|
+
| test_health_includes_all_checks | PASS |
|
|
30
|
+
| test_ping_returns_200 | PASS |
|
|
31
|
+
| test_health_returns_503_when_db_fails | PASS |
|
|
32
|
+
|
|
33
|
+
### test_friction.py(摩擦点確認)
|
|
34
|
+
| テスト | 結果 | 摩擦 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| test_health_endpoint_has_no_built_in_route | PASS | 軽微(how-to 推奨パターン記載で対応) |
|
|
37
|
+
| test_health_status_lacks_http_status_code_mapping | PASS | あり |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 発見した摩擦点
|
|
42
|
+
|
|
43
|
+
### FT31-F1: HealthStatus が HTTP ステータスコードのマッピングを持たない
|
|
44
|
+
|
|
45
|
+
**概要**: `/health` エンドポイントを実装するとき、
|
|
46
|
+
`status="ok"` → 200、`status="error"` → 503 のマッピングを毎回手動で書く必要がある。
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# 毎回このボイラープレートが必要
|
|
50
|
+
http_status = 200 if status.is_healthy else 503
|
|
51
|
+
return JSONResponse({"status": status.status}, status_code=http_status)
|
|
52
|
+
|
|
53
|
+
# 期待する使い方
|
|
54
|
+
return JSONResponse({"status": status.status}, status_code=status.http_status_code)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**期待する解決策**: `HealthStatus` に `http_status_code: int` プロパティを追加する。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## まとめ
|
|
62
|
+
|
|
63
|
+
`DatabaseHealthCheck` + `CompositeHealthCheck` の基本機能は問題なく動作する。
|
|
64
|
+
|
|
65
|
+
摩擦点:
|
|
66
|
+
1. **`HealthStatus.http_status_code` プロパティがない** → Issue 化・修正対象
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# FT32: SecurityHeadersMiddleware CSP カスタマイズ実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `SecurityHeadersMiddleware` のカスタマイズ可能性検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft32-security-headers/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`SecurityHeadersMiddleware` の CSP カスタマイズ機能を実際のアプリで検証し、
|
|
12
|
+
ハードコードされたヘッダーの問題を記録する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- デフォルトセキュリティヘッダーの確認
|
|
19
|
+
- カスタム CSP の適用確認
|
|
20
|
+
- `extra_no_csp_paths` の動作確認
|
|
21
|
+
- ハードコードされたヘッダーの制限を確認
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## テスト結果
|
|
26
|
+
|
|
27
|
+
### test_app.py(正常系・機能確認)
|
|
28
|
+
| テスト | 結果 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| test_default_security_headers_present | PASS |
|
|
31
|
+
| test_default_csp_is_default_src_self | PASS |
|
|
32
|
+
| test_custom_csp_is_applied | PASS |
|
|
33
|
+
| test_docs_path_has_no_csp | PASS |
|
|
34
|
+
| test_extra_no_csp_paths_skip_csp | PASS |
|
|
35
|
+
|
|
36
|
+
### test_friction.py(摩擦点確認)
|
|
37
|
+
| テスト | 結果 | 摩擦 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| test_permissions_policy_is_hardcoded | PASS | あり |
|
|
40
|
+
| test_no_hsts_header | PASS | あり |
|
|
41
|
+
| test_x_frame_options_is_hardcoded_to_deny | PASS | あり(軽微) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 発見した摩擦点
|
|
46
|
+
|
|
47
|
+
### FT32-F1: Permissions-Policy がハードコードされている
|
|
48
|
+
|
|
49
|
+
**概要**: `geolocation=(), microphone=()` が固定値でカスタマイズできない。
|
|
50
|
+
位置情報 API を使うアプリでは `geolocation=(self)` に変更できない。
|
|
51
|
+
|
|
52
|
+
**期待する解決策**: `permissions_policy: str | None = None` パラメータを追加。
|
|
53
|
+
|
|
54
|
+
### FT32-F2: HSTS ヘッダーがない
|
|
55
|
+
|
|
56
|
+
**概要**: production 環境では `Strict-Transport-Security` を設定すべきだが、
|
|
57
|
+
`SecurityHeadersMiddleware` は HSTS を付与しない。
|
|
58
|
+
開発環境では不要なため、オプションとして設定できるべき。
|
|
59
|
+
|
|
60
|
+
**期待する解決策**: `hsts: str | None = None` パラメータを追加。
|
|
61
|
+
|
|
62
|
+
### FT32-F3: X-Frame-Options が DENY にハードコード(軽微)
|
|
63
|
+
|
|
64
|
+
**概要**: iframe 内表示が必要なケースで SAMEORIGIN に変更できない。
|
|
65
|
+
ただし DENY がセキュリティ上より安全なデフォルトであり、一般ユース向け。
|
|
66
|
+
|
|
67
|
+
**判断**: ニッチなケースのため低優先度。Issue 化はするが即座に修正しない。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## まとめ
|
|
72
|
+
|
|
73
|
+
CSP カスタマイズ機能は問題なく動作する。
|
|
74
|
+
|
|
75
|
+
摩擦点:
|
|
76
|
+
1. **Permissions-Policy ハードコード** → Issue 化・修正対象
|
|
77
|
+
2. **HSTS ヘッダーなし** → Issue 化・修正対象
|
|
78
|
+
3. **X-Frame-Options ハードコード** → 低優先度 Issue
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# How-to: AsyncUseCase と FastAPI の統合
|
|
2
|
+
|
|
3
|
+
## AsyncUseCaseProtocol の基本実装
|
|
4
|
+
|
|
5
|
+
`AsyncUseCaseProtocol` は Protocol(構造的部分型)なので継承不要です。
|
|
6
|
+
`async def execute(self, input_: I) -> O` を実装するだけで適合します。
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from nene2.use_case import AsyncUseCaseProtocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class FetchUserInput:
|
|
15
|
+
user_id: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class FetchUserOutput:
|
|
20
|
+
user_id: int
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FetchUserUseCase:
|
|
25
|
+
async def execute(self, input_: FetchUserInput) -> FetchUserOutput:
|
|
26
|
+
# 外部 API 呼び出し・DB アクセスなど非同期処理
|
|
27
|
+
return FetchUserOutput(user_id=input_.user_id, name="Alice")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## FastAPI Depends との統合
|
|
33
|
+
|
|
34
|
+
ファクトリ関数を `Depends()` に渡すのが標準パターンです。
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from fastapi import Depends, FastAPI
|
|
38
|
+
from fastapi.responses import JSONResponse
|
|
39
|
+
|
|
40
|
+
app = FastAPI()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_fetch_user_use_case() -> FetchUserUseCase:
|
|
44
|
+
return FetchUserUseCase()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.get("/users/{user_id}")
|
|
48
|
+
async def get_user(
|
|
49
|
+
user_id: int,
|
|
50
|
+
use_case: FetchUserUseCase = Depends(get_fetch_user_use_case),
|
|
51
|
+
) -> JSONResponse:
|
|
52
|
+
result = await use_case.execute(FetchUserInput(user_id=user_id))
|
|
53
|
+
return JSONResponse({"user_id": result.user_id, "name": result.name})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 外部依存を持つ UseCase の DI
|
|
59
|
+
|
|
60
|
+
リポジトリや外部クライアントを受け取る UseCase は、依存も Depends で注入します。
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class FetchUserUseCase:
|
|
64
|
+
def __init__(self, repository: UserRepositoryInterface) -> None:
|
|
65
|
+
self._repository = repository
|
|
66
|
+
|
|
67
|
+
async def execute(self, input_: FetchUserInput) -> FetchUserOutput:
|
|
68
|
+
user = await self._repository.find_by_id(input_.user_id)
|
|
69
|
+
return FetchUserOutput(user_id=user.id, name=user.name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_user_repository() -> UserRepositoryInterface:
|
|
73
|
+
return InMemoryUserRepository()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_fetch_user_use_case(
|
|
77
|
+
repository: UserRepositoryInterface = Depends(get_user_repository),
|
|
78
|
+
) -> FetchUserUseCase:
|
|
79
|
+
return FetchUserUseCase(repository)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 並行実行
|
|
85
|
+
|
|
86
|
+
複数の AsyncUseCase を並行実行するには `asyncio.gather()` を使います。
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.get("/dashboard")
|
|
93
|
+
async def dashboard(
|
|
94
|
+
user_id: int,
|
|
95
|
+
fetch_user: FetchUserUseCase = Depends(get_fetch_user_use_case),
|
|
96
|
+
fetch_stats: FetchStatsUseCase = Depends(get_fetch_stats_use_case),
|
|
97
|
+
) -> JSONResponse:
|
|
98
|
+
user, stats = await asyncio.gather(
|
|
99
|
+
fetch_user.execute(FetchUserInput(user_id=user_id)),
|
|
100
|
+
fetch_stats.execute(FetchStatsInput(user_id=user_id)),
|
|
101
|
+
)
|
|
102
|
+
return JSONResponse({"user": user.name, "stats": stats.count})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## isinstance() の注意点
|
|
108
|
+
|
|
109
|
+
`AsyncUseCaseProtocol` は `@runtime_checkable` ですが、`isinstance()` は
|
|
110
|
+
`execute` 属性の存在のみを確認します(sync/async の区別はしません)。
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# isinstance() は sync UseCase も True を返す(false positive)
|
|
114
|
+
isinstance(sync_use_case, AsyncUseCaseProtocol) # → True
|
|
115
|
+
|
|
116
|
+
# 正しい非同期確認方法
|
|
117
|
+
import inspect
|
|
118
|
+
inspect.iscoroutinefunction(use_case.execute) # → True/False
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
型安全性は `mypy --strict` の静的解析で保証します。詳細は ADR-0010 を参照してください。
|
|
@@ -18,11 +18,19 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
18
18
|
Args:
|
|
19
19
|
exclude_paths: Paths to skip logging for (e.g. ``["/health"]``).
|
|
20
20
|
Useful for high-frequency health-check endpoints where log noise is unwanted.
|
|
21
|
+
extra_context: Additional key-value pairs bound to every request log entry
|
|
22
|
+
(e.g. ``{"service": "my-api", "version": "1.0.0"}``).
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
app: object,
|
|
28
|
+
exclude_paths: list[str] | None = None,
|
|
29
|
+
extra_context: dict[str, str] | None = None,
|
|
30
|
+
) -> None:
|
|
24
31
|
super().__init__(app) # type: ignore[arg-type]
|
|
25
32
|
self._exclude_paths = set(exclude_paths or [])
|
|
33
|
+
self._extra_context: dict[str, str] = extra_context or {}
|
|
26
34
|
|
|
27
35
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
28
36
|
if request.url.path in self._exclude_paths:
|
|
@@ -34,6 +42,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
34
42
|
request_id=request_id_var.get(),
|
|
35
43
|
method=request.method,
|
|
36
44
|
path=request.url.path,
|
|
45
|
+
**self._extra_context,
|
|
37
46
|
)
|
|
38
47
|
logger.info("request.received")
|
|
39
48
|
response = await call_next(request)
|
|
@@ -10,12 +10,12 @@ from starlette.requests import Request
|
|
|
10
10
|
from starlette.responses import Response
|
|
11
11
|
|
|
12
12
|
_DEFAULT_CSP = "default-src 'self'"
|
|
13
|
+
_DEFAULT_PERMISSIONS_POLICY = "geolocation=(), microphone=()"
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
_STATIC_HEADERS: dict[str, str] = {
|
|
15
16
|
"X-Content-Type-Options": "nosniff",
|
|
16
17
|
"X-Frame-Options": "DENY",
|
|
17
18
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
18
|
-
"Permissions-Policy": "geolocation=(), microphone=()",
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
_DEFAULT_NO_CSP_PATHS: frozenset[str] = frozenset({"/docs", "/redoc", "/openapi.json"})
|
|
@@ -30,6 +30,11 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
30
30
|
Args:
|
|
31
31
|
csp: Custom Content-Security-Policy header value.
|
|
32
32
|
Defaults to ``"default-src 'self'"`` when not specified.
|
|
33
|
+
permissions_policy: Custom Permissions-Policy header value.
|
|
34
|
+
Defaults to ``"geolocation=(), microphone=()"`` when not specified.
|
|
35
|
+
hsts: Strict-Transport-Security header value (e.g.
|
|
36
|
+
``"max-age=31536000; includeSubDomains"``). Not set by default.
|
|
37
|
+
Enable only in production environments serving HTTPS.
|
|
33
38
|
extra_no_csp_paths: Additional paths to skip the CSP header for.
|
|
34
39
|
Useful when FastAPI is configured with custom ``docs_url`` / ``redoc_url``.
|
|
35
40
|
The built-in paths ``/docs``, ``/redoc``, and ``/openapi.json`` are always included.
|
|
@@ -39,16 +44,25 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
39
44
|
self,
|
|
40
45
|
app: object,
|
|
41
46
|
csp: str | None = None,
|
|
47
|
+
permissions_policy: str | None = None,
|
|
48
|
+
hsts: str | None = None,
|
|
42
49
|
extra_no_csp_paths: list[str] | None = None,
|
|
43
50
|
) -> None:
|
|
44
51
|
super().__init__(app) # type: ignore[arg-type]
|
|
45
52
|
self._csp = csp if csp is not None else _DEFAULT_CSP
|
|
53
|
+
self._permissions_policy = (
|
|
54
|
+
permissions_policy if permissions_policy is not None else _DEFAULT_PERMISSIONS_POLICY
|
|
55
|
+
)
|
|
56
|
+
self._hsts = hsts
|
|
46
57
|
self._no_csp_paths = _DEFAULT_NO_CSP_PATHS | frozenset(extra_no_csp_paths or [])
|
|
47
58
|
|
|
48
59
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
49
60
|
response = await call_next(request)
|
|
50
|
-
for header, value in
|
|
61
|
+
for header, value in _STATIC_HEADERS.items():
|
|
51
62
|
response.headers[header] = value
|
|
63
|
+
response.headers["Permissions-Policy"] = self._permissions_policy
|
|
64
|
+
if self._hsts:
|
|
65
|
+
response.headers["Strict-Transport-Security"] = self._hsts
|
|
52
66
|
if request.url.path not in self._no_csp_paths:
|
|
53
67
|
response.headers["Content-Security-Policy"] = self._csp
|
|
54
68
|
return response
|
|
@@ -56,3 +56,11 @@ def test_composite_with_empty_checks_returns_ok() -> None:
|
|
|
56
56
|
result = composite.check()
|
|
57
57
|
assert result.is_healthy is True
|
|
58
58
|
assert result.checks == {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_health_status_http_status_code_ok() -> None:
|
|
62
|
+
assert HealthStatus(status="ok").http_status_code == 200
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_health_status_http_status_code_error() -> None:
|
|
66
|
+
assert HealthStatus(status="error").http_status_code == 503
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for RequestLoggingMiddleware."""
|
|
2
2
|
|
|
3
|
+
import structlog
|
|
3
4
|
from fastapi import FastAPI
|
|
4
5
|
from fastapi.responses import JSONResponse
|
|
5
6
|
from fastapi.testclient import TestClient
|
|
@@ -41,6 +42,42 @@ def test_logging_does_not_remove_headers() -> None:
|
|
|
41
42
|
assert "X-Request-Id" in response.headers
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def test_extra_context_is_bound_to_structlog() -> None:
|
|
46
|
+
captured: list[dict] = []
|
|
47
|
+
app = FastAPI()
|
|
48
|
+
app.add_middleware(
|
|
49
|
+
RequestLoggingMiddleware,
|
|
50
|
+
extra_context={"service": "my-api", "version": "1.0"},
|
|
51
|
+
)
|
|
52
|
+
app.add_middleware(RequestIdMiddleware)
|
|
53
|
+
|
|
54
|
+
@app.get("/ping")
|
|
55
|
+
async def ping() -> JSONResponse:
|
|
56
|
+
captured.append(dict(structlog.contextvars.get_contextvars()))
|
|
57
|
+
return JSONResponse({"ok": True})
|
|
58
|
+
|
|
59
|
+
client = TestClient(app)
|
|
60
|
+
client.get("/ping")
|
|
61
|
+
assert captured[0]["service"] == "my-api"
|
|
62
|
+
assert captured[0]["version"] == "1.0"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_extra_context_default_is_empty() -> None:
|
|
66
|
+
captured: list[dict] = []
|
|
67
|
+
app = FastAPI()
|
|
68
|
+
app.add_middleware(RequestLoggingMiddleware) # extra_context なし
|
|
69
|
+
app.add_middleware(RequestIdMiddleware)
|
|
70
|
+
|
|
71
|
+
@app.get("/ping")
|
|
72
|
+
async def ping() -> JSONResponse:
|
|
73
|
+
captured.append(dict(structlog.contextvars.get_contextvars()))
|
|
74
|
+
return JSONResponse({"ok": True})
|
|
75
|
+
|
|
76
|
+
client = TestClient(app)
|
|
77
|
+
client.get("/ping")
|
|
78
|
+
assert "service" not in captured[0]
|
|
79
|
+
|
|
80
|
+
|
|
44
81
|
def test_exclude_paths_passes_requests_through() -> None:
|
|
45
82
|
"""exclude_paths に指定したパスへのリクエストがミドルウェアを通過すること"""
|
|
46
83
|
app = FastAPI()
|
|
@@ -80,6 +80,44 @@ def test_extra_no_csp_paths() -> None:
|
|
|
80
80
|
assert "Content-Security-Policy" in client.get("/ping").headers
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_custom_permissions_policy() -> None:
|
|
84
|
+
app = FastAPI()
|
|
85
|
+
app.add_middleware(
|
|
86
|
+
SecurityHeadersMiddleware,
|
|
87
|
+
permissions_policy="geolocation=(self), microphone=()",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@app.get("/ping")
|
|
91
|
+
async def ping() -> JSONResponse:
|
|
92
|
+
return JSONResponse({"ok": True})
|
|
93
|
+
|
|
94
|
+
client = TestClient(app)
|
|
95
|
+
r = client.get("/ping")
|
|
96
|
+
assert r.headers["Permissions-Policy"] == "geolocation=(self), microphone=()"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_hsts_header_when_specified() -> None:
|
|
100
|
+
app = FastAPI()
|
|
101
|
+
app.add_middleware(
|
|
102
|
+
SecurityHeadersMiddleware,
|
|
103
|
+
hsts="max-age=31536000; includeSubDomains",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@app.get("/ping")
|
|
107
|
+
async def ping() -> JSONResponse:
|
|
108
|
+
return JSONResponse({"ok": True})
|
|
109
|
+
|
|
110
|
+
client = TestClient(app)
|
|
111
|
+
r = client.get("/ping")
|
|
112
|
+
assert r.headers["Strict-Transport-Security"] == "max-age=31536000; includeSubDomains"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_no_hsts_by_default() -> None:
|
|
116
|
+
client = TestClient(_make_app())
|
|
117
|
+
r = client.get("/ping")
|
|
118
|
+
assert "Strict-Transport-Security" not in r.headers
|
|
119
|
+
|
|
120
|
+
|
|
83
121
|
def test_default_no_csp_paths_still_work_with_extra_paths() -> None:
|
|
84
122
|
app = FastAPI()
|
|
85
123
|
app.add_middleware(SecurityHeadersMiddleware, extra_no_csp_paths=["/custom"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|