nene2-python 1.8.30__tar.gz → 1.8.31__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.30 → nene2_python-1.8.31}/CHANGELOG.md +11 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/PKG-INFO +2 -1
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-88.md +163 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-89.md +184 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-90.md +147 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-91.md +177 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-92.md +97 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-93.md +112 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-94.md +91 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-95.md +121 -0
- nene2_python-1.8.31/docs/field-trials/2026-05-field-trial-96.md +97 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/pyproject.toml +2 -1
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/http/__init__.py +2 -0
- nene2_python-1.8.31/src/nene2/http/etag.py +23 -0
- nene2_python-1.8.31/tests/nene2/http/test_etag.py +51 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/uv.lock +3 -1
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.env.example +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.gitignore +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/AGENTS.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/CLAUDE.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/Dockerfile +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/LICENSE +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/README.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/alembic/README +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/alembic/env.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/alembic.ini +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/compose.yaml +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/de/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/fr/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/reference/api.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/roadmap.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/todo/current.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/zh/index.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/package-lock.json +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/package.json +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/__main__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/app.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/mcp.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/schema.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.30 → nene2_python-1.8.31}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.30] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT87 フィールドトライアル — カスタムレスポンスヘッダーパターン検証と problem_details_response() 改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `problem_details_response()` に `headers: dict[str, str] | None = None` パラメーターを追加 (#369) (FT87)
|
|
14
|
+
— エラーレスポンスに `Retry-After`(429)、`WWW-Authenticate`(401)などのカスタムヘッダーを付与可能に
|
|
15
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-85.md`, `2026-05-field-trial-86.md`, `2026-05-field-trial-87.md` (FT85, FT86, FT87)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
8
19
|
## [1.8.29] — 2026-05-20
|
|
9
20
|
|
|
10
21
|
FT84 フィールドトライアル — 認証 Depends ユーティリティ検証と make_require_auth() 追加。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.31
|
|
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
|
|
@@ -28,6 +28,7 @@ Requires-Dist: httpx>=0.27
|
|
|
28
28
|
Requires-Dist: mcp>=1.0
|
|
29
29
|
Requires-Dist: pydantic-settings>=2.6
|
|
30
30
|
Requires-Dist: pydantic>=2.9
|
|
31
|
+
Requires-Dist: pyjwt>=2.12.0
|
|
31
32
|
Requires-Dist: python-multipart>=0.0.12
|
|
32
33
|
Requires-Dist: pyyaml>=6.0
|
|
33
34
|
Requires-Dist: sqlalchemy>=2.0.49
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# FT88: ドメインイベント — 同期イベントバスパターン検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: FastAPI/nene2 でのドメインイベント実装方法と摩擦点
|
|
5
|
+
**バージョン**: v1.8.30
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft88-domain-events/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
ドメインイベント(`OrderPlacedEvent`, `OrderCancelledEvent`)を発行し、
|
|
13
|
+
通知・監査ログなどのサイドエフェクトを分離するパターンを検証。
|
|
14
|
+
シンプルな同期 `EventBus` を自前実装し、nene2 のアーキテクチャと組み合わせた。
|
|
15
|
+
nene2 に EventBus の仕組みがないため、ガイダンスの欠如が摩擦ポイントとなる。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装パターン
|
|
20
|
+
|
|
21
|
+
### EventBus(自前実装)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
type EventHandler[T] = Callable[[T], None]
|
|
25
|
+
|
|
26
|
+
class EventBus:
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._handlers: dict[type, list[EventHandler[Any]]] = defaultdict(list)
|
|
29
|
+
self.published: list[Any] = [] # テスト用
|
|
30
|
+
|
|
31
|
+
def subscribe[T](self, event_type: type[T], handler: EventHandler[T]) -> None:
|
|
32
|
+
self._handlers[event_type].append(handler)
|
|
33
|
+
|
|
34
|
+
def publish(self, event: Any) -> None:
|
|
35
|
+
self.published.append(event)
|
|
36
|
+
for handler in self._handlers[type(event)]:
|
|
37
|
+
handler(event)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### UseCase でのイベント発行
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
@app.post("/orders", response_model=OrderResponse, status_code=201)
|
|
44
|
+
def place_order(body: PlaceOrderBody) -> JSONResponse:
|
|
45
|
+
order = Order(...)
|
|
46
|
+
_orders[order.order_id] = order
|
|
47
|
+
|
|
48
|
+
# ← イベント発行
|
|
49
|
+
event_bus.publish(OrderPlacedEvent(
|
|
50
|
+
order_id=order.order_id,
|
|
51
|
+
customer_id=order.customer_id,
|
|
52
|
+
total=order.total,
|
|
53
|
+
))
|
|
54
|
+
return JSONResponse({...}, status_code=201)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 発見した問題
|
|
60
|
+
|
|
61
|
+
### 問題1: EventBus が nene2 に含まれていない
|
|
62
|
+
|
|
63
|
+
nene2 はドメインイベントのアーキテクチャパターンを提供していない。
|
|
64
|
+
ユーザーが自前で `EventBus` を実装する必要があり、設計が人によって異なる。
|
|
65
|
+
「どのレイヤーでイベントを発行するか」「イベントをどう注入するか」のガイダンスがない。
|
|
66
|
+
|
|
67
|
+
### 問題2: 同期イベントバスでは例外がHTTPレスポンスに影響する
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# イベントハンドラーで例外が発生すると...
|
|
71
|
+
def _on_order_placed(event: OrderPlacedEvent) -> None:
|
|
72
|
+
send_email(event) # ← これが失敗すると
|
|
73
|
+
|
|
74
|
+
# → ErrorHandlerMiddleware が 500 を返す
|
|
75
|
+
# → 注文は作成されたのに 500 でクライアントに返る
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
「注文は DB に保存されたが通知送信に失敗」の場合、
|
|
79
|
+
同期バスでは HTTP レスポンスが 500 になってしまう。
|
|
80
|
+
`BackgroundTasks` と組み合わせるか、非同期バスを使う必要があるが、
|
|
81
|
+
nene2 ドキュメントにそのパターンが示されていない。
|
|
82
|
+
|
|
83
|
+
### 問題3: TestClient.delete() が json= をサポートしない
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# ❌ TypeError: TestClient.delete() got an unexpected keyword argument 'json'
|
|
87
|
+
client.delete("/orders/1", json={"reason": "..."})
|
|
88
|
+
|
|
89
|
+
# ✅ 回避策
|
|
90
|
+
client.request("DELETE", "/orders/1", json={"reason": "..."})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
DELETE + リクエストボディのパターンを httpx の TestClient がサポートしない。
|
|
94
|
+
この摩擦は nene2 に起因しないが、DELETE + ボディが必要な API 設計時にハマる。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## テスト結果(全14件パス)
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
test_place_order_returns_201 PASSED
|
|
102
|
+
test_place_order_publishes_event PASSED
|
|
103
|
+
test_place_order_triggers_notification PASSED
|
|
104
|
+
test_place_order_records_audit_log PASSED
|
|
105
|
+
test_cancel_order_returns_204 PASSED
|
|
106
|
+
test_cancel_order_publishes_event PASSED
|
|
107
|
+
test_cancel_order_404 PASSED
|
|
108
|
+
test_cancel_order_does_not_publish_event_on_404 PASSED
|
|
109
|
+
test_event_bus_subscribe_and_publish PASSED
|
|
110
|
+
test_event_bus_multiple_handlers PASSED
|
|
111
|
+
test_event_bus_unrelated_handler_not_called PASSED
|
|
112
|
+
test_event_bus_records_published_events PASSED
|
|
113
|
+
test_friction_event_bus_not_part_of_nene2 PASSED
|
|
114
|
+
test_friction_event_handler_exception_propagates PASSED
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 摩擦ポイント一覧
|
|
120
|
+
|
|
121
|
+
| ID | 内容 | 深刻度 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| F88-1 | EventBus が nene2 に含まれていない、ガイダンスもない | 中 |
|
|
124
|
+
| F88-2 | 同期イベントバスではハンドラー例外が HTTP 500 になる、BackgroundTasks との組み合わせガイドがない | 高 |
|
|
125
|
+
| F88-3 | TestClient.delete() が json= をサポートしない(httpx の制限) | 低 |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 使用感(主観評価)
|
|
130
|
+
|
|
131
|
+
### 直感性 ★★★☆☆
|
|
132
|
+
|
|
133
|
+
EventBus 自体の実装は簡単。Python 3.12 のジェネリクス構文 (`type EventHandler[T]`) で
|
|
134
|
+
型安全にハンドラーを登録できる。問題はアーキテクチャの指針がないこと。
|
|
135
|
+
|
|
136
|
+
### 実害の深刻さ ★★★★☆
|
|
137
|
+
|
|
138
|
+
F88-2 の「サイドエフェクト失敗で HTTP 500」は実際の運用で問題になる。
|
|
139
|
+
メール送信・Slack 通知などの外部連携は失敗しても注文作成は成功扱いにしたい。
|
|
140
|
+
`BackgroundTasks` との組み合わせパターンが必要。
|
|
141
|
+
|
|
142
|
+
### 修正のしやすさ ★★★★☆
|
|
143
|
+
|
|
144
|
+
- F88-1: ドキュメント(アーキテクチャガイド)を追加するだけ
|
|
145
|
+
- F88-2: `BackgroundTasks` とのパターン例を how-to に追加
|
|
146
|
+
- F88-3: コードコメント or ドキュメントで `client.request()` を使うよう明記
|
|
147
|
+
|
|
148
|
+
### 総合コメント
|
|
149
|
+
|
|
150
|
+
nene2 は「薄い HTTP 層」原則で UseCase とドメインを分離しているため、
|
|
151
|
+
EventBus を UseCase に注入するのは自然に実装できる。
|
|
152
|
+
ただし、「どこで EventBus を初期化して DI するか」が明確でなく、
|
|
153
|
+
グローバル変数 vs lifespan + app.state vs Depends() の選択に迷う。
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 推奨アクション
|
|
158
|
+
|
|
159
|
+
1. **docs**: how-to ガイドに「ドメインイベントパターン」を追加
|
|
160
|
+
- シンプルな同期 EventBus の実装例
|
|
161
|
+
- BackgroundTasks と組み合わせた非同期サイドエフェクトパターン
|
|
162
|
+
- EventBus の DI 方法(lifespan + app.state または module-level singleton)
|
|
163
|
+
2. **docs**: DELETE + リクエストボディのテスト方法 (`client.request("DELETE", ...)`) を明記
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# FT89: カスタムバリデーション — Pydantic バリデーターと nene2 ValidationException の統合
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: カスタム Pydantic バリデーター・クロスフィールド検証・nene2 ValidationException の統合
|
|
5
|
+
**バージョン**: v1.8.30
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft89-custom-validation/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
Pydantic v2 の `@field_validator` / `@model_validator` を使ったカスタムバリデーションと
|
|
13
|
+
nene2 の `ValidationException` の統合を検証。
|
|
14
|
+
`@model_validator` で `raise ValueError(...)` すると
|
|
15
|
+
nene2 の Problem Details で `field: "request"` になる摩擦が発見された。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 検証したパターン
|
|
20
|
+
|
|
21
|
+
### 1. クロスフィールドバリデーション(`@model_validator`)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
class DateRangeBody(BaseModel):
|
|
25
|
+
start_date: date
|
|
26
|
+
end_date: date
|
|
27
|
+
|
|
28
|
+
@model_validator(mode="after")
|
|
29
|
+
def end_date_after_start_date(self) -> "DateRangeBody":
|
|
30
|
+
if self.end_date <= self.start_date:
|
|
31
|
+
raise ValueError("end_date must be after start_date")
|
|
32
|
+
return self
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
✅ 動作するが、エラーレスポンスの `field` が `"request"` になる(後述)。
|
|
36
|
+
|
|
37
|
+
### 2. フィールドレベルバリデーション(`@field_validator`)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
class PasswordBody(BaseModel):
|
|
41
|
+
@field_validator("new_password")
|
|
42
|
+
@classmethod
|
|
43
|
+
def new_password_not_same_as_current(cls, value: str, info: FieldValidationInfo) -> str:
|
|
44
|
+
if info.data.get("current_password") == value:
|
|
45
|
+
raise ValueError("new_password must differ from current_password")
|
|
46
|
+
return value
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
✅ `field: "new_password"` が正しく設定される。
|
|
50
|
+
|
|
51
|
+
### 3. UseCase 層からの ValidationException
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def validate_business_rule(label: str) -> None:
|
|
55
|
+
errors: list[ValidationError] = []
|
|
56
|
+
if label.lower() in RESERVED_LABELS:
|
|
57
|
+
errors.append(ValidationError(
|
|
58
|
+
field="label",
|
|
59
|
+
message=f"'{label}' is reserved.",
|
|
60
|
+
code="reserved-label",
|
|
61
|
+
))
|
|
62
|
+
if errors:
|
|
63
|
+
raise ValidationException(errors)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
✅ 完全にコントロール可能。field / message / code を自由に設定できる。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 発見した問題
|
|
71
|
+
|
|
72
|
+
### 問題1: `@model_validator` のエラーが `field: "request"` になる
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# @model_validator で raise ValueError
|
|
76
|
+
# → Pydantic の loc: ("body",) or ()
|
|
77
|
+
# → nene2 の request_validation_error_handler で "body" を除くと空
|
|
78
|
+
# → field = "request" になる
|
|
79
|
+
|
|
80
|
+
# 実際のレスポンス:
|
|
81
|
+
{
|
|
82
|
+
"type": "https://nene2.dev/problems/validation-failed",
|
|
83
|
+
"errors": [
|
|
84
|
+
{
|
|
85
|
+
"field": "request", # ← "end_date" や "end_date_after_start_date" ではない
|
|
86
|
+
"message": "Value error, end_date must be after start_date",
|
|
87
|
+
"code": "value_error"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`@field_validator("end_date")` のエラーは `field: "end_date"` になるが、
|
|
94
|
+
`@model_validator` のエラーは `field: "request"` になる。
|
|
95
|
+
クライアント(TypeScript など)がフィールド単位でエラーを表示する場合、
|
|
96
|
+
どのフィールドにエラーが対応するかが不明になる。
|
|
97
|
+
|
|
98
|
+
### 回避策: UseCase 層で ValidationException を raise
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
@app.post("/date-ranges")
|
|
102
|
+
def create_date_range(body: DateRangeBody) -> JSONResponse:
|
|
103
|
+
# Pydantic の @model_validator をやめて UseCase 層で検証
|
|
104
|
+
errors: list[ValidationError] = []
|
|
105
|
+
if body.end_date <= body.start_date:
|
|
106
|
+
errors.append(ValidationError("end_date", "end_date must be after start_date", "invalid"))
|
|
107
|
+
if errors:
|
|
108
|
+
raise ValidationException(errors)
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`ValidationException` を使えば field 名を明示できる。
|
|
113
|
+
ただし Pydantic の `@model_validator` との二重定義になる。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## テスト結果(全16件パス)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
test_date_range_valid_returns_201 PASSED
|
|
121
|
+
test_date_range_end_before_start_returns_422 PASSED
|
|
122
|
+
test_date_range_same_dates_returns_422 PASSED
|
|
123
|
+
test_date_range_reserved_label_returns_422 PASSED
|
|
124
|
+
test_password_change_valid_returns_204 PASSED
|
|
125
|
+
test_password_change_same_as_current_returns_422 PASSED
|
|
126
|
+
test_password_change_mismatch_returns_422 PASSED
|
|
127
|
+
test_password_too_short_returns_422 PASSED
|
|
128
|
+
test_event_valid_returns_201 PASSED
|
|
129
|
+
test_event_empty_tag_returns_422 PASSED
|
|
130
|
+
test_event_duplicate_tags_returns_422 PASSED
|
|
131
|
+
test_event_too_many_tags_returns_422 PASSED
|
|
132
|
+
test_explicit_error_missing_title_returns_422 PASSED
|
|
133
|
+
test_validation_error_response_is_problem_details_format PASSED
|
|
134
|
+
test_friction_pydantic_error_field_path_in_problem_details PASSED
|
|
135
|
+
test_friction_multiple_validation_errors_all_returned PASSED
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 摩擦ポイント一覧
|
|
141
|
+
|
|
142
|
+
| ID | 内容 | 深刻度 |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| F89-1 | `@model_validator` のエラーが `field: "request"` になり具体的なフィールド名が失われる | 中 |
|
|
145
|
+
| F89-2 | クロスフィールドバリデーションを Pydantic で書くか UseCase で書くかの指針がない | 低 |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 使用感(主観評価)
|
|
150
|
+
|
|
151
|
+
### 直感性 ★★★★☆
|
|
152
|
+
|
|
153
|
+
Pydantic v2 の `@field_validator` は直感的で nene2 と自然に統合できる。
|
|
154
|
+
`@model_validator` も動くが、エラーの `field` 名が `"request"` になる不一致が惜しい。
|
|
155
|
+
|
|
156
|
+
### 実害の深刻さ ★★★☆☆
|
|
157
|
+
|
|
158
|
+
`field: "request"` 問題は、クライアントサイドがフィールド単位でバリデーション
|
|
159
|
+
エラーを表示しない場合(メッセージを表示するだけ)は問題にならない。
|
|
160
|
+
しかし TypeScript/React でフォームのフィールドにエラーを表示する場合は不便。
|
|
161
|
+
|
|
162
|
+
### 修正のしやすさ ★★★★☆
|
|
163
|
+
|
|
164
|
+
F89-1 はドキュメントだけで対応可能(`@model_validator` より `ValidationException` を推奨と明記)。
|
|
165
|
+
または `request_validation_error_handler` で `model_validator` の loc パターンを
|
|
166
|
+
検出して field 名を抽出するよう改善することも検討できる。
|
|
167
|
+
|
|
168
|
+
### 総合コメント
|
|
169
|
+
|
|
170
|
+
nene2 の `ValidationException` + `ValidationError` パターンは優れている。
|
|
171
|
+
フィールド名・メッセージ・コードが明示的で、クライアントに構造化エラーを返せる。
|
|
172
|
+
Pydantic の `@field_validator` との組み合わせも問題なし。
|
|
173
|
+
`@model_validator` の field パス問題は既知の FastAPI/Pydantic の挙動で、
|
|
174
|
+
ドキュメントで「クロスフィールドバリデーションは UseCase で行う」と案内するのが現実的。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 推奨アクション
|
|
179
|
+
|
|
180
|
+
1. **docs**: how-to ガイドに「カスタムバリデーションパターン」を追加
|
|
181
|
+
- `@field_validator` は nene2 Problem Details と自然に統合される
|
|
182
|
+
- クロスフィールドバリデーションは `ValidationException` で行うと `field` 名が正確
|
|
183
|
+
- `@model_validator` のエラーは `field: "request"` になることを明記
|
|
184
|
+
2. **docs**: Pydantic バリデーションと UseCase バリデーションの使い分けを説明
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# FT90: ファイルアップロード — multipart/form-data バリデーションパターン検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: FastAPI UploadFile + nene2 でのファイルアップロードバリデーション
|
|
5
|
+
**バージョン**: v1.8.30
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft90-file-upload/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
`UploadFile` を使ったファイルアップロード、コンテントタイプ・サイズバリデーション、
|
|
13
|
+
複数ファイル一括アップロード、`RequestSizeLimitMiddleware` との共存を検証。
|
|
14
|
+
`setup_middlewares()` のパラメーター名の非自明さと、
|
|
15
|
+
FastAPI がコンテントタイプを自動検証しないことが摩擦として発見された。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装パターン
|
|
20
|
+
|
|
21
|
+
### 単一ファイルアップロード + バリデーション
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
_ALLOWED_IMAGE_TYPES = frozenset({"image/jpeg", "image/png", "image/webp", "image/gif"})
|
|
25
|
+
_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
|
|
26
|
+
|
|
27
|
+
@app.post("/files", response_model=FileResponse, status_code=201)
|
|
28
|
+
async def upload_file(
|
|
29
|
+
file: UploadFile = File(description="ファイル"),
|
|
30
|
+
description: str = Form(default="", max_length=500),
|
|
31
|
+
) -> JSONResponse:
|
|
32
|
+
if file.content_type not in _ALLOWED_IMAGE_TYPES:
|
|
33
|
+
return problem_details_response(
|
|
34
|
+
"invalid-content-type", "Invalid Content Type", 415,
|
|
35
|
+
headers={"Accept": ", ".join(sorted(_ALLOWED_IMAGE_TYPES))},
|
|
36
|
+
)
|
|
37
|
+
content = await file.read()
|
|
38
|
+
if len(content) > _MAX_FILE_SIZE:
|
|
39
|
+
return problem_details_response("file-too-large", "File Too Large", 413, ...)
|
|
40
|
+
...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### nene2 の RequestSizeLimitMiddleware との共存
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# ✅ 正しいパラメーター名: max_request_bytes
|
|
47
|
+
setup_middlewares(app, max_request_bytes=10 * 1024 * 1024)
|
|
48
|
+
|
|
49
|
+
# ❌ 間違い(TypeError になる)
|
|
50
|
+
setup_middlewares(app, max_request_size=10 * 1024 * 1024)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 発見した問題
|
|
56
|
+
|
|
57
|
+
### 問題1: `setup_middlewares()` のパラメーター名が非自明
|
|
58
|
+
|
|
59
|
+
`max_request_bytes` という名前は正確だが、
|
|
60
|
+
`max_request_size` や `max_body_size` を試みるユーザーが多い。
|
|
61
|
+
`TypeError: setup_middlewares() got an unexpected keyword argument 'max_request_size'. Did you mean 'max_request_bytes'?`
|
|
62
|
+
というエラーが出るため気付けるが、IDE 補完がないと分かりにくい。
|
|
63
|
+
|
|
64
|
+
### 問題2: FastAPI は UploadFile のコンテントタイプを自動検証しない
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# コンテントタイプを偽って送信しても FastAPI は受け付ける
|
|
68
|
+
client.post("/files", files={"file": ("evil.exe", content, "image/jpeg")})
|
|
69
|
+
# → content_type="image/jpeg" として処理される(EXE バイナリでも)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
本番環境では `file.content_type` ヘッダーだけでなく、
|
|
73
|
+
ファイルの魔法バイト(magic bytes)を検証する必要があるが、
|
|
74
|
+
nene2 ドキュメントにそのパターンが示されていない。
|
|
75
|
+
|
|
76
|
+
### 問題3: `async def` ハンドラーが必要
|
|
77
|
+
|
|
78
|
+
`UploadFile.read()` は `await` が必要なため、ハンドラーを `async def` で定義する必要がある。
|
|
79
|
+
nene2 の run_in_threadpool パターン(FT76)との組み合わせで注意が必要。
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## テスト結果(全13件パス)
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
test_upload_valid_jpeg_returns_201 PASSED
|
|
87
|
+
test_upload_png_returns_201 PASSED
|
|
88
|
+
test_upload_with_description_form_field PASSED
|
|
89
|
+
test_upload_invalid_content_type_returns_415 PASSED
|
|
90
|
+
test_upload_too_large_returns_413 PASSED
|
|
91
|
+
test_upload_same_content_returns_same_id PASSED
|
|
92
|
+
test_get_uploaded_file_info PASSED
|
|
93
|
+
test_get_nonexistent_file_returns_404 PASSED
|
|
94
|
+
test_list_files_after_upload PASSED
|
|
95
|
+
test_batch_upload_multiple_files PASSED
|
|
96
|
+
test_batch_upload_invalid_type_returns_415 PASSED
|
|
97
|
+
test_friction_upload_file_missing_returns_422 PASSED
|
|
98
|
+
test_friction_content_type_not_validated_by_fastapi PASSED
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 摩擦ポイント一覧
|
|
104
|
+
|
|
105
|
+
| ID | 内容 | 深刻度 |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| F90-1 | `setup_middlewares()` の `max_request_bytes` パラメーター名が非自明(`max_request_size` と間違えやすい) | 低 |
|
|
108
|
+
| F90-2 | FastAPI は UploadFile のコンテントタイプを自動検証しない(魔法バイト検証のパターン未文書) | 中 |
|
|
109
|
+
| F90-3 | `UploadFile.read()` は `async def` が必要(sync ハンドラーとの混在に注意) | 低 |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 使用感(主観評価)
|
|
114
|
+
|
|
115
|
+
### 直感性 ★★★★☆
|
|
116
|
+
|
|
117
|
+
`UploadFile` + `File()` のパターンは直感的。
|
|
118
|
+
Form フィールドと同時に送信する場合も `data={"field": "value"}` で簡単。
|
|
119
|
+
`problem_details_response(headers=...)` で 415 の `Accept` ヘッダーも付けられた(FT87 の成果)。
|
|
120
|
+
|
|
121
|
+
### 実害の深刻さ ★★★☆☆
|
|
122
|
+
|
|
123
|
+
F90-2 はセキュリティ観点で中程度。
|
|
124
|
+
コンテントタイプを偽ったファイルアップロードは現実の攻撃手法。
|
|
125
|
+
nene2 ドキュメントに魔法バイト検証の例があれば防げる。
|
|
126
|
+
|
|
127
|
+
### 修正のしやすさ ★★★★★
|
|
128
|
+
|
|
129
|
+
F90-1: `max_request_bytes` のドキュメントに `max_request_size` との違いを明記するだけ。
|
|
130
|
+
F90-2: ドキュメントに魔法バイト検証の例を追加。
|
|
131
|
+
F90-3: ドキュメントに `async def` が必要な旨を明記。
|
|
132
|
+
|
|
133
|
+
### 総合コメント
|
|
134
|
+
|
|
135
|
+
nene2 の `RequestSizeLimitMiddleware` との共存は問題なし。
|
|
136
|
+
`problem_details_response()` を使ったエラー応答(415, 413)もきれいに動く。
|
|
137
|
+
FT87 で追加した `headers` パラメーターが早速役立った(415 に `Accept` ヘッダーを付与)。
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 推奨アクション
|
|
142
|
+
|
|
143
|
+
1. **docs**: how-to に「ファイルアップロード」ガイドを追加
|
|
144
|
+
- `UploadFile` + コンテントタイプ・サイズバリデーションのパターン
|
|
145
|
+
- 魔法バイト(magic bytes)検証の例
|
|
146
|
+
- `RequestSizeLimitMiddleware` との組み合わせ(`max_request_bytes` を明示)
|
|
147
|
+
2. **docs**: `max_request_bytes` パラメーターに `max_request_size` との混同を防ぐ注記
|