nene2-python 1.8.29__tar.gz → 1.8.30__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.29 → nene2_python-1.8.30}/CHANGELOG.md +12 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/PKG-INFO +1 -1
- nene2_python-1.8.30/docs/field-trials/2026-05-field-trial-85.md +207 -0
- nene2_python-1.8.30/docs/field-trials/2026-05-field-trial-86.md +171 -0
- nene2_python-1.8.30/docs/field-trials/2026-05-field-trial-87.md +194 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/pyproject.toml +1 -1
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/http/problem_details.py +5 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/http/test_problem_details.py +23 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/uv.lock +1 -1
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.env.example +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.gitignore +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/AGENTS.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/CLAUDE.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/Dockerfile +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/LICENSE +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/README.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/alembic/README +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/alembic/env.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/alembic.ini +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/compose.yaml +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/de/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/fr/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/reference/api.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/roadmap.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/todo/current.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/zh/index.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/package-lock.json +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/package.json +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/__main__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/app.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/mcp.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/schema.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.29 → nene2_python-1.8.30}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.29] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT84 フィールドトライアル — 認証 Depends ユーティリティ検証と make_require_auth() 追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `nene2.auth` に `make_require_auth(verifier)` Depends ファクトリーを追加 (#359) (FT84)
|
|
14
|
+
— `TokenVerifierProtocol` を FastAPI の `Depends` に接続するボイラープレートを解消
|
|
15
|
+
— 有効トークンで token 文字列を返し、未認証・無効トークンで 401 を raise
|
|
16
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-84.md` (FT84)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
8
20
|
## [1.8.28] — 2026-05-20
|
|
9
21
|
|
|
10
22
|
FT83 フィールドトライアル — Depends() DI パターン検証と PaginationResponse / PaginationDep 改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.30
|
|
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,207 @@
|
|
|
1
|
+
# FT85: OpenAPI スキーマ品質 — JSONResponse と response_model の摩擦
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: JSONResponse を返すと OpenAPI スキーマが Any になる問題と正しいパターン検証
|
|
5
|
+
**バージョン**: v1.8.29
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft85-openapi-schema/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
nene2 のハンドラーは `JSONResponse` を返すが、`response_model` を省略すると
|
|
13
|
+
OpenAPI スキーマに型情報が含まれず Swagger UI が使いにくくなる。
|
|
14
|
+
CLAUDE.md には「`response_model` で明示(`Any` 返却禁止)」と書かれているが、
|
|
15
|
+
`JSONResponse` との組み合わせ方が示されていない。
|
|
16
|
+
3つのパターン(response_model なし / あり / Pydantic 直返し)を検証した。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3パターンの比較
|
|
21
|
+
|
|
22
|
+
### パターン1: response_model なし(❌ 非推奨)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
@app.get("/articles/{article_id}")
|
|
26
|
+
def get_article(article_id: int) -> JSONResponse:
|
|
27
|
+
return JSONResponse({"article_id": 1, "title": "..."})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- OpenAPI スキーマ: `{}` または `{"title": "Response"}`(型情報なし)
|
|
31
|
+
- Swagger UI: レスポンス例なし、フィールド定義なし
|
|
32
|
+
- 型安全性: なし
|
|
33
|
+
|
|
34
|
+
### パターン2: response_model あり(✅ nene2 推奨)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
class ArticleResponse(BaseModel):
|
|
38
|
+
article_id: int = Field(description="記事 ID")
|
|
39
|
+
title: str = Field(max_length=200, description="記事タイトル")
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@app.get(
|
|
43
|
+
"/articles/{article_id}",
|
|
44
|
+
response_model=ArticleResponse,
|
|
45
|
+
responses={404: {"description": "Article not found"}},
|
|
46
|
+
summary="記事を取得する",
|
|
47
|
+
tags=["articles"],
|
|
48
|
+
)
|
|
49
|
+
def get_article(article_id: int) -> JSONResponse:
|
|
50
|
+
if article_id not in _articles:
|
|
51
|
+
return problem_details_response(...) # nene2 パターン維持
|
|
52
|
+
return JSONResponse({...})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- OpenAPI スキーマ: `ArticleResponse` の完全な型情報
|
|
56
|
+
- Swagger UI: フィールド説明・型・例が表示される
|
|
57
|
+
- nene2 の `problem_details_response()` と共存可能
|
|
58
|
+
|
|
59
|
+
### パターン3: Pydantic モデルを直接返す
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
@app.get("/articles/{article_id}", response_model=ArticleResponse)
|
|
63
|
+
def get_article(article_id: int) -> ArticleResponse:
|
|
64
|
+
if article_id not in _articles:
|
|
65
|
+
raise HTTPException(404, "Not found") # ← nene2 スタイルではない
|
|
66
|
+
return ArticleResponse(...)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- OpenAPI スキーマ: 最も完全
|
|
70
|
+
- 型安全性: 最高(戻り値が検証される)
|
|
71
|
+
- 問題: nene2 の `problem_details_response()` が使えない
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 発見した問題
|
|
76
|
+
|
|
77
|
+
### 問題1: JSONResponse + response_model の組み合わせが未文書
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# nene2 ユーザーはこれが正しい書き方だと知らない
|
|
81
|
+
@app.get("/articles/{id}", response_model=ArticleResponse)
|
|
82
|
+
def get_article(id: int) -> JSONResponse: # ← 戻り値型と response_model が一致しない
|
|
83
|
+
return JSONResponse({...})
|
|
84
|
+
|
|
85
|
+
# response_model は OpenAPI スキーマ生成のみに使われ、
|
|
86
|
+
# JSONResponse の内容は response_model でバリデーションされない
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`response_model` と `JSONResponse` の組み合わせは動作するが、
|
|
90
|
+
FastAPI は `JSONResponse` の内容を `response_model` で検証しない。
|
|
91
|
+
「`response_model` を指定すれば内容も検証される」と誤解するユーザーがいる。
|
|
92
|
+
|
|
93
|
+
### 問題2: Pydantic レスポンスモデルと Domain dataclass の二重定義
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class Article:
|
|
98
|
+
article_id: int
|
|
99
|
+
title: str
|
|
100
|
+
body: str
|
|
101
|
+
author: str
|
|
102
|
+
|
|
103
|
+
class ArticleResponse(BaseModel):
|
|
104
|
+
article_id: int = Field(description="記事 ID")
|
|
105
|
+
title: str = Field(max_length=200, description="記事タイトル")
|
|
106
|
+
body: str = Field(...)
|
|
107
|
+
author: str = Field(...)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Domain オブジェクトと API スキーマオブジェクトを両方定義する必要があり、
|
|
111
|
+
フィールドを変更すると両方を更新しなければならない。
|
|
112
|
+
|
|
113
|
+
### 問題3: problem_details_response() と Pydantic 直返しの非一貫性
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# パターン2 (JSONResponse): nene2 スタイルの 404
|
|
117
|
+
@app.get("/articles/{id}", response_model=ArticleResponse)
|
|
118
|
+
def get_article_v2(id: int) -> JSONResponse:
|
|
119
|
+
if id not in _articles:
|
|
120
|
+
return problem_details_response("not-found", ..., 404, ...) # ✅ RFC 9457
|
|
121
|
+
return JSONResponse({...})
|
|
122
|
+
|
|
123
|
+
# パターン3 (Pydantic 直返し): FastAPI スタイルの 404
|
|
124
|
+
@app.get("/articles/{id}", response_model=ArticleResponse)
|
|
125
|
+
def get_article_v3(id: int) -> ArticleResponse:
|
|
126
|
+
if id not in _articles:
|
|
127
|
+
raise HTTPException(404) # ❌ {"detail": "Not found"} になる(nene2 スタイルではない)
|
|
128
|
+
return ArticleResponse(...)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Pydantic モデルを直接返す場合は `problem_details_response()` を使えず、
|
|
132
|
+
`HTTPException` を raise する必要がある。
|
|
133
|
+
これは nene2 の Problem Details ポリシーと矛盾する。
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## テスト結果(全16件パス)
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
test_no_schema_create_returns_201 PASSED
|
|
141
|
+
test_no_schema_get_returns_200 PASSED
|
|
142
|
+
test_no_schema_openapi_get_has_no_response_schema PASSED # スキーマなし確認
|
|
143
|
+
test_with_schema_create_returns_201 PASSED
|
|
144
|
+
test_with_schema_get_returns_200 PASSED
|
|
145
|
+
test_with_schema_list_returns_200 PASSED
|
|
146
|
+
test_with_schema_openapi_has_response_schema PASSED # スキーマあり確認
|
|
147
|
+
test_with_schema_openapi_has_tags PASSED # tags 反映
|
|
148
|
+
test_with_schema_openapi_has_summary PASSED # summary 反映
|
|
149
|
+
test_with_schema_404_returns_problem_details PASSED # nene2 404 と共存
|
|
150
|
+
test_pydantic_return_get_returns_200 PASSED
|
|
151
|
+
test_pydantic_return_openapi_has_schema PASSED
|
|
152
|
+
test_friction_json_response_loses_schema PASSED # 摩擦記録
|
|
153
|
+
test_friction_response_model_does_not_validate PASSED # 摩擦記録
|
|
154
|
+
test_friction_problem_details_and_pydantic_conflict PASSED # 摩擦記録
|
|
155
|
+
test_friction_duplicate_type_definition PASSED # 摩擦記録
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 摩擦ポイント一覧
|
|
161
|
+
|
|
162
|
+
| ID | 内容 | 深刻度 |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| F85-1 | `JSONResponse` + `response_model` の正しい組み合わせ方がドキュメントに未記載 | 中 |
|
|
165
|
+
| F85-2 | Domain dataclass と Pydantic レスポンスモデルの二重定義が避けられない | 低 |
|
|
166
|
+
| F85-3 | Pydantic 直返しパターンでは `problem_details_response()` が使えない(HTTPException と非一貫) | 中 |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 使用感(主観評価)
|
|
171
|
+
|
|
172
|
+
### 直感性 ★★★☆☆
|
|
173
|
+
|
|
174
|
+
`JSONResponse` + `response_model` のパターンは直感的ではない。
|
|
175
|
+
「`JSONResponse` を返すと戻り値型が `JSONResponse` なのに `response_model=ArticleResponse` と
|
|
176
|
+
書く」という不整合に戸惑う。FastAPI のドキュメントを読めば理解できるが、
|
|
177
|
+
nene2 のコンテキストでの説明がない。
|
|
178
|
+
|
|
179
|
+
### 実害の深刻さ ★★★☆☆
|
|
180
|
+
|
|
181
|
+
`response_model` を省略すると Swagger UI に型情報が出ず、
|
|
182
|
+
API クライアント(TypeScript, Kotlin 等)の自動生成コードに型が付かない。
|
|
183
|
+
実際の運用では非常に不便で、フロントエンドチームから「なぜ型がないの?」と言われる。
|
|
184
|
+
|
|
185
|
+
### 修正のしやすさ ★★★★★
|
|
186
|
+
|
|
187
|
+
コード修正は不要。必要なのはドキュメントだけ:
|
|
188
|
+
- nene2 how-to: `JSONResponse` + `response_model` の正しいパターン例
|
|
189
|
+
- `response_model` がバリデーションではなく OpenAPI スキーマ生成のみに使われることの説明
|
|
190
|
+
- `problem_details_response()` との共存パターン
|
|
191
|
+
|
|
192
|
+
### 総合コメント
|
|
193
|
+
|
|
194
|
+
「`JSONResponse` を使いながら完全な OpenAPI スキーマを生成する」パターンは
|
|
195
|
+
FastAPI の機能で実現できるが、nene2 のドキュメントに記載がない。
|
|
196
|
+
CLAUDE.md には「`response_model` で明示」と書かれているが、
|
|
197
|
+
例コード(example app)が `response_model` を使っていない矛盾もある。
|
|
198
|
+
コード修正なしでドキュメントだけで対応できる摩擦点。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 推奨アクション
|
|
203
|
+
|
|
204
|
+
1. **docs**: how-to ガイドに「OpenAPI スキーマを整備する」記事を追加
|
|
205
|
+
— `response_model=PydanticModel` + `def handler() -> JSONResponse` パターン
|
|
206
|
+
— `problem_details_response()` との共存例
|
|
207
|
+
2. **refactor**: `example/` ハンドラーに `response_model` を追加して CLAUDE.md ポリシーに準拠させる
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# FT86: Lifespan イベント — startup/shutdown とリソース共有パターン
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: FastAPI lifespan context manager でのリソース初期化・クリーンアップと nene2 との共存
|
|
5
|
+
**バージョン**: v1.8.29
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft86-lifespan/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
FastAPI の `lifespan` パラメーターを使った起動時リソース初期化(DBプール、キャッシュ)と
|
|
13
|
+
`app.state` 経由でハンドラーへの注入パターンを検証。
|
|
14
|
+
nene2 の `setup_middlewares()` との共存は問題なし。
|
|
15
|
+
テスト時の `TestClient` の挙動と状態分離に摩擦あり。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装パターン
|
|
20
|
+
|
|
21
|
+
### パターン: lifespan + app.state + Depends
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from contextlib import asynccontextmanager
|
|
25
|
+
from collections.abc import AsyncGenerator
|
|
26
|
+
from fastapi import FastAPI, Depends, Request
|
|
27
|
+
from typing import Annotated
|
|
28
|
+
|
|
29
|
+
@asynccontextmanager
|
|
30
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
31
|
+
# startup
|
|
32
|
+
cache = InMemoryCache()
|
|
33
|
+
db_pool = DatabasePool()
|
|
34
|
+
db_pool.connect()
|
|
35
|
+
app.state.cache = cache
|
|
36
|
+
app.state.db_pool = db_pool
|
|
37
|
+
|
|
38
|
+
yield # アプリ実行
|
|
39
|
+
|
|
40
|
+
# shutdown
|
|
41
|
+
db_pool.disconnect()
|
|
42
|
+
cache.clear()
|
|
43
|
+
|
|
44
|
+
app = FastAPI(lifespan=lifespan)
|
|
45
|
+
setup_middlewares(app) # nene2 ミドルウェアと共存 ✅
|
|
46
|
+
|
|
47
|
+
# Depends でリソースを注入
|
|
48
|
+
def get_cache(request: Request) -> InMemoryCache:
|
|
49
|
+
return request.app.state.cache # type: ignore[return-value]
|
|
50
|
+
|
|
51
|
+
CacheDep = Annotated[InMemoryCache, Depends(get_cache)]
|
|
52
|
+
|
|
53
|
+
@app.get("/cache/{key}", response_model=CacheEntryResponse)
|
|
54
|
+
def get_entry(key: str, cache: CacheDep) -> JSONResponse:
|
|
55
|
+
return JSONResponse({"key": key, "value": cache.get(key)})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 発見した問題
|
|
61
|
+
|
|
62
|
+
### 問題1: TestClient の with ブロック必須が直感的でない
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# ❌ lifespan が実行されない — app.state.cache が未設定
|
|
66
|
+
client = TestClient(app)
|
|
67
|
+
client.get("/cache/greeting") # → AttributeError → 500
|
|
68
|
+
|
|
69
|
+
# ✅ 正しい使い方
|
|
70
|
+
with TestClient(app) as client:
|
|
71
|
+
client.get("/cache/greeting") # → 200
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`TestClient(app)` だけでは lifespan の startup が実行されない。
|
|
75
|
+
`with` ブロックで包む必要があるが、nene2 ドキュメントに記載がない。
|
|
76
|
+
|
|
77
|
+
### 問題2: テスト間の状態分離が保証されない
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# テスト A: with TestClient(app) を実行 → startup → app.state.cache が設定される
|
|
81
|
+
with TestClient(app) as client:
|
|
82
|
+
... # startup が実行され app.state に値がセットされる
|
|
83
|
+
|
|
84
|
+
# テスト B: with なしで実行 → 前のテストの app.state.cache が残っている
|
|
85
|
+
client = TestClient(app)
|
|
86
|
+
client.get("/status") # app.state が残っているため 200 になる(本来は 500 のはず)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
pytest が同一プロセスで `app` モジュールを共有するため、
|
|
90
|
+
前のテストで設定された `app.state` が次のテストに引き継がれる。
|
|
91
|
+
テスト順序によって結果が変わる不安定なテストスイートになりやすい。
|
|
92
|
+
|
|
93
|
+
### 問題3: app.state の型安全性がない
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# mypy では Any になる
|
|
97
|
+
cache = request.app.state.cache # type: ignore[return-value]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Starlette.State` は動的アトリビューとを持つ `__slots__` なしのオブジェクト。
|
|
101
|
+
mypy は型を推論できず、`type: ignore` コメントが必要になる。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## テスト結果(全11件パス)
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
test_lifespan_startup_initializes_resources PASSED
|
|
109
|
+
test_lifespan_cache_has_initial_value PASSED
|
|
110
|
+
test_lifespan_cache_missing_key_returns_null PASSED
|
|
111
|
+
test_lifespan_cache_set_and_get PASSED
|
|
112
|
+
test_lifespan_db_query_uses_pool PASSED
|
|
113
|
+
test_lifespan_db_query_count_increments PASSED
|
|
114
|
+
test_lifespan_cache_shared_across_requests PASSED
|
|
115
|
+
test_no_lifespan_status_returns_false PASSED
|
|
116
|
+
test_friction_testclient_requires_with_block PASSED
|
|
117
|
+
test_friction_app_state_no_type_safety PASSED
|
|
118
|
+
test_friction_lifespan_error_handling PASSED
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 摩擦ポイント一覧
|
|
124
|
+
|
|
125
|
+
| ID | 内容 | 深刻度 |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| F86-1 | `TestClient` を `with` ブロックで使わないと lifespan が実行されず 500 になる | 高 |
|
|
128
|
+
| F86-2 | テスト間で `app.state` が引き継がれてテスト順序依存になる | 中 |
|
|
129
|
+
| F86-3 | `app.state` へのアクセスが型安全でなく `type: ignore` が必要 | 低 |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 使用感(主観評価)
|
|
134
|
+
|
|
135
|
+
### 直感性 ★★☆☆☆
|
|
136
|
+
|
|
137
|
+
`lifespan` パラメーターと `yield` 境界は直感的だが、
|
|
138
|
+
TestClient の `with` ブロック必須は知らないとハマる。
|
|
139
|
+
「なぜ `client.get()` が 500 を返すのか」のデバッグに時間がかかる。
|
|
140
|
+
`AttributeError: 'State' object has no attribute 'cache'` が
|
|
141
|
+
nene2 の `ErrorHandlerMiddleware` に吸収されて 500 になるため
|
|
142
|
+
エラーメッセージが見えにくい(`APP_DEBUG=true` なら見える)。
|
|
143
|
+
|
|
144
|
+
### 実害の深刻さ ★★★★☆
|
|
145
|
+
|
|
146
|
+
テスト間の状態分離問題は CI で「時々落ちる」テストを生む。
|
|
147
|
+
テスト実行順序が変わった(pytest-randomly 導入、テスト追加)タイミングで
|
|
148
|
+
突然失敗するため、原因特定が難しい。
|
|
149
|
+
|
|
150
|
+
### 修正のしやすさ ★★★★☆
|
|
151
|
+
|
|
152
|
+
コード修正なし、ドキュメント追加のみで対応できる:
|
|
153
|
+
- how-to: `TestClient` を `with` ブロックで使う方法
|
|
154
|
+
- how-to: テスト間の状態分離のための `pytest.fixture` パターン
|
|
155
|
+
- how-to: `app.state` の型安全なアクセスパターン(typed wrapper)
|
|
156
|
+
|
|
157
|
+
### 総合コメント
|
|
158
|
+
|
|
159
|
+
lifespan + nene2 の組み合わせ自体は問題なく動く。
|
|
160
|
+
`setup_middlewares()` との共存も完全。
|
|
161
|
+
主な摩擦はテスト方法の周知不足であり、ドキュメントで解決できる。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 推奨アクション
|
|
166
|
+
|
|
167
|
+
1. **docs**: how-to ガイドに「lifespan でリソースを管理する」記事を追加
|
|
168
|
+
- `TestClient` は必ず `with` ブロックで使うことを明示
|
|
169
|
+
- テスト間の状態分離パターン(`pytest.fixture` + `with TestClient`)
|
|
170
|
+
- `app.state` の型安全なアクセスパターン(`get_or_raise` helper)
|
|
171
|
+
2. **docs**: nene2 example/ に lifespan パターンのサンプルを追加
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# FT87: カスタムレスポンスヘッダー — X-Total-Count / X-RateLimit パターン検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: JSONResponse へのカスタムヘッダー付与と nene2 ユーティリティとの統合
|
|
5
|
+
**バージョン**: v1.8.29
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft87-response-headers/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
カスタムレスポンスヘッダー(`X-Total-Count`, `X-RateLimit-Remaining`, etc.)の付与パターンを
|
|
13
|
+
4種類検証した。JSONResponse コンストラクタでの直接指定が最も簡単だが、
|
|
14
|
+
`problem_details_response()` に `headers` パラメーターがなく、
|
|
15
|
+
エラーレスポンスにヘッダーを付けられない摩擦が発見された。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 4パターンの比較
|
|
20
|
+
|
|
21
|
+
### パターン1: JSONResponse コンストラクタ(✅ 推奨)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
@app.get("/items", response_model=list[ItemResponse])
|
|
25
|
+
def list_items(pagination: PaginationDep) -> JSONResponse:
|
|
26
|
+
items, total = ...
|
|
27
|
+
return JSONResponse(
|
|
28
|
+
content={"items": items, "total": total},
|
|
29
|
+
headers={
|
|
30
|
+
"X-Total-Count": str(total),
|
|
31
|
+
"X-Limit": str(pagination.limit),
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
最もシンプル。nene2 スタイルと完全一致。
|
|
37
|
+
|
|
38
|
+
### パターン2: `response: Response` パラメーター(❌ JSONResponse では無視)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
@app.get("/items")
|
|
42
|
+
def list_items(response: Response) -> JSONResponse:
|
|
43
|
+
response.headers["X-Total"] = "10" # ← 無視される
|
|
44
|
+
return JSONResponse({"items": [...]})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
FastAPI の `response: Response` でのヘッダー設定は、
|
|
48
|
+
ハンドラーが `JSONResponse` を直接返す場合は無視される。
|
|
49
|
+
`JSONResponse` はそれ自体が完全なレスポンスオブジェクトのため。
|
|
50
|
+
|
|
51
|
+
### パターン3: ミドルウェアで一括付与
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
class XServerVersionMiddleware(BaseHTTPMiddleware):
|
|
55
|
+
async def dispatch(self, request, call_next):
|
|
56
|
+
response = await call_next(request)
|
|
57
|
+
response.headers["X-Server-Version"] = "1.0"
|
|
58
|
+
return response
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
全レスポンスに共通ヘッダーを付ける場合に有効。
|
|
62
|
+
nene2 の `SecurityHeadersMiddleware` と同様のパターン。
|
|
63
|
+
|
|
64
|
+
### パターン4: PaginationResponse + ヘッダー
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
pagination_response = PaginationResponse(
|
|
68
|
+
items=data, limit=pagination.limit, offset=pagination.offset, total=total
|
|
69
|
+
)
|
|
70
|
+
return JSONResponse(
|
|
71
|
+
content=pagination_response.to_dict(),
|
|
72
|
+
headers={"X-Total-Count": str(total), "X-Total-Pages": str(total_pages)},
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 発見した問題
|
|
79
|
+
|
|
80
|
+
### 問題1: `problem_details_response()` に `headers` パラメーターがない
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# やりたいこと: 429 レスポンスに Retry-After ヘッダーを付ける
|
|
84
|
+
return problem_details_response(
|
|
85
|
+
"rate-limited", "Rate Limited", 429, "Too many requests.",
|
|
86
|
+
headers={"Retry-After": "60", "X-RateLimit-Remaining": "0"}, # ← パラメーターが存在しない
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# 現状の回避策(冗長):
|
|
90
|
+
body = {
|
|
91
|
+
"type": "https://nene2.dev/problems/rate-limited",
|
|
92
|
+
"title": "Rate Limited",
|
|
93
|
+
"status": 429,
|
|
94
|
+
"detail": "Too many requests.",
|
|
95
|
+
}
|
|
96
|
+
return JSONResponse(content=body, status_code=429,
|
|
97
|
+
media_type="application/problem+json",
|
|
98
|
+
headers={"Retry-After": "60"})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
RFC 7807 / 9457 では `Retry-After`, `WWW-Authenticate`, `Location` などのヘッダーが
|
|
102
|
+
エラーレスポンスに必要なケースが多い。現在は `problem_details_response()` を捨てて
|
|
103
|
+
`JSONResponse` を直接構築する必要がある。
|
|
104
|
+
|
|
105
|
+
### 問題2: `response: Response` パラメーターが JSONResponse と非一貫
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# FastAPI ドキュメントには response: Response でヘッダーを設定する方法が書かれているが、
|
|
109
|
+
# JSONResponse を直接返す nene2 スタイルでは動作しない
|
|
110
|
+
@app.get("/items")
|
|
111
|
+
def list_items(response: Response) -> JSONResponse:
|
|
112
|
+
response.headers["X-Total"] = "10" # ← 無視される ← 摩擦
|
|
113
|
+
return JSONResponse({"items": []})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
FastAPI ドキュメントを読んだユーザーが試みるパターンだが、
|
|
117
|
+
nene2 の JSONResponse スタイルでは動作しない。
|
|
118
|
+
|
|
119
|
+
### 問題3: PaginationResponse に `page` / `total_pages` がない
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# ユーザーが期待するインターフェース(page-based)
|
|
123
|
+
PaginationResponse(items=data, total=100, page=2, per_page=20)
|
|
124
|
+
# → total_pages = 5 を自動計算してほしい
|
|
125
|
+
|
|
126
|
+
# 実際のインターフェース(offset-based)
|
|
127
|
+
PaginationResponse(items=data, limit=20, offset=20, total=100)
|
|
128
|
+
# → page や total_pages は自分で計算する必要がある
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`page` / `total_pages` を使いたい場合は手動計算が必要。
|
|
132
|
+
Frontend に `X-Total-Pages` ヘッダーを返したいケースで毎回計算コードが必要になる。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## テスト結果(全11件パス)
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
test_list_returns_x_total_count_header PASSED
|
|
140
|
+
test_list_returns_pagination_headers PASSED
|
|
141
|
+
test_create_returns_x_resource_id_header PASSED
|
|
142
|
+
test_get_item_returns_version_header PASSED
|
|
143
|
+
test_404_does_not_have_version_header PASSED
|
|
144
|
+
test_nene2_security_headers_preserved PASSED
|
|
145
|
+
test_middleware_adds_server_version_to_all_responses PASSED
|
|
146
|
+
test_pagination_headers_match_body PASSED
|
|
147
|
+
test_pagination_header_limit_value PASSED
|
|
148
|
+
test_friction_response_param_ignored_with_json_response PASSED
|
|
149
|
+
test_friction_problem_details_response_no_custom_headers PASSED
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 摩擦ポイント一覧
|
|
155
|
+
|
|
156
|
+
| ID | 内容 | 深刻度 |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| F87-1 | `problem_details_response()` に `headers` パラメーターがない — エラーレスポンスへのヘッダー付与が不便 | 高 |
|
|
159
|
+
| F87-2 | `response: Response` パラメーターのヘッダー設定が JSONResponse では無視される(未文書) | 中 |
|
|
160
|
+
| F87-3 | `PaginationResponse` に `page` / `total_pages` がなく手動計算が必要 | 低 |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 使用感(主観評価)
|
|
165
|
+
|
|
166
|
+
### 直感性 ★★★★☆
|
|
167
|
+
|
|
168
|
+
JSONResponse コンストラクタへの直接指定は直感的でシンプル。
|
|
169
|
+
問題は `problem_details_response()` でエラーヘッダーが付けられないこと。
|
|
170
|
+
|
|
171
|
+
### 実害の深刻さ ★★★★☆
|
|
172
|
+
|
|
173
|
+
429 Rate Limited + `Retry-After` ヘッダーは RFC 6585 で推奨されている組み合わせ。
|
|
174
|
+
現状では `problem_details_response()` が使えず、
|
|
175
|
+
RFC 9457 フォーマットと一致した手動 JSONResponse 構築が必要になる。
|
|
176
|
+
|
|
177
|
+
### 修正のしやすさ ★★★★★
|
|
178
|
+
|
|
179
|
+
`problem_details_response()` に `headers: dict[str, str] | None = None` を追加するだけ。
|
|
180
|
+
変更は最小で後方互換性あり。
|
|
181
|
+
|
|
182
|
+
### 総合コメント
|
|
183
|
+
|
|
184
|
+
F87-1 はコード修正で簡単に解決できる高影響の摩擦点。
|
|
185
|
+
`problem_details_response()` を nene2 のメインの エラー応答ファクトリとして機能させるなら、
|
|
186
|
+
`headers` パラメーターは必須機能と言える。
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 推奨アクション
|
|
191
|
+
|
|
192
|
+
1. **fix**: `problem_details_response()` に `headers: dict[str, str] | None = None` パラメーターを追加
|
|
193
|
+
2. **docs**: `response: Response` パラメーターが JSONResponse と非互換であることを how-to に明記
|
|
194
|
+
3. **docs**: カスタムレスポンスヘッダーの推奨パターンを how-to に追加
|
|
@@ -57,6 +57,7 @@ def problem_details_response(
|
|
|
57
57
|
extra: dict[str, Any] | None = None,
|
|
58
58
|
*,
|
|
59
59
|
base_url: str | None = None,
|
|
60
|
+
headers: dict[str, str] | None = None,
|
|
60
61
|
) -> JSONResponse:
|
|
61
62
|
"""Build an RFC 9457 Problem Details JSON response.
|
|
62
63
|
|
|
@@ -72,6 +73,9 @@ def problem_details_response(
|
|
|
72
73
|
Raises ``ValueError`` if any key shadows a reserved field
|
|
73
74
|
(``type``, ``title``, ``status``, ``detail``).
|
|
74
75
|
base_url: Override the base URL for this call only.
|
|
76
|
+
headers: Additional HTTP response headers. Useful for error-specific headers
|
|
77
|
+
such as ``Retry-After`` (429), ``WWW-Authenticate`` (401), or
|
|
78
|
+
``Location`` (3xx redirects in error flows).
|
|
75
79
|
|
|
76
80
|
``base_url`` resolution order:
|
|
77
81
|
1. Explicit ``base_url`` argument
|
|
@@ -96,4 +100,5 @@ def problem_details_response(
|
|
|
96
100
|
content=body,
|
|
97
101
|
status_code=status,
|
|
98
102
|
media_type="application/problem+json",
|
|
103
|
+
headers=headers,
|
|
99
104
|
)
|
|
@@ -98,3 +98,26 @@ def test_extra_with_type_reserved_raises_value_error() -> None:
|
|
|
98
98
|
def test_extra_with_status_reserved_raises_value_error() -> None:
|
|
99
99
|
with pytest.raises(ValueError, match="reserved Problem Details fields"):
|
|
100
100
|
problem_details_response("x", "X", 400, extra={"status": 500})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_problem_details_response_with_headers() -> None:
|
|
104
|
+
r = problem_details_response("rate-limited", "Rate Limited", 429, headers={"Retry-After": "60"})
|
|
105
|
+
assert r.status_code == 429
|
|
106
|
+
assert r.headers["retry-after"] == "60"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_problem_details_response_headers_with_multiple_entries() -> None:
|
|
110
|
+
r = problem_details_response(
|
|
111
|
+
"rate-limited",
|
|
112
|
+
"Rate Limited",
|
|
113
|
+
429,
|
|
114
|
+
headers={"Retry-After": "60", "X-RateLimit-Remaining": "0"},
|
|
115
|
+
)
|
|
116
|
+
assert r.headers["retry-after"] == "60"
|
|
117
|
+
assert r.headers["x-ratelimit-remaining"] == "0"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_problem_details_response_headers_none_by_default() -> None:
|
|
121
|
+
r = problem_details_response("not-found", "Not Found", 404)
|
|
122
|
+
assert "retry-after" not in r.headers
|
|
123
|
+
assert r.status_code == 404
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|