nene2-python 1.6.0__tar.gz → 1.8.0__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.6.0 → nene2_python-1.8.0}/CHANGELOG.md +41 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/PKG-INFO +1 -1
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-14.md +45 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-15.md +67 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-16.md +62 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-17.md +66 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-18.md +60 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-19.md +98 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-20.md +94 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-21.md +104 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-22.md +87 -0
- nene2_python-1.8.0/docs/field-trials/2026-05-field-trial-23.md +80 -0
- nene2_python-1.8.0/docs/how-to/problem-details.md +107 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/pyproject.toml +1 -1
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/__init__.py +2 -1
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/exceptions.py +4 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/sqlalchemy_executor.py +17 -5
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/http/__init__.py +4 -2
- nene2_python-1.8.0/src/nene2/http/health.py +48 -0
- nene2_python-1.8.0/src/nene2/http/problem_details.py +61 -0
- nene2_python-1.8.0/src/nene2/log/__init__.py +5 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/log/setup.py +40 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/__init__.py +2 -1
- nene2_python-1.8.0/src/nene2/middleware/domain_exception.py +82 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/request_logging.py +13 -1
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/request_size_limit.py +1 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/security_headers.py +24 -7
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/throttle.py +42 -10
- nene2_python-1.8.0/src/nene2/use_case/protocols.py +51 -0
- nene2_python-1.8.0/tests/nene2/database/test_transaction.py +137 -0
- nene2_python-1.8.0/tests/nene2/http/test_health.py +58 -0
- nene2_python-1.8.0/tests/nene2/http/test_problem_details.py +63 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_request_logging.py +19 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_request_size_limit.py +1 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_security_headers.py +41 -0
- nene2_python-1.8.0/tests/nene2/middleware/test_simple_domain_handler.py +100 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_throttle.py +28 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/uv.lock +1 -1
- nene2_python-1.6.0/src/nene2/http/health.py +0 -20
- nene2_python-1.6.0/src/nene2/http/problem_details.py +0 -37
- nene2_python-1.6.0/src/nene2/log/__init__.py +0 -5
- nene2_python-1.6.0/src/nene2/middleware/domain_exception.py +0 -18
- nene2_python-1.6.0/src/nene2/use_case/protocols.py +0 -24
- nene2_python-1.6.0/tests/nene2/database/test_transaction.py +0 -73
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.env.example +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.gitignore +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.vitepress/config.mts +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/AGENTS.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/CLAUDE.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/Dockerfile +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/LICENSE +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/README.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/alembic/README +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/alembic/env.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/alembic/script.py.mako +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/alembic.ini +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/compose.yaml +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/de/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/fr/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/pt-br/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/reference/api.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/reference/configuration.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/roadmap.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/todo/current.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/zh/index.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/package-lock.json +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/package.json +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/__main__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/app.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/entity.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/handler.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/mcp.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/entity.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/handler.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/note/use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/schema.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/entity.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/handler.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/health.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/py.typed +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/scripts/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/conftest.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/test_cors.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.6.0 → nene2_python-1.8.0}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,47 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.0] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT18〜FT23 フィールドトライアル — ログテスト・Problem Details・ThrottleMiddleware・ドメイン例外・HealthCheck・RequestSizeLimit の各改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `nene2.log.configure_for_testing()` — structlog を pytest の `caplog` でキャプチャできるように設定するヘルパー関数 (FT18)
|
|
14
|
+
- `nene2.http.configure_problem_details(base_url)` — プロジェクト全体のデフォルト `base_url` を一箇所で設定する関数 (FT19)
|
|
15
|
+
- `ThrottleMiddleware` — 全レスポンスに `X-RateLimit-Limit`/`Remaining`/`Reset` ヘッダーを付与 (FT20)
|
|
16
|
+
- `SimpleDomainHandler` — `exception_class`/`problem_type`/`title`/`status` を渡すだけでドメイン例外ハンドラーを作成できるファクトリクラス (FT21)
|
|
17
|
+
- `nene2.http.CompositeHealthCheck` — 複数の `HealthCheckProtocol` を集約するクラス (FT22)
|
|
18
|
+
- `HealthCheckProtocol` に `@runtime_checkable` を追加 (FT22)
|
|
19
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-18.md` 〜 `docs/field-trials/2026-05-field-trial-23.md`
|
|
20
|
+
- `docs/how-to/problem-details.md` — Problem Details の使い方ガイドを追加
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `RequestLoggingMiddleware` — `exclude_paths: list[str] | None` パラメータを追加(特定パスのログをスキップ可能に)(FT18)
|
|
24
|
+
- `ThrottleMiddleware` — 内部の `_is_allowed()` を `_check_rate()` にリネームし、戻り値を `_RateInfo` dataclass に変更 (FT20)
|
|
25
|
+
- `RequestSizeLimitMiddleware` — 413 レスポンスに `max_bytes` 構造化フィールドを追加 (FT23)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## [1.7.0] — 2026-05-20
|
|
30
|
+
|
|
31
|
+
FT14〜FT17 フィールドトライアル — プロトコル docstring 改善・ミドルウェアカスタマイズ・DB 例外統一・バグ修正。
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `SecurityHeadersMiddleware` — `csp: str | None` パラメータで Content-Security-Policy 値をカスタマイズ可能に (FT15)
|
|
35
|
+
- `SecurityHeadersMiddleware` — `extra_no_csp_paths: list[str] | None` パラメータでカスタム OpenAPI パスの CSP スキップを設定可能に (FT15)
|
|
36
|
+
- `DatabaseIntegrityException` — UNIQUE/FK/CHECK 制約違反時に発生する新例外クラス (FT16)
|
|
37
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-14.md` 〜 `docs/field-trials/2026-05-field-trial-17.md`
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- `AsyncUseCaseProtocol` / `UseCaseProtocol` — docstring に `@runtime_checkable` の `isinstance` 制限と `inspect.iscoroutinefunction()` によるランタイム確認方法を明記 (FT14)
|
|
41
|
+
- `SqlAlchemyTransactionManager.transactional()` — `IntegrityError` をキャッチして `DatabaseIntegrityException` にラップするよう変更 (FT16)
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- `SqlAlchemyQueryExecutor.write()` — `IntegrityError` が `DatabaseIntegrityException` にラップされない不整合を修正 (FT17-F1)
|
|
45
|
+
- `SqlAlchemyQueryExecutor.write()` と `_BoundQueryExecutor.write()` — UPDATE/DELETE で 0 行影響した場合に前の INSERT の `lastrowid` が返るバグを修正; INSERT のみ `lastrowid`、UPDATE/DELETE は `rowcount` を返すよう変更 (FT17-F2)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
8
49
|
## [1.6.0] — 2026-05-20
|
|
9
50
|
|
|
10
51
|
FT13 (ValidationException実運用) field trial — validation DX improvements.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
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,45 @@
|
|
|
1
|
+
# Field Trial 14 — AsyncUseCaseProtocol 実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** Weather Dashboard API(複数都市の天気を asyncio.gather で並列取得)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft14-async/`
|
|
6
|
+
**nene2-python version:** v1.6.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`AsyncUseCaseProtocol` を使った非同期 UseCase を実際に実装し、
|
|
11
|
+
`asyncio.gather` による並列 I/O の動作とプロトコルの挙動を検証した。
|
|
12
|
+
|
|
13
|
+
## 動作確認結果
|
|
14
|
+
|
|
15
|
+
- `AsyncUseCaseProtocol` の `isinstance` 検査が正しく動くこと ✓
|
|
16
|
+
- `asyncio.gather` で 4 都市を並列取得した場合、50ms(直列の場合 200ms)で完了すること ✓
|
|
17
|
+
- `FetchDashboardUseCase(fetch_weather: AsyncUseCaseProtocol[...])` のコンストラクタインジェクションが機能すること ✓
|
|
18
|
+
- FastAPI の `async def` ハンドラーから非同期 UseCase を `await` で呼び出せること ✓
|
|
19
|
+
|
|
20
|
+
## 摩擦点
|
|
21
|
+
|
|
22
|
+
### FT14-F1 (LOW, ドキュメント): runtime_checkable の制限がプロトコルの docstring に記載されていない
|
|
23
|
+
|
|
24
|
+
`isinstance(sync_obj, AsyncUseCaseProtocol)` が `True` を返す(Python の `@runtime_checkable` は
|
|
25
|
+
メソッド名の存在のみを検査し、async/sync を区別しない)。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
class FakeSyncWeather:
|
|
29
|
+
def execute(self, input_: WeatherInput) -> str: # async ではない
|
|
30
|
+
return "fake"
|
|
31
|
+
|
|
32
|
+
isinstance(FakeSyncWeather(), AsyncUseCaseProtocol) # → True(注意が必要)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
この制限は ADR-0010 に記録済みだが、`AsyncUseCaseProtocol` の docstring には記載されていない。
|
|
36
|
+
利用者が docstring だけ見て `isinstance` でランタイムガードを書くと誤動作する。
|
|
37
|
+
|
|
38
|
+
**対応**: プロトコルの docstring に「mypy --strict が静的に保証する; isinstance はメソッド名のみを検査する」旨を追記する。
|
|
39
|
+
|
|
40
|
+
## まとめ
|
|
41
|
+
|
|
42
|
+
基本動作は問題なし。`asyncio.gather` パターン・コンストラクタインジェクション・FastAPI 統合のいずれも
|
|
43
|
+
摩擦なく動作した。唯一の摩擦は docstring によるドキュメントギャップ(LOW)のみ。
|
|
44
|
+
|
|
45
|
+
FT14 は「設計が正しく実装されている」ことの確認として有用だった。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Field Trial 15 — SecurityHeadersMiddleware 実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** FT15 Security Headers API(セキュリティヘッダーの付与・CSP 動作確認)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft15-security-headers/`
|
|
6
|
+
**nene2-python version:** v1.6.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`SecurityHeadersMiddleware` を実際のアプリに組み込み、各ヘッダーの付与動作・
|
|
11
|
+
CSP スキップロジック・カスタマイズ可能性を検証した。
|
|
12
|
+
|
|
13
|
+
## 動作確認結果
|
|
14
|
+
|
|
15
|
+
- 通常エンドポイントに `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Content-Security-Policy`, `Permissions-Policy` の全ヘッダーが付与されること ✓
|
|
16
|
+
- `/docs`, `/redoc`, `/openapi.json` では `Content-Security-Policy` がスキップされること ✓
|
|
17
|
+
- CSP スキップ時も他のヘッダーは引き続き付与されること ✓
|
|
18
|
+
|
|
19
|
+
## 摩擦点
|
|
20
|
+
|
|
21
|
+
### FT15-F1 (MEDIUM, 拡張性): カスタム OpenAPI パスで CSP スキップが効かない
|
|
22
|
+
|
|
23
|
+
`SecurityHeadersMiddleware` がスキップする OpenAPI パスは `_OPENAPI_PATHS` 定数で
|
|
24
|
+
`{"/docs", "/redoc", "/openapi.json"}` にハードコードされている。
|
|
25
|
+
|
|
26
|
+
FastAPI では `docs_url` / `redoc_url` でカスタムパスを設定できるが、
|
|
27
|
+
カスタムパス(例: `/api-docs`)では CSP スキップが効かず、Swagger UI が CDN アセットを
|
|
28
|
+
ブロックされる可能性がある。
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
app = FastAPI(docs_url="/api-docs", redoc_url="/api-redoc")
|
|
32
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
33
|
+
# → /api-docs に CSP "default-src 'self'" が付いてしまう
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**対応**: コンストラクタに `extra_no_csp_paths: list[str] | None = None` を追加し、
|
|
37
|
+
ユーザーがカスタム OpenAPI パスを指定できるようにする。または `ThrottleMiddleware` と
|
|
38
|
+
同様の `exclude_paths` パターンで完全スキップを可能にする。
|
|
39
|
+
|
|
40
|
+
### FT15-F2 (LOW, 拡張性): CSP をコンストラクタで上書きできない
|
|
41
|
+
|
|
42
|
+
CSP の値 `"default-src 'self'"` はモジュールレベルの `_HEADERS` 定数にハードコードされており、
|
|
43
|
+
コンストラクタから上書きできない。
|
|
44
|
+
|
|
45
|
+
CDN アセットを許可したい(`content-src 'self' cdn.example.com`)など、
|
|
46
|
+
CSP を変更したいユーザーは `_HEADERS` をモジュールレベルで直接書き換えるしかなく、
|
|
47
|
+
グローバルな副作用が発生する。
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# 現状の唯一の方法(副作用あり)
|
|
51
|
+
from nene2.middleware import security_headers
|
|
52
|
+
security_headers._HEADERS["Content-Security-Policy"] = "default-src 'self' cdn.example.com"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**対応**: `SecurityHeadersMiddleware(csp: str | None = None)` でコンストラクタから
|
|
56
|
+
CSP 値を上書きできるようにする。
|
|
57
|
+
|
|
58
|
+
## まとめ
|
|
59
|
+
|
|
60
|
+
基本動作は問題なし。全セキュリティヘッダーの付与・OpenAPI パスでの CSP スキップも
|
|
61
|
+
正常に機能した。摩擦は拡張性の面で2点あり:
|
|
62
|
+
|
|
63
|
+
- FT15-F1 (MEDIUM): カスタム OpenAPI パスで CSP スキップが効かない
|
|
64
|
+
- FT15-F2 (LOW): CSP 値をコンストラクタから変更できない
|
|
65
|
+
|
|
66
|
+
`ThrottleMiddleware` が `exclude_paths` を持つのに対し、`SecurityHeadersMiddleware` は
|
|
67
|
+
カスタマイズポイントがゼロであるため、実用アプリでの柔軟性が低い。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Field Trial 16 — transactional(callback) パターン実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** 銀行口座送金 API(送金元の残高を減らして送金先に加算する atomic 操作)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft16-transaction/`
|
|
6
|
+
**nene2-python version:** v1.6.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`transactional(callback)` パターンを実際の送金ユースケースに適用し、
|
|
11
|
+
ロールバック挙動・例外処理・async コンテキストとの相互作用を検証した。
|
|
12
|
+
|
|
13
|
+
## 動作確認結果
|
|
14
|
+
|
|
15
|
+
- `transactional(callback)` が atomic に実行されること ✓
|
|
16
|
+
- `CHECK 制約違反`(残高 < 0)発生時に全操作がロールバックされること ✓
|
|
17
|
+
- `UNIQUE 制約違反`(IntegrityError)でもロールバックが正しく行われること ✓
|
|
18
|
+
- SQLite `:memory:` + `StaticPool` で `SqlAlchemyQueryExecutor` と `SqlAlchemyTransactionManager` を同一 DB に向けられること ✓
|
|
19
|
+
|
|
20
|
+
## 摩擦点
|
|
21
|
+
|
|
22
|
+
### FT16-F1 (MEDIUM, API一貫性): IntegrityError が DatabaseConnectionException にラップされない
|
|
23
|
+
|
|
24
|
+
`SqlAlchemyTransactionManager.transactional()` は `OperationalError` のみを
|
|
25
|
+
`DatabaseConnectionException` に変換し、`IntegrityError`(UNIQUE 制約違反・FK 制約違反など)は
|
|
26
|
+
生の SQLAlchemy 例外として呼び出し側に伝播する。
|
|
27
|
+
|
|
28
|
+
UseCase 層でフレームワーク独自例外に統一されていないため、
|
|
29
|
+
呼び出し元が SQLAlchemy の例外型に依存した `except IntegrityError` を書く必要がある。
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from sqlalchemy.exc import IntegrityError
|
|
33
|
+
|
|
34
|
+
with pytest.raises(IntegrityError): # 摩擦: SQLAlchemy 依存が漏れ出す
|
|
35
|
+
tx_manager.transactional(_duplicate_insert)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**対応案**: `IntegrityError` も `DatabaseConnectionException`(または新設の `DatabaseIntegrityException`)にラップする。または `transactional()` がキャッチすべき SQLAlchemy 例外一覧をドキュメントに明記する。
|
|
39
|
+
|
|
40
|
+
### FT16-F2 (LOW, 非同期対応): async コンテキストから transactional() を呼ぶとイベントループをブロックする
|
|
41
|
+
|
|
42
|
+
`SqlAlchemyTransactionManager.transactional()` は同期 API であるため、
|
|
43
|
+
FastAPI の `async def` ハンドラーから直接呼ぶとイベントループをブロックする。
|
|
44
|
+
現状の実装でも動作はするが、高負荷時にパフォーマンス劣化の原因になりうる。
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
@app.post("/transfer")
|
|
48
|
+
async def transfer(...) -> JSONResponse:
|
|
49
|
+
# 同期 transactional() を直接呼んでいる(イベントループブロッキング)
|
|
50
|
+
result = transfer_uc.execute(...)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**対応案**: `asyncio.to_thread()` でのラップをドキュメントに記載する。
|
|
54
|
+
または `AsyncSqlAlchemyTransactionManager` を将来的に追加する(SQLAlchemy async core を利用)。
|
|
55
|
+
|
|
56
|
+
## まとめ
|
|
57
|
+
|
|
58
|
+
コアの atomic 保証(コミット・ロールバック)は期待通りに機能した。
|
|
59
|
+
摩擦は例外ハンドリングの API 一貫性(F1: MEDIUM)と非同期対応(F2: LOW)の2点。
|
|
60
|
+
|
|
61
|
+
F1 は UseCase 層が SQLAlchemy に依存するアーキテクチャ上の問題であり、
|
|
62
|
+
Clean Architecture の原則に照らして対応が必要。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Field Trial 17 — 複数ドメイン連携実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** タスク管理API(Task + Category の2ドメイン連携)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft17-multi-domain/`
|
|
6
|
+
**nene2-python version:** v1.6.0 (local dev build)
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
Task(タスク)と Category(カテゴリ)の2ドメインを連携させたアプリを実装し、
|
|
11
|
+
`SqlAlchemyQueryExecutor.write()` と `DatabaseIntegrityException`、
|
|
12
|
+
`transactional(callback)` の混在パターンの DX を検証した。
|
|
13
|
+
|
|
14
|
+
## 動作確認結果
|
|
15
|
+
|
|
16
|
+
- カテゴリ一覧・タスク一覧・カテゴリでのフィルタリングが動作すること ✓
|
|
17
|
+
- タスク完了(UPDATE)操作が正常に機能すること ✓(ただし FT17-F2 の制約あり)
|
|
18
|
+
- `transactional()` 内の `IntegrityError` が `DatabaseIntegrityException` に変換されること ✓
|
|
19
|
+
|
|
20
|
+
## 摩擦点
|
|
21
|
+
|
|
22
|
+
### FT17-F1 (HIGH, バグ): SqlAlchemyQueryExecutor.write() が IntegrityError をキャッチしない
|
|
23
|
+
|
|
24
|
+
`_BoundQueryExecutor.write()`(`transactional()` 内部で使われる)は FT16 で `IntegrityError` を
|
|
25
|
+
`DatabaseIntegrityException` に変換するよう修正されたが、
|
|
26
|
+
`SqlAlchemyQueryExecutor.write()`(直接呼び出し)は対応していない。
|
|
27
|
+
|
|
28
|
+
同じ「INSERT が重複するケース」でも、使う API によって異なる例外型が飛ぶ:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# 直接 write() → IntegrityError (SQLAlchemy)
|
|
32
|
+
executor.write("INSERT INTO categories (name) VALUES (:name)", {"name": "dup"})
|
|
33
|
+
|
|
34
|
+
# transactional() 内 write() → DatabaseIntegrityException (nene2)
|
|
35
|
+
tx_manager.transactional(lambda ex: ex.write("INSERT INTO categories (name) VALUES (:name)", {"name": "dup"}))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
UseCase 層でどちらの API を使うかによって `except` 節を変える必要があり、一貫性がない。
|
|
39
|
+
|
|
40
|
+
**対応**: `SqlAlchemyQueryExecutor.write()` でも `IntegrityError` をキャッチして
|
|
41
|
+
`DatabaseIntegrityException` にラップする。
|
|
42
|
+
|
|
43
|
+
### FT17-F2 (HIGH, バグ): write() が UPDATE/DELETE で 0 行影響した場合に誤った値を返す
|
|
44
|
+
|
|
45
|
+
`write()` は `result.lastrowid or result.rowcount` を返すが、SQLite では UPDATE/DELETE の後も
|
|
46
|
+
`cursor.lastrowid` が前の INSERT の rowid を保持する。
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# DB に id=1 の行がある状態で
|
|
50
|
+
result = executor.write("UPDATE items SET name = 'x' WHERE id = 999") # 0行影響
|
|
51
|
+
# result == 1 (前のINSERTのlastrowid) — 期待値は 0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`if affected == 0: raise NotFound` のパターンが正しく動かない。
|
|
55
|
+
|
|
56
|
+
**対応**: SQL が `INSERT` で始まる場合のみ `lastrowid` を使い、UPDATE/DELETE は `rowcount` を返す。
|
|
57
|
+
|
|
58
|
+
## まとめ
|
|
59
|
+
|
|
60
|
+
ドメイン間連携(JOIN クエリ、カテゴリフィルタリング)は問題なく動作した。
|
|
61
|
+
ただし `SqlAlchemyQueryExecutor.write()` に HIGH レベルのバグが2件あり:
|
|
62
|
+
|
|
63
|
+
- FT17-F1: `IntegrityError` が `DatabaseIntegrityException` に変換されない(API 非対称)
|
|
64
|
+
- FT17-F2: UPDATE/DELETE で 0 行影響した場合の戻り値が不正
|
|
65
|
+
|
|
66
|
+
両方とも `_BoundQueryExecutor.write()` には修正済みだが、`SqlAlchemyQueryExecutor.write()` に未適用。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Field Trial 18 — RequestLoggingMiddleware 実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** FT18 Request Logging API(structlog ログ・request_id 連携・JSON ログ形式検証)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft18-request-logging/`
|
|
6
|
+
**nene2-python version:** v1.7.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`RequestLoggingMiddleware` を `RequestIdMiddleware` と組み合わせて実際に動かし、
|
|
11
|
+
ログ出力の内容・request_id との連携・テストでのログ検証方法を確認した。
|
|
12
|
+
|
|
13
|
+
## 動作確認結果
|
|
14
|
+
|
|
15
|
+
- `request.received` / `request.completed` のログが出力されること ✓
|
|
16
|
+
- ログに `method`, `path`, `status_code`, `duration_ms` が含まれること ✓
|
|
17
|
+
- `RequestIdMiddleware` と組み合わせると `request_id` がログに入ること ✓
|
|
18
|
+
- ログに `request_id` が含まれ、レスポンスヘッダーの `X-Request-Id` と一致すること ✓
|
|
19
|
+
|
|
20
|
+
## 摩擦点
|
|
21
|
+
|
|
22
|
+
### FT18-F1 (MEDIUM, テスト容易性): structlog が pytest caplog で捕捉できない
|
|
23
|
+
|
|
24
|
+
`RequestLoggingMiddleware` は structlog を使ってログを出力するが、
|
|
25
|
+
structlog のデフォルト設定では Python stdlib の logging ハンドラーを経由せず
|
|
26
|
+
直接 stdout に書き込む。
|
|
27
|
+
|
|
28
|
+
そのため pytest の `caplog` では構造化ログを捕捉できない。
|
|
29
|
+
テストで `structlog` のログを検証するには `capsys` で stdout を捕捉するか、
|
|
30
|
+
structlog を stdlib logging に向ける設定変更が必要。
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# caplog では捕捉できない(摩擦)
|
|
34
|
+
with caplog.at_level(logging.INFO, logger="nene2.middleware.request_logging"):
|
|
35
|
+
client.get("/health")
|
|
36
|
+
assert len(caplog.records) == 0 # 常に 0 — ログが入らない
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**対応案**: `nene2.log` に `configure_for_testing()` ヘルパーを追加し、
|
|
40
|
+
テスト環境で `structlog` を stdlib logging ブリッジ経由で使える設定を提供する。
|
|
41
|
+
|
|
42
|
+
### FT18-F2 (LOW, 拡張性): ログレベルをコンストラクタから変更できない
|
|
43
|
+
|
|
44
|
+
`RequestLoggingMiddleware` は常に `logger.info()` を使う。
|
|
45
|
+
ヘルスチェックなど高頻度のパスのログを `DEBUG` に落としたい場合や、
|
|
46
|
+
特定パスのログを無効化したい場合にコンストラクタパラメータで設定できない。
|
|
47
|
+
|
|
48
|
+
他のミドルウェア(`ThrottleMiddleware` の `exclude_paths` など)との一貫性が取れていない。
|
|
49
|
+
|
|
50
|
+
**対応案**: `exclude_paths: list[str] | None = None` パラメータを追加して
|
|
51
|
+
特定パスのログを無効化できるようにする。または `log_level` パラメータで
|
|
52
|
+
デフォルトのログレベルを変更可能にする。
|
|
53
|
+
|
|
54
|
+
## まとめ
|
|
55
|
+
|
|
56
|
+
基本的なリクエストロギングは問題なく動作した。`RequestIdMiddleware` との連携も良好。
|
|
57
|
+
摩擦はテスト時の `caplog` 非対応(F1: MEDIUM)と拡張性(F2: LOW)の2点。
|
|
58
|
+
|
|
59
|
+
F1 は structlog の設計に起因するが、テスト用ヘルパーを提供することで改善できる。
|
|
60
|
+
F2 は他のミドルウェアとの一貫性の問題。
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# FT19: problem_details_response() RFC 9457 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `problem_details_response()` を使って RFC 9457 準拠のエラー応答を実装する
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft19-problem-details/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.http.problem_details_response()` を実際のアプリ(記事 API)に組み込み、
|
|
12
|
+
401/404/422 のエラー応答を RFC 9457 形式で返すパターンを検証する。
|
|
13
|
+
また、`ErrorHandlerMiddleware` との統合状況を確認する。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実施内容
|
|
18
|
+
|
|
19
|
+
記事 CRUD API(`/articles/{article_id}`)を作成し、以下のシナリオで `problem_details_response()` を使用:
|
|
20
|
+
|
|
21
|
+
- **401 Unauthorized**: X-API-Key ヘッダー未提供または無効
|
|
22
|
+
- **404 Not Found**: 存在しない記事 ID へのアクセス(`extra={"article_id": article_id}` 付き)
|
|
23
|
+
- **422 Validation Failed**: 空のタイトルで記事作成(`extra={"errors": [...]}` 付き)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## テスト結果
|
|
28
|
+
|
|
29
|
+
### test_app.py(正常系・準拠確認)
|
|
30
|
+
| テスト | 結果 |
|
|
31
|
+
|---|---|
|
|
32
|
+
| test_get_article_success | PASS |
|
|
33
|
+
| test_get_article_not_found_returns_problem_details | PASS |
|
|
34
|
+
| test_unauthorized_returns_problem_details | PASS |
|
|
35
|
+
| test_validation_error_returns_problem_details | PASS |
|
|
36
|
+
| test_problem_details_type_is_absolute_url | PASS |
|
|
37
|
+
|
|
38
|
+
### test_friction.py(摩擦点確認)
|
|
39
|
+
| テスト | 結果 | 摩擦 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| test_base_url_is_not_customizable_per_call | PASS | あり |
|
|
42
|
+
| test_validation_exception_and_problem_details_not_integrated | PASS | なし(当初の予想と逆) |
|
|
43
|
+
| test_no_typed_problem_type_constants | PASS | あり |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 発見した摩擦点
|
|
48
|
+
|
|
49
|
+
### FT19-F1: プロジェクト全体の base_url を一箇所で設定できない
|
|
50
|
+
|
|
51
|
+
**概要**: `problem_details_response()` は `base_url` パラメータを持つが、
|
|
52
|
+
プロジェクト全体で一箇所に設定する仕組みがない。
|
|
53
|
+
複数のハンドラーで同じ `base_url` を保証するには、
|
|
54
|
+
毎回引数で渡すか、プロジェクトでラッパー関数を書く必要がある。
|
|
55
|
+
|
|
56
|
+
**影響**: 大規模プロジェクトで `base_url` の不統一が発生しやすい。
|
|
57
|
+
|
|
58
|
+
**期待する解決策**: `configure_problem_details(base_url: str)` のような
|
|
59
|
+
モジュールレベルの設定関数を提供する。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### FT19-F2: ErrorHandlerMiddleware と problem_details_response() の統合(摩擦なし)
|
|
64
|
+
|
|
65
|
+
当初「`ErrorHandlerMiddleware` が `ValidationException` を処理する際に
|
|
66
|
+
`application/json` を返すのではないか」と懸念していたが、
|
|
67
|
+
実際には `problem_details_response()` を内部で使っており、
|
|
68
|
+
`application/problem+json` が正しく返される。
|
|
69
|
+
|
|
70
|
+
**結論**: 摩擦なし。むしろ正しく統合されている。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### FT19-F3: problem_type 文字列に型安全な定数がない
|
|
75
|
+
|
|
76
|
+
**概要**: `problem_type` は文字列リテラルで渡すため、タイポがあっても
|
|
77
|
+
mypy では検出されない。同じプロジェクト内で `"not-found"` と `"not_found"` が
|
|
78
|
+
混在してもエラーにならない。
|
|
79
|
+
|
|
80
|
+
**影響**: 大規模プロジェクトで problem_type の不統一が起きやすい。
|
|
81
|
+
|
|
82
|
+
**期待する解決策**: 標準的な problem_type 定数のドキュメント化、
|
|
83
|
+
またはユーザーが StrEnum を使うパターンのガイダンス。
|
|
84
|
+
(フレームワーク側で全 problem_type を定義するのは over-engineering のため、
|
|
85
|
+
ドキュメントやパターン提示が適切)
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## まとめ
|
|
90
|
+
|
|
91
|
+
`problem_details_response()` は RFC 9457 に準拠しており、`ErrorHandlerMiddleware` と
|
|
92
|
+
も正しく統合されている。実用上の摩擦は以下の 2 点:
|
|
93
|
+
|
|
94
|
+
1. **プロジェクト全体の base_url 設定機構がない** → Issue 化して修正対象
|
|
95
|
+
2. **problem_type 文字列の型安全性がない** → Issue 化してドキュメント対応
|
|
96
|
+
|
|
97
|
+
v1.4.0 で追加された `exclude_paths`、FT15-FT18 で追加された各ミドルウェア改善も
|
|
98
|
+
本 FT で間接的に動作確認できた。
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# FT20: ThrottleMiddleware 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `ThrottleMiddleware` を使ったレート制限 API の実運用検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft20-throttle/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.middleware.ThrottleMiddleware` を実際のアプリ(公開 API + ヘルスチェック)に組み込み、
|
|
12
|
+
レート制限の動作確認と摩擦点を発見する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
- `limit=3, window=60` のレート制限を設定した FastAPI アプリを作成
|
|
19
|
+
- `/health` を `exclude_paths` で除外
|
|
20
|
+
- `/api/data`、`/api/expensive` の 2 エンドポイントにレート制限を適用
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## テスト結果
|
|
25
|
+
|
|
26
|
+
### test_app.py(正常系・機能確認)
|
|
27
|
+
| テスト | 結果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| test_health_check_is_not_rate_limited | PASS |
|
|
30
|
+
| test_requests_within_limit_succeed | PASS |
|
|
31
|
+
| test_requests_exceeding_limit_return_429 | PASS |
|
|
32
|
+
| test_rate_limit_429_includes_retry_after_header | PASS |
|
|
33
|
+
| test_rate_limit_applies_across_different_endpoints | PASS |
|
|
34
|
+
|
|
35
|
+
### test_friction.py(摩擦点確認)
|
|
36
|
+
| テスト | 結果 | 摩擦 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| test_no_rate_limit_headers_on_successful_responses | PASS | あり |
|
|
39
|
+
| test_no_per_path_rate_limits | PASS | あり |
|
|
40
|
+
| test_memory_not_cleaned_up_over_time | PASS | あり |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 発見した摩擦点
|
|
45
|
+
|
|
46
|
+
### FT20-F1: 通常レスポンスに X-RateLimit-* ヘッダーが付かない
|
|
47
|
+
|
|
48
|
+
**概要**: 429 応答には `Retry-After` ヘッダーが付くが、
|
|
49
|
+
通常のレスポンスには `X-RateLimit-Limit`、`X-RateLimit-Remaining`、`X-RateLimit-Reset`
|
|
50
|
+
が付かない。
|
|
51
|
+
|
|
52
|
+
**影響**: クライアントが自分のレート制限状況をリアルタイムで把握できない。
|
|
53
|
+
SDK やクライアントが事前にスロットリングする「adaptive throttling」が実装できない。
|
|
54
|
+
|
|
55
|
+
**期待する解決策**: 全レスポンスに `X-RateLimit-*` ヘッダーを付与するオプションを追加。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### FT20-F2: エンドポイントごとに異なるレート制限を設定できない
|
|
60
|
+
|
|
61
|
+
**概要**: `ThrottleMiddleware` は全エンドポイントで同一の `limit`/`window` のみ対応。
|
|
62
|
+
コスト大きなエンドポイント(`/api/expensive`: 10req/min)と軽いエンドポイント
|
|
63
|
+
(`/api/data`: 100req/min)で異なる制限を設定できない。
|
|
64
|
+
|
|
65
|
+
**影響**: 実運用では計算コストの重いエンドポイントを個別に絞りたいケースが多い。
|
|
66
|
+
|
|
67
|
+
**期待する解決策**: `path_limits: dict[str, int] | None = None` のようなパスごとの
|
|
68
|
+
レート制限設定パラメータを追加。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### FT20-F3: 古い IP エントリがメモリから削除されない
|
|
73
|
+
|
|
74
|
+
**概要**: `_counts` dict のエントリは、ウィンドウ経過後もメモリに残り続ける。
|
|
75
|
+
リクエスト時に古いエントリを上書きするだけで、削除はしない設計。
|
|
76
|
+
|
|
77
|
+
**影響**: 長時間稼働するサーバーでユニーク IP が多い場合、メモリが増加し続ける。
|
|
78
|
+
|
|
79
|
+
**期待する解決策**: ウィンドウ経過後の古いエントリを定期的に削除する
|
|
80
|
+
クリーンアップ機構(ローリングクリーンアップや LRU キャッシュ制限など)を追加。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## まとめ
|
|
85
|
+
|
|
86
|
+
`ThrottleMiddleware` の基本機能(IP ベースの固定ウィンドウレート制限、429 応答、
|
|
87
|
+
`Retry-After` ヘッダー、`exclude_paths`)は問題なく動作する。
|
|
88
|
+
実運用でよく必要になる以下の 3 点が摩擦として発見された:
|
|
89
|
+
|
|
90
|
+
1. **`X-RateLimit-*` ヘッダーの欠如** → クライアントが制限状況を把握できない
|
|
91
|
+
2. **パスごとのレート制限が不可** → 計算コストの差があるエンドポイントの制御が難しい
|
|
92
|
+
3. **古いエントリのメモリ残留** → 長時間稼働時のメモリリーク懸念
|
|
93
|
+
|
|
94
|
+
F1 (X-RateLimit headers) が最も実装インパクトが高く、今回の修正対象とする。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# FT21: DomainExceptionHandler 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `DomainExceptionHandlerProtocol` を使ったカスタム例外ハンドリングの実運用検証
|
|
5
|
+
**FT アプリ**: `/home/xi/docker/nene2-python-FT/ft21-domain-exception/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 目的
|
|
10
|
+
|
|
11
|
+
`nene2.middleware.ErrorHandlerMiddleware` の `domain_handlers` オプションを使い、
|
|
12
|
+
複数のカスタムドメイン例外を Problem Details に変換するパターンを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
ブログ記事 API(`/posts/{post_id}`)を作成し、以下のドメイン例外を実装:
|
|
19
|
+
|
|
20
|
+
- `PostNotFoundError` → 404 problem-details
|
|
21
|
+
- `PostAccessDeniedError` → 403 problem-details
|
|
22
|
+
- `PostAlreadyPublishedError` → 409 problem-details
|
|
23
|
+
|
|
24
|
+
各例外に対応する `DomainExceptionHandlerProtocol` 実装クラスを作成し、
|
|
25
|
+
`ErrorHandlerMiddleware(domain_handlers=[...])` に登録。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## テスト結果
|
|
30
|
+
|
|
31
|
+
### test_app.py(正常系・機能確認)
|
|
32
|
+
| テスト | 結果 |
|
|
33
|
+
|---|---|
|
|
34
|
+
| test_get_existing_post_returns_200 | PASS |
|
|
35
|
+
| test_get_nonexistent_post_returns_404_problem_details | PASS |
|
|
36
|
+
| test_get_other_users_post_returns_403 | PASS |
|
|
37
|
+
| test_publish_already_published_returns_409 | PASS |
|
|
38
|
+
| test_publish_unpublished_post_succeeds | PASS |
|
|
39
|
+
|
|
40
|
+
### test_friction.py(摩擦点確認)
|
|
41
|
+
| テスト | 結果 | 摩擦 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| test_no_base_class_for_domain_exception_handlers | PASS | あり |
|
|
44
|
+
| test_handler_registration_requires_list_at_middleware_init | PASS | あり(軽微) |
|
|
45
|
+
| test_unregistered_exception_falls_through_to_500 | PASS | あり(仕様だが摩擦) |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 発見した摩擦点
|
|
50
|
+
|
|
51
|
+
### FT21-F1: DomainExceptionHandler のボイラープレートが多い
|
|
52
|
+
|
|
53
|
+
**概要**: `DomainExceptionHandlerProtocol` を満たすクラスを毎回 `handles()`/`handle()` の
|
|
54
|
+
2 メソッドで実装する必要がある。多くのケースでパターンは同じ:
|
|
55
|
+
1. `isinstance()` チェック
|
|
56
|
+
2. `problem_details_response()` を呼ぶ
|
|
57
|
+
|
|
58
|
+
**影響**: 3 つのドメイン例外に対して 3 つのハンドラークラスを書かなければならず、
|
|
59
|
+
コード量が増える。
|
|
60
|
+
|
|
61
|
+
**期待する解決策**:
|
|
62
|
+
```python
|
|
63
|
+
# 現状(各例外ごとにクラスが必要)
|
|
64
|
+
class PostNotFoundHandler:
|
|
65
|
+
def handles(self, exc: Exception) -> bool:
|
|
66
|
+
return isinstance(exc, PostNotFoundError)
|
|
67
|
+
def handle(self, exc: Exception) -> Response:
|
|
68
|
+
return problem_details_response("post-not-found", "Not Found", 404)
|
|
69
|
+
|
|
70
|
+
# 期待: ファクトリで一行
|
|
71
|
+
handler = SimpleDomainHandler(PostNotFoundError, "post-not-found", "Post Not Found", 404)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### FT21-F2: ハンドラーをミドルウェア初期化時にしか登録できない
|
|
77
|
+
|
|
78
|
+
**概要**: `ErrorHandlerMiddleware` に `register_handler()` や `add_handler()` のような
|
|
79
|
+
動的登録メソッドがないため、ミドルウェア初期化時に全ハンドラーをリストで渡す必要がある。
|
|
80
|
+
|
|
81
|
+
**影響**: ドメインモジュールが分散している場合、一箇所に集めて渡す必要がある。
|
|
82
|
+
(これは設計上妥当とも言えるが、大規模プロジェクトでは不便)
|
|
83
|
+
|
|
84
|
+
**判断**: 初期化時一括登録は依存関係が明示的で好ましい設計のため、今回は修正しない。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### FT21-F3: 未登録ドメイン例外は 500 にフォールスルーする
|
|
89
|
+
|
|
90
|
+
**概要**: `domain_handlers` への登録を忘れると 500 応答になる。
|
|
91
|
+
ログを確認しないと原因が分からない。
|
|
92
|
+
|
|
93
|
+
**判断**: `debug=True` で例外メッセージが `detail` に含まれるため、
|
|
94
|
+
開発中は `ErrorHandlerMiddleware(debug=True)` を使えばデバッグ可能。
|
|
95
|
+
ドキュメントに明記する対応が適切。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## まとめ
|
|
100
|
+
|
|
101
|
+
`DomainExceptionHandlerProtocol` + `ErrorHandlerMiddleware` の組み合わせは機能するが、
|
|
102
|
+
各例外ごとにクラスを 1 つ書く必要があるボイラープレートが摩擦の主な原因。
|
|
103
|
+
|
|
104
|
+
`SimpleDomainHandler` ファクトリを追加することで DX が大幅に向上する(FT21-F1 → Issue化・修正対象)。
|