nene2-python 1.8.5__tar.gz → 1.8.7__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.5 → nene2_python-1.8.7}/CHANGELOG.md +23 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/PKG-INFO +1 -1
- nene2_python-1.8.7/docs/field-trials/2026-05-field-trial-41.md +126 -0
- nene2_python-1.8.7/docs/field-trials/2026-05-field-trial-42.md +137 -0
- nene2_python-1.8.7/docs/field-trials/2026-05-field-trial-43.md +112 -0
- nene2_python-1.8.7/docs/field-trials/2026-05-field-trial-44.md +110 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/run-tests.md +21 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/pyproject.toml +1 -1
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/http/__init__.py +6 -1
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/http/problem_details.py +21 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/__init__.py +2 -1
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/http/test_problem_details.py +17 -4
- {nene2_python-1.8.5 → nene2_python-1.8.7}/uv.lock +1 -1
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.env.example +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.gitignore +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/AGENTS.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/CLAUDE.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/Dockerfile +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/LICENSE +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/README.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/alembic/README +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/alembic/env.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/alembic.ini +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/compose.yaml +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/de/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/fr/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/reference/api.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/roadmap.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/todo/current.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/zh/index.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/package-lock.json +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/package.json +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/__main__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/app.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/mcp.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/schema.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.5 → nene2_python-1.8.7}/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.7] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT43〜FT44 フィールドトライアル — ThrottleMiddleware path_limits 確認・PaginationQueryParser バリデーション改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `nene2.middleware.request_validation_error_handler` を公開エクスポートに追加 — FastAPI の `RequestValidationError` を Problem Details 形式に変換するハンドラーを `from nene2.middleware import request_validation_error_handler` でアクセス可能に (FT44)
|
|
14
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-43.md`、`docs/field-trials/2026-05-field-trial-44.md`
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [1.8.6] — 2026-05-20
|
|
19
|
+
|
|
20
|
+
FT41〜FT42 フィールドトライアル — structlog テスト統合ドキュメント・configure_problem_details リセット関数。
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `nene2.http.reset_problem_details()` — `configure_problem_details()` で設定したグローバル状態をテスト間でリセットするヘルパー関数 (FT42)
|
|
24
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-41.md`、`docs/field-trials/2026-05-field-trial-42.md`
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `docs/how-to/run-tests.md` — `configure_for_testing()` + `caplog` による structlog ログキャプチャパターンを追記 (FT41)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
8
31
|
## [1.8.5] — 2026-05-20
|
|
9
32
|
|
|
10
33
|
FT37〜FT40 フィールドトライアル — RequestSizeLimitMiddleware パスごとサイズ制限とドキュメント改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.7
|
|
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,126 @@
|
|
|
1
|
+
# Field Trial 41: structlog テスト統合 — configure_for_testing() + caplog ログ相関
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.5 時点
|
|
5
|
+
**テーマ**: `configure_for_testing()` + pytest `caplog` でリクエスト ID をログで追跡するパターンの実運用確認
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
`nene2.log.configure_for_testing()` を `conftest.py` のモジュールレベルで呼び出し、
|
|
12
|
+
`RequestIdMiddleware` + `RequestLoggingMiddleware` を組み合わせたアプリのログを
|
|
13
|
+
pytest の `caplog` でキャプチャ・検証するパターンを実装した。
|
|
14
|
+
structlog と stdlib logging の橋渡し構造における制約も確認した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
`/home/xi/docker/nene2-python-FT/ft41-log-testing/` に以下を作成:
|
|
21
|
+
|
|
22
|
+
- **`conftest.py`** — モジュールレベルで `configure_for_testing()` を呼び出し
|
|
23
|
+
- **`app.py`** — `RequestIdMiddleware` / `RequestLoggingMiddleware` / `ErrorHandlerMiddleware` を積んだ FastAPI アプリ。`extra_context` パラメータで静的フィールドを全ログに付加
|
|
24
|
+
- **`test_app.py`** — 正常系・ヘッダー確認・caplog キャプチャ・ミドルウェアログ確認 (5 件)
|
|
25
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
26
|
+
|
|
27
|
+
**テスト結果**: 9 件全通過 ✅
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 摩擦点
|
|
32
|
+
|
|
33
|
+
### FP41-1: configure_for_testing() は conftest.py のモジュールレベルで呼ぶ必要がある
|
|
34
|
+
|
|
35
|
+
**分類**: 軽微な摩擦(設計通り・注意喚起)
|
|
36
|
+
|
|
37
|
+
`configure_for_testing()` は structlog のグローバル設定を変更するため、
|
|
38
|
+
テスト関数内で呼んでも機能するが、全テストに適用するには
|
|
39
|
+
`conftest.py` のモジュールレベルで呼ぶことが重要。
|
|
40
|
+
テスト関数内で呼ぶと、その関数のみに限定されず他のテストに副作用を与える可能性がある。
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# conftest.py — 正しいパターン
|
|
44
|
+
from nene2.log import configure_for_testing
|
|
45
|
+
configure_for_testing()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**判断**: ドキュメントに記載されたパターン通り。現行の `docs/how-to/run-tests.md` に
|
|
49
|
+
明示的に記載することで摩擦を減らせる。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### FP41-2: caplog.records の LogRecord に request_id フィールドが直接ない
|
|
54
|
+
|
|
55
|
+
**分類**: 設計上の制約(許容範囲)
|
|
56
|
+
|
|
57
|
+
`RequestIdMiddleware` が `structlog.contextvars.bind_contextvars(request_id=...)` で
|
|
58
|
+
バインドした値は、pytest の `caplog` が返す stdlib `LogRecord` に直接属性として
|
|
59
|
+
アクセスできない(`record.request_id` が存在しない)。
|
|
60
|
+
|
|
61
|
+
`ProcessorFormatter` を通すとメッセージ文字列に request_id が含まれるが、
|
|
62
|
+
構造化フィールドとして直接取り出すことはできない。
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# NG — LogRecord に直接属性はない
|
|
66
|
+
for record in caplog.records:
|
|
67
|
+
print(record.request_id) # AttributeError
|
|
68
|
+
|
|
69
|
+
# OK — メッセージ文字列に含まれる
|
|
70
|
+
assert "request-id-test" not in " ".join(r.message for r in caplog.records)
|
|
71
|
+
# (request_id の値は UUID であり、テスト側から事前に知ることはできない)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**判断**: structlog と stdlib logging の橋渡し構造による設計上の制約。
|
|
75
|
+
`caplog` でのログ検証はメッセージ文字列ベースで行うのが現実的なアプローチ。
|
|
76
|
+
`ProcessorFormatter` に対するテストを書きたい場合は structlog の `capture_logs()` を使う方法もある。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### FP41-3: caplog のキャプチャには configure_for_testing() が必須
|
|
81
|
+
|
|
82
|
+
**分類**: 注意喚起(ドキュメントで対応済み)
|
|
83
|
+
|
|
84
|
+
`configure_for_testing()` を呼ばない状態では、structlog のログは
|
|
85
|
+
pytest の `caplog` にキャプチャされない。
|
|
86
|
+
JSON レンダラーのまま stdout に出力されるため、
|
|
87
|
+
テストで structlog ログを検証するには必ず `configure_for_testing()` を呼ぶ必要がある。
|
|
88
|
+
|
|
89
|
+
**判断**: FT18 で実装した機能の使い方確認。`run-tests.md` に caplog との統合手順を追記する価値がある。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### FP41-4: structlog.contextvars でバインドした値はメッセージ文字列に含まれる
|
|
94
|
+
|
|
95
|
+
**分類**: 摩擦なし(設計の確認)
|
|
96
|
+
|
|
97
|
+
`structlog.contextvars.bind_contextvars(key="value")` でバインドした値は、
|
|
98
|
+
`configure_for_testing()` の設定下では `record.message` にキー=値形式で含まれる。
|
|
99
|
+
テストで構造化フィールドを検証する場合はメッセージ文字列の部分一致で確認できる。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
structlog.contextvars.bind_contextvars(request_id="test-123")
|
|
103
|
+
log.info("hello")
|
|
104
|
+
# caplog.records[0].message → "hello request_id=test-123" (または類似形式)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**判断**: structlog + caplog の統合パターンとして文字列検索が現実的。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## フレームワーク変更
|
|
112
|
+
|
|
113
|
+
なし(全て設計通りの挙動)
|
|
114
|
+
|
|
115
|
+
ドキュメント追記のみ検討:
|
|
116
|
+
- `docs/how-to/run-tests.md` に `configure_for_testing()` + caplog パターンを追記
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 関連
|
|
121
|
+
|
|
122
|
+
- `nene2.log.configure_for_testing` (FT18, v1.8.0)
|
|
123
|
+
- `nene2.middleware.RequestIdMiddleware`
|
|
124
|
+
- `nene2.middleware.RequestLoggingMiddleware`
|
|
125
|
+
- FT18 (configure_for_testing 実装, v1.8.0)
|
|
126
|
+
- FT30 (RequestLoggingMiddleware extra_context, v1.8.2)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Field Trial 42: get_request_id() Depends + configure_problem_details() 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.5 時点
|
|
5
|
+
**テーマ**: `get_request_id()` を FastAPI `Depends` で注入しレスポンスに含めるパターン、および `configure_problem_details()` のプロジェクト全体設定
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
`nene2.middleware.get_request_id()` を `Annotated[str, Depends(get_request_id)]` 構文で
|
|
12
|
+
ハンドラーに注入し、レスポンスボディに `request_id` を含めるパターンを実装した。
|
|
13
|
+
`configure_problem_details()` でプロジェクト全体の Problem Details base_url を設定し、
|
|
14
|
+
カスタム例外を `SimpleDomainHandler` でマッピングする構成も確認した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
`/home/xi/docker/nene2-python-FT/ft42-request-id-depends/` に以下を作成:
|
|
21
|
+
|
|
22
|
+
- **`app.py`** — `get_request_id()` Depends 注入・`configure_problem_details()` 設定・`SimpleDomainHandler` でカスタム例外マッピング
|
|
23
|
+
- **`test_app.py`** — 正常系・404・Problem Details base_url・request_id 相関 (9 件)
|
|
24
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
25
|
+
|
|
26
|
+
**テスト結果**: 13 件全通過 ✅
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 摩擦点
|
|
31
|
+
|
|
32
|
+
### FP42-1: RequestIdMiddleware なしで get_request_id() を呼ぶと空文字が返る
|
|
33
|
+
|
|
34
|
+
**分類**: 注意喚起(ドキュメントに記載済み)
|
|
35
|
+
|
|
36
|
+
`get_request_id()` は `contextvars` を参照する。
|
|
37
|
+
テスト関数から直接呼ぶ場合や `RequestIdMiddleware` を経由しない場合は `""` を返す。
|
|
38
|
+
`TestClient` 経由でリクエストを送れば正しく UUID が返る。
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# RequestIdMiddleware なしで直接呼ぶ → ""
|
|
42
|
+
from nene2.middleware import get_request_id
|
|
43
|
+
assert get_request_id() == ""
|
|
44
|
+
|
|
45
|
+
# TestClient 経由で呼ぶ → UUID v4
|
|
46
|
+
r = client.get("/debug/request-id")
|
|
47
|
+
assert len(r.json()["request_id"]) == 36
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**判断**: ドキュメントに記載済みの動作。TestClient 経由で使うことを徹底すれば問題ない。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### FP42-2: configure_problem_details() のグローバル状態がテスト間で共有される
|
|
55
|
+
|
|
56
|
+
**分類**: 軽微な摩擦(設計上の制約・テスト時の注意点)
|
|
57
|
+
|
|
58
|
+
`configure_problem_details()` はモジュールレベルのグローバル変数 `_configured_base_url` を変更する。
|
|
59
|
+
異なる `base_url` で複数の `create_app()` を呼ぶと最後の設定が残り、
|
|
60
|
+
テスト間で state が漏れる。
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
create_app(base_url="https://first.example.com/problems/")
|
|
64
|
+
# _configured_base_url == "https://first.example.com/problems/"
|
|
65
|
+
|
|
66
|
+
create_app(base_url="https://second.example.com/problems/")
|
|
67
|
+
# _configured_base_url == "https://second.example.com/problems/"
|
|
68
|
+
# ← first の設定は上書きされている
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**対処**: テスト間で隔離が必要な場合、`nene2.http.problem_details._configured_base_url = None`
|
|
72
|
+
で手動リセットするか、全テストで同一の base_url を使う。
|
|
73
|
+
運用環境では起動時に一度だけ呼ぶ設計なので実害はない。
|
|
74
|
+
|
|
75
|
+
**判断**: アプリ起動時一度だけ呼ぶ設計であり仕様通り。テスト向けに `reset_problem_details()` 関数を追加する価値があるかもしれない。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### FP42-3: SimpleDomainHandler のエラーレスポンスに request_id が自動付与されない
|
|
80
|
+
|
|
81
|
+
**分類**: 設計上の制約(許容範囲・パターン提示)
|
|
82
|
+
|
|
83
|
+
`ErrorHandlerMiddleware` + `SimpleDomainHandler` が生成する 404 レスポンスには
|
|
84
|
+
`request_id` フィールドが自動追加されない。
|
|
85
|
+
`X-Request-Id` ヘッダーは `RequestIdMiddleware` が付与するが、
|
|
86
|
+
レスポンスボディへの `request_id` 付与はアプリ側で明示的に行う必要がある。
|
|
87
|
+
|
|
88
|
+
エラーレスポンスに `request_id` を含めたい場合は、`problem_details_response()` を
|
|
89
|
+
直接呼ぶ exception handler を登録するか、`SimpleDomainHandler` を継承して
|
|
90
|
+
`request_id` を `extra` に追加するカスタム実装が必要。
|
|
91
|
+
|
|
92
|
+
**判断**: `ErrorHandlerMiddleware` はドメインレイヤーに依存しない設計のため、
|
|
93
|
+
`request_id` のような HTTP 横断概念を自動付与しないのは正しい。
|
|
94
|
+
クライアントが `X-Request-Id` ヘッダーを参照すれば相関できる。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### FP42-4: Annotated[str, Depends(get_request_id)] 構文は問題なく動作する
|
|
99
|
+
|
|
100
|
+
**分類**: 摩擦なし(良い設計の確認)
|
|
101
|
+
|
|
102
|
+
Python 3.12+ 推奨の `Annotated` 構文で `get_request_id()` を注入できる。
|
|
103
|
+
FastAPI の型推論も正しく動作し、`str` 型として扱われる。
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from typing import Annotated
|
|
107
|
+
from fastapi import Depends
|
|
108
|
+
from nene2.middleware import get_request_id
|
|
109
|
+
|
|
110
|
+
async def handler(
|
|
111
|
+
request_id: Annotated[str, Depends(get_request_id)],
|
|
112
|
+
) -> JSONResponse:
|
|
113
|
+
return JSONResponse({"request_id": request_id})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**判断**: FT25 で実装した `get_request_id()` は `Annotated` + `Depends` パターンと完全に互換。
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## フレームワーク変更
|
|
121
|
+
|
|
122
|
+
なし(全て設計通りの挙動)
|
|
123
|
+
|
|
124
|
+
以下のドキュメント追記を検討:
|
|
125
|
+
- `docs/how-to/` に `get_request_id()` Depends パターンの how-to ガイドを追加
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 関連
|
|
130
|
+
|
|
131
|
+
- `nene2.middleware.get_request_id` (FT25, v1.8.1)
|
|
132
|
+
- `nene2.middleware.RequestIdMiddleware`
|
|
133
|
+
- `nene2.http.configure_problem_details` (FT19, v1.8.0)
|
|
134
|
+
- `nene2.middleware.SimpleDomainHandler` (FT21, v1.8.0)
|
|
135
|
+
- FT19 (configure_problem_details 実装, v1.8.0)
|
|
136
|
+
- FT21 (SimpleDomainHandler 実装, v1.8.0)
|
|
137
|
+
- FT25 (get_request_id 実装, v1.8.1)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Field Trial 43: ThrottleMiddleware path_limits 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.6 時点
|
|
5
|
+
**テーマ**: `ThrottleMiddleware` の `path_limits` パラメータを使ってエンドポイントごとにレート制限を設定するパターンの実運用確認
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
グローバルレート制限(`limit=100`)と、特定パスへの厳しい制限(`path_limits={"/api/search": 10, "/api/upload": 5}`)を組み合わせた API を実装し、動作を確認した。
|
|
12
|
+
レートカウンターがパスごとに独立して管理されること、`X-RateLimit-*` ヘッダーが適切に付与されることを検証した。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実装内容
|
|
17
|
+
|
|
18
|
+
`/home/xi/docker/nene2-python-FT/ft43-throttle-path-limits/` に以下を作成:
|
|
19
|
+
|
|
20
|
+
- **`app.py`** — グローバル + パスごとのレート制限設定、`/health` 除外パスの構成
|
|
21
|
+
- **`test_app.py`** — 正常系・ヘッダー確認・path_limits 独立性・429 動作 (10 件)
|
|
22
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
23
|
+
|
|
24
|
+
**テスト結果**: 14 件全通過 ✅
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 摩擦点
|
|
29
|
+
|
|
30
|
+
### FP43-1: path_limits のカウンターはグローバルカウンターと完全に独立している
|
|
31
|
+
|
|
32
|
+
**分類**: 摩擦なし(良い設計の確認)
|
|
33
|
+
|
|
34
|
+
`path_limits` に指定したパスは `{client}:{path}` をキーとして使い、
|
|
35
|
+
グローバルカウンター (`{client}`) とは別々に管理される。
|
|
36
|
+
`/api/search` を使い切っても `/api/items` のカウンターには影響しない。
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# /api/search の制限 (3 req) を使い切っても
|
|
40
|
+
for _ in range(3):
|
|
41
|
+
client.get("/api/search")
|
|
42
|
+
# /api/items の制限は消費されていない
|
|
43
|
+
r = client.get("/api/items")
|
|
44
|
+
assert r.status_code == 200 # OK
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**判断**: FT28 で実装した設計通り。パス別独立カウンターは想定通りに動作する。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### FP43-2: X-RateLimit-Limit ヘッダーがパスごとの制限値を反映する
|
|
52
|
+
|
|
53
|
+
**分類**: 摩擦なし(良い設計の確認)
|
|
54
|
+
|
|
55
|
+
`/api/search` へのリクエストには `X-RateLimit-Limit: 3` が付与され、
|
|
56
|
+
`/api/items` へのリクエストには `X-RateLimit-Limit: 10` が付与される。
|
|
57
|
+
クライアントはヘッダーを見て自分のリミットがいくつかを判断できる。
|
|
58
|
+
|
|
59
|
+
**判断**: FT20 で実装したヘッダー付与が path_limits と正しく連携している。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### FP43-3: X-Forwarded-For がクライアントキーとして使われるためバイパスに注意
|
|
64
|
+
|
|
65
|
+
**分類**: 設計上の制約(ドキュメントに記載済み・運用上の注意点)
|
|
66
|
+
|
|
67
|
+
`X-Forwarded-For` ヘッダーがある場合、それをクライアント IP として使う設計のため、
|
|
68
|
+
異なる `X-Forwarded-For` を送ることで別のクライアントとして扱われ、レート制限をバイパスできる。
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# 通常の IP で制限を使い切ったあと
|
|
72
|
+
for _ in range(2):
|
|
73
|
+
client.get("/api/items")
|
|
74
|
+
r = client.get("/api/items")
|
|
75
|
+
assert r.status_code == 429
|
|
76
|
+
|
|
77
|
+
# 別の IP を騙って送ると 200 になる
|
|
78
|
+
r = client.get("/api/items", headers={"X-Forwarded-For": "10.0.0.1"})
|
|
79
|
+
assert r.status_code == 200 # バイパスできてしまう
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**判断**: ドキュメントの Warning セクションに記載されている既知の制限。
|
|
83
|
+
信頼できるリバースプロキシを前段に置くことで軽減できる。
|
|
84
|
+
テスト環境での動作確認として有用。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### FP43-4: path_limits の対象外パスはグローバル制限のみが適用される
|
|
89
|
+
|
|
90
|
+
**分類**: 摩擦なし(設計の確認)
|
|
91
|
+
|
|
92
|
+
`path_limits` に指定していないパス (`/api/items` など) はグローバルの `limit` が適用される。
|
|
93
|
+
複数のエンドポイントに異なる制限を設けつつ、デフォルトのグローバル制限を基本として使う設計が自然に実現できる。
|
|
94
|
+
|
|
95
|
+
**判断**: FT28 の設計通り。`path_limits` に指定のないパスは `{client}` をキーに使い、グローバルカウンターで管理される。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## フレームワーク変更
|
|
100
|
+
|
|
101
|
+
なし(全て設計通りの挙動)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 関連
|
|
106
|
+
|
|
107
|
+
- `nene2.middleware.ThrottleMiddleware` (FT20, v1.8.0)
|
|
108
|
+
- `ThrottleMiddleware.path_limits` (FT28, v1.8.1)
|
|
109
|
+
- `ThrottleMiddleware` ウィンドウクリーンアップ (FT27, v1.8.1)
|
|
110
|
+
- FT20 (ThrottleMiddleware ヘッダー実装, v1.8.0)
|
|
111
|
+
- FT27 (ThrottleMiddleware クリーンアップ修正, v1.8.1)
|
|
112
|
+
- FT28 (ThrottleMiddleware path_limits 実装, v1.8.1)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Field Trial 44: PaginationQueryParser + PaginationResponse 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.6 時点
|
|
5
|
+
**テーマ**: `PaginationQueryParser` を `Annotated[..., Depends()]` 構文で使い `PaginationResponse.to_dict()` でスロット付きデータクラスをシリアライズするパターンの実運用確認
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
`PaginationQueryParser` を FastAPI の `Depends()` として注入し、
|
|
12
|
+
`PaginationResponse` + `to_dict()` でスロット付き `dataclass(frozen=True, slots=True)` を
|
|
13
|
+
シリアライズするパターンを実装した。
|
|
14
|
+
`total` フィールドのあり/なし両パターン、および生の dict アイテムを含むケースも確認した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
`/home/xi/docker/nene2-python-FT/ft44-pagination/` に以下を作成:
|
|
21
|
+
|
|
22
|
+
- **`app.py`** — `PaginationQueryParser` Depends 注入、スロット付きデータクラス `Product`、3 つのエンドポイント(total あり/なし/生 dict)
|
|
23
|
+
- **`test_app.py`** — デフォルト・カスタム・オフセット・total・スロット付きシリアライズ・422 (10 件)
|
|
24
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
25
|
+
|
|
26
|
+
**テスト結果**: 14 件全通過 ✅
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 摩擦点
|
|
31
|
+
|
|
32
|
+
### FP44-1: OpenAPI スキーマにクエリパラメータが正しく文書化される
|
|
33
|
+
|
|
34
|
+
**分類**: 摩擦なし(良い設計の確認)
|
|
35
|
+
|
|
36
|
+
`Annotated[PaginationQueryParser, Depends()]` 構文を使うと、
|
|
37
|
+
`Query(ge=1, le=100, description="Items per page (1–100)")` の情報が
|
|
38
|
+
FastAPI の自動 OpenAPI スキーマ生成に反映される。
|
|
39
|
+
`/openapi.json` の `parameters` に `limit`・`offset` が説明付きで表示される。
|
|
40
|
+
|
|
41
|
+
**判断**: FT10 で実装した `Depends()` 対応の効果が確認できた。
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### FP44-2: PaginationResponse.to_dict() は元のアイテムを変更しない
|
|
46
|
+
|
|
47
|
+
**分類**: 摩擦なし(設計の確認)
|
|
48
|
+
|
|
49
|
+
`to_dict()` は `dataclasses.asdict()` で新しい dict を生成するため、
|
|
50
|
+
元の `dataclass` インスタンスは変更されない。immutable な `frozen=True` データクラスが
|
|
51
|
+
そのままリポジトリで保持できる。
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### FP44-3: items が空リストのときも to_dict() が正常動作する
|
|
56
|
+
|
|
57
|
+
**分類**: 摩擦なし(エッジケース確認)
|
|
58
|
+
|
|
59
|
+
`items=[]` のとき `to_dict()` は `{"items": [], "limit": 20, "offset": 100, "total": 50}`
|
|
60
|
+
を返す。ページを超えたオフセットでのリクエストが自然に処理される。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### FP44-4: Depends() 使用時のバリデーションエラーが Problem Details 形式にならない
|
|
65
|
+
|
|
66
|
+
**分類**: 摩擦あり(Issues #268 で対応)
|
|
67
|
+
|
|
68
|
+
`PaginationQueryParser` を `Depends()` として使うと、
|
|
69
|
+
`limit=0` や `limit=101` のバリデーションは FastAPI が実行し、
|
|
70
|
+
エラーは FastAPI のデフォルト Pydantic 形式(`{"detail": [...]}`)になる:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{"detail": [{"type": "greater_than_equal", ...}]}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
一方、nene2 の `ValidationException` は Problem Details 形式:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{"type": "...", "title": "Validation Failed", "status": 422, "errors": [...]}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
この不一致を解消するため、`nene2.middleware.request_validation_error_handler` を
|
|
83
|
+
FastAPI の exception handler として登録することで Problem Details 形式に統一できる:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from fastapi.exceptions import RequestValidationError
|
|
87
|
+
from nene2.middleware import request_validation_error_handler
|
|
88
|
+
|
|
89
|
+
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**対応**: `request_validation_error_handler` は `error_handler.py` に既存実装済みだったが、
|
|
93
|
+
`nene2.middleware` からエクスポートされていなかった (Issue #268)。
|
|
94
|
+
`nene2.middleware.__init__` に追加することで `from nene2.middleware import request_validation_error_handler` が可能になった。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## フレームワーク変更
|
|
99
|
+
|
|
100
|
+
- `nene2.middleware.__init__` に `request_validation_error_handler` を追加エクスポート (#268)
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 関連
|
|
105
|
+
|
|
106
|
+
- `nene2.http.PaginationQueryParser` (FT10, v1.3.0)
|
|
107
|
+
- `nene2.http.PaginationResponse` (FT10, v1.3.0)
|
|
108
|
+
- `nene2.middleware.request_validation_error_handler`
|
|
109
|
+
- FT10 (PaginationQueryParser Depends 対応, v1.3.0)
|
|
110
|
+
- Issue #268 (request_validation_error_handler エクスポート追加)
|
|
@@ -103,6 +103,27 @@ engine = create_engine(
|
|
|
103
103
|
|
|
104
104
|
`StaticPool` guarantees all logical connections share the same underlying SQLite connection, so tables created in one operation are visible to the next.
|
|
105
105
|
|
|
106
|
+
## Capturing structlog output with caplog
|
|
107
|
+
|
|
108
|
+
Call `configure_for_testing()` at module level in `conftest.py` to route structlog through stdlib logging so pytest's `caplog` fixture can capture it.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# conftest.py
|
|
112
|
+
from nene2.log import configure_for_testing
|
|
113
|
+
configure_for_testing()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then assert on message strings in tests:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
def test_handler_logs(caplog: pytest.LogCaptureFixture) -> None:
|
|
120
|
+
client = TestClient(create_app())
|
|
121
|
+
client.post("/api/echo", json={"message": "hello"})
|
|
122
|
+
assert any("processing echo" in r.message for r in caplog.records)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Note**: `caplog.records` returns stdlib `LogRecord` objects. Fields bound with `structlog.contextvars.bind_contextvars()` (such as `request_id`) are not directly accessible as `record.request_id` — they appear as part of the formatted message string instead.
|
|
126
|
+
|
|
106
127
|
## Coverage requirements
|
|
107
128
|
|
|
108
129
|
| Scope | Target |
|
|
@@ -8,7 +8,11 @@ from .health import (
|
|
|
8
8
|
HealthStatus,
|
|
9
9
|
)
|
|
10
10
|
from .pagination import PaginationQuery, PaginationQueryParser, PaginationResponse
|
|
11
|
-
from .problem_details import
|
|
11
|
+
from .problem_details import (
|
|
12
|
+
configure_problem_details,
|
|
13
|
+
problem_details_response,
|
|
14
|
+
reset_problem_details,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
__all__ = [
|
|
14
18
|
"AsyncCompositeHealthCheck",
|
|
@@ -21,4 +25,5 @@ __all__ = [
|
|
|
21
25
|
"PaginationResponse",
|
|
22
26
|
"configure_problem_details",
|
|
23
27
|
"problem_details_response",
|
|
28
|
+
"reset_problem_details",
|
|
24
29
|
]
|
|
@@ -27,6 +27,27 @@ def configure_problem_details(base_url: str) -> None:
|
|
|
27
27
|
_configured_base_url = base_url
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def reset_problem_details() -> None:
|
|
31
|
+
"""Reset the base_url configured by configure_problem_details().
|
|
32
|
+
|
|
33
|
+
Intended for use in tests only. Restores the default behaviour of
|
|
34
|
+
falling back to ``PROBLEM_DETAILS_BASE_URL``.
|
|
35
|
+
|
|
36
|
+
Example::
|
|
37
|
+
|
|
38
|
+
import pytest
|
|
39
|
+
from nene2.http import reset_problem_details
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(autouse=True)
|
|
43
|
+
def _reset():
|
|
44
|
+
yield
|
|
45
|
+
reset_problem_details()
|
|
46
|
+
"""
|
|
47
|
+
global _configured_base_url # noqa: PLW0603
|
|
48
|
+
_configured_base_url = None
|
|
49
|
+
|
|
50
|
+
|
|
30
51
|
def problem_details_response(
|
|
31
52
|
problem_type: str,
|
|
32
53
|
title: str,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""NENE2 middleware pipeline."""
|
|
2
2
|
|
|
3
3
|
from .domain_exception import DomainExceptionHandlerProtocol, SimpleDomainHandler
|
|
4
|
-
from .error_handler import ErrorHandlerMiddleware
|
|
4
|
+
from .error_handler import ErrorHandlerMiddleware, request_validation_error_handler
|
|
5
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
|
|
@@ -12,6 +12,7 @@ __all__ = [
|
|
|
12
12
|
"DomainExceptionHandlerProtocol",
|
|
13
13
|
"SimpleDomainHandler",
|
|
14
14
|
"ErrorHandlerMiddleware",
|
|
15
|
+
"request_validation_error_handler",
|
|
15
16
|
"RequestIdMiddleware",
|
|
16
17
|
"get_request_id",
|
|
17
18
|
"RequestLoggingMiddleware",
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from nene2.http import configure_problem_details, problem_details_response
|
|
5
|
+
from nene2.http import configure_problem_details, problem_details_response, reset_problem_details
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
@pytest.fixture(autouse=True)
|
|
10
9
|
def reset_configured_base_url() -> None:
|
|
11
10
|
"""Reset module-level configured base_url between tests."""
|
|
12
|
-
|
|
11
|
+
reset_problem_details()
|
|
13
12
|
yield
|
|
14
|
-
|
|
13
|
+
reset_problem_details()
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
def test_problem_details_response_uses_default_base_url() -> None:
|
|
@@ -61,3 +60,17 @@ def test_problem_details_response_includes_detail_when_provided() -> None:
|
|
|
61
60
|
def test_problem_details_response_includes_extra_fields() -> None:
|
|
62
61
|
r = problem_details_response("not-found", "Not Found", 404, extra={"resource_id": 42})
|
|
63
62
|
assert b'"resource_id":42' in r.body
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_reset_problem_details_restores_default_base_url() -> None:
|
|
66
|
+
configure_problem_details("https://custom.example.com/problems/")
|
|
67
|
+
reset_problem_details()
|
|
68
|
+
r = problem_details_response("not-found", "Not Found", 404)
|
|
69
|
+
assert b"nene2.dev/problems/not-found" in r.body
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_reset_problem_details_is_idempotent() -> None:
|
|
73
|
+
reset_problem_details()
|
|
74
|
+
reset_problem_details()
|
|
75
|
+
r = problem_details_response("not-found", "Not Found", 404)
|
|
76
|
+
assert b"nene2.dev/problems/not-found" in r.body
|
|
File without changes
|