nene2-python 1.8.32__tar.gz → 1.8.34__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.32 → nene2_python-1.8.34}/CHANGELOG.md +31 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/PKG-INFO +1 -1
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-100.md +62 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-101.md +59 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-102.md +34 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-103.md +47 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-104.md +48 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-105.md +43 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-106.md +39 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-107.md +46 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-108.md +46 -0
- nene2_python-1.8.34/docs/field-trials/2026-05-field-trial-109.md +53 -0
- nene2_python-1.8.34/docs/how-to/api-versioning.md +92 -0
- nene2_python-1.8.34/docs/how-to/custom-auth-middleware.md +141 -0
- nene2_python-1.8.34/docs/how-to/dependency-injection.md +136 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/lifespan-and-app-state.md +50 -1
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/response-patterns.md +30 -1
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/run-tests.md +21 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/streaming.md +13 -0
- nene2_python-1.8.34/docs/how-to/webhook.md +151 -0
- nene2_python-1.8.34/docs/todo/current.md +52 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/pyproject.toml +6 -1
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/app.py +1 -0
- nene2_python-1.8.34/src/nene2/cache/__init__.py +5 -0
- nene2_python-1.8.34/src/nene2/cache/ttl.py +57 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/sqlalchemy_executor.py +10 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/scripts/export_openapi.py +5 -0
- nene2_python-1.8.34/tests/conftest.py +20 -0
- nene2_python-1.8.34/tests/example/comment/test_comment_http.py +112 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/comment/test_comment_repository.py +11 -10
- nene2_python-1.8.34/tests/example/conftest.py +20 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/note/test_note_repository.py +9 -9
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/tag/test_tag_repository.py +9 -9
- nene2_python-1.8.34/tests/example/test_cors.py +75 -0
- nene2_python-1.8.34/tests/nene2/cache/test_ttl.py +76 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/database/test_transaction.py +19 -23
- nene2_python-1.8.34/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/uv.lock +1 -1
- nene2_python-1.8.32/docs/todo/current.md +0 -59
- nene2_python-1.8.32/tests/example/comment/test_comment_http.py +0 -97
- nene2_python-1.8.32/tests/example/conftest.py +0 -13
- nene2_python-1.8.32/tests/example/test_cors.py +0 -63
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.env.example +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.gitignore +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/AGENTS.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/CLAUDE.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/Dockerfile +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/LICENSE +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/README.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/alembic/README +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/alembic/env.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/alembic.ini +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/compose.yaml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/de/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/fr/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/reference/api.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/roadmap.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/zh/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/package-lock.json +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/package.json +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/__main__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/mcp.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/schema.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.32/tests/nene2/config → nene2_python-1.8.34/tests/nene2/cache}/__init__.py +0 -0
- {nene2_python-1.8.32/tests/nene2/database → nene2_python-1.8.34/tests/nene2/config}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.32/tests/nene2/http → nene2_python-1.8.34/tests/nene2/database}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.32/tests/nene2/log → nene2_python-1.8.34/tests/nene2/http}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.32/tests/nene2/mcp → nene2_python-1.8.34/tests/nene2/log}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.32/tests/nene2/middleware → nene2_python-1.8.34/tests/nene2/mcp}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.32/tests/nene2/security → nene2_python-1.8.34/tests/nene2/middleware}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.32/tests/nene2/use_case → nene2_python-1.8.34/tests/nene2/security}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.32/tests/nene2/validation → nene2_python-1.8.34/tests/nene2/use_case}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.32/tests/scripts → nene2_python-1.8.34/tests/nene2/validation}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.34}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,37 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.34] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
テストの ResourceWarning 解消と `SqlAlchemyQueryExecutor` / `SqlAlchemyTransactionManager` の `engine` プロパティ追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `SqlAlchemyQueryExecutor.engine` プロパティ — 下層の SQLAlchemy エンジンを公開(テストのティアダウンで `executor.engine.dispose()` に使用)(#428)
|
|
14
|
+
- `SqlAlchemyTransactionManager.engine` プロパティ — 同上 (#428)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- テスト実行時に Python 3.14 で 80+ 件出ていた `ResourceWarning: unclosed database` を解消 (#428)
|
|
18
|
+
- `create_app()` で `app.state.db_executor` を保存してテストからエンジンを dispose できるように変更
|
|
19
|
+
- `export_openapi.py` でエンジンを dispose するように修正
|
|
20
|
+
- テストフィクスチャを `yield` + `engine.dispose()` パターンに統一
|
|
21
|
+
- `tests/conftest.py` を新規作成し module-level app エンジンをセッション終了時に dispose
|
|
22
|
+
- `StaticPool` の最後の 1 件は `pyproject.toml` の `filterwarnings` で抑制
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [1.8.33] — 2026-05-20
|
|
27
|
+
|
|
28
|
+
FT100 フィールドトライアル — In-memory TTL レスポンスキャッシュパターン検証と nene2.cache モジュール追加。
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `nene2.cache` モジュールを新設 (#409) (FT100)
|
|
32
|
+
- `TtlCache[V]` — TTL 付きインメモリキャッシュ(ジェネリック型)
|
|
33
|
+
- `time.monotonic()` ベースの TTL で NTP 調整の影響を受けない
|
|
34
|
+
- `get()`, `set()`, `delete()`, `clear()`, `size()` API
|
|
35
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-100.md` (FT100)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
8
39
|
## [1.8.32] — 2026-05-20
|
|
9
40
|
|
|
10
41
|
FT99 フィールドトライアル — Webhook HMAC-SHA256 署名検証パターン検証と nene2.security モジュール追加。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.34
|
|
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,62 @@
|
|
|
1
|
+
# Field Trial 100: In-memory TTL レスポンスキャッシュ
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
重い処理の結果を TTL 付きインメモリキャッシュに格納し、重複リクエストにキャッシュから応答するパターンを nene2 上で実装する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft100-response-cache/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `TtlCache` データクラス(TTL 付き辞書)
|
|
12
|
+
- FastAPI lifespan でキャッシュを初期化・破棄
|
|
13
|
+
- `Depends(get_cache)` でハンドラーに注入
|
|
14
|
+
- キャッシュヒット/ミス判定、TTL 失効テスト
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 7 テスト通過。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: nene2 に TTL キャッシュユーティリティがない
|
|
23
|
+
|
|
24
|
+
**状況**: キャッシュは Web API の基本パターンだが、nene2 に `TtlCache` のようなユーティリティが存在しない。`TtlCache` を毎回自前実装する必要がある。
|
|
25
|
+
|
|
26
|
+
**影響**: 開発者がスレッドセーフでないキャッシュを実装してしまうリスク。また asyncio 環境での並行アクセスの考慮が漏れやすい(Python GIL により dict 操作自体はアトミックだが、get-then-set パターンは競合する)。
|
|
27
|
+
|
|
28
|
+
**期待する API**:
|
|
29
|
+
```python
|
|
30
|
+
from nene2.cache import TtlCache
|
|
31
|
+
|
|
32
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
33
|
+
cache.set("key", value)
|
|
34
|
+
value = cache.get("key") # None if expired
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### FP2: lifespan + グローバル変数パターンに型エラーが出る
|
|
38
|
+
|
|
39
|
+
**状況**: キャッシュを lifespan で初期化してグローバル変数に格納するパターンで、`async def lifespan(app: FastAPI)` の型注釈に `type: ignore[type-arg]` が必要。
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
_cache: TtlCache | None = None
|
|
43
|
+
|
|
44
|
+
@asynccontextmanager
|
|
45
|
+
async def lifespan(app: FastAPI): # type: ignore[type-arg] が必要
|
|
46
|
+
global _cache
|
|
47
|
+
_cache = TtlCache(ttl_seconds=60.0)
|
|
48
|
+
yield
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`app.state` を使うパターンの方がよりクリーンだが、`app.state` の型付けも `app.state.cache` のアクセスで `Any` になる。
|
|
52
|
+
|
|
53
|
+
### FP3: `app.state` でのキャッシュ管理がドキュメント化されていない
|
|
54
|
+
|
|
55
|
+
**状況**: `lifespan-and-app-state.md` では `app.state.db` の例はあるが、キャッシュを `app.state` に格納するパターン(`request.app.state.cache`)の説明がない。
|
|
56
|
+
|
|
57
|
+
**影響**: グローバル変数を使う開発者が多く、テスト時のリセットが困難になる。
|
|
58
|
+
|
|
59
|
+
## まとめ
|
|
60
|
+
|
|
61
|
+
キャッシュユーティリティ追加は中程度の価値あり(FP1)。FP2・FP3 はドキュメント摩擦。
|
|
62
|
+
今回は FP1 を `nene2.cache` モジュールとして実装し、Issue を起票する。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Field Trial 101: Query Parameter Filter/Sort パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`?status=published&sort=views&order=desc&tag=fastapi` のような複数フィルター + ソートのクエリパラメーターを型安全に扱うパターンを nene2 上で実装する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft101-query-filter/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `ArticleFilter` dataclass に `StrEnum` で列挙型フィルター
|
|
12
|
+
- `get_article_filter()` ファクトリ関数で `Depends()` に接続
|
|
13
|
+
- `PaginationQueryParser` と組み合わせてページネーション
|
|
14
|
+
- 11 テスト通過
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 11 テスト通過(修正後)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `PaginationQueryParser.as_depends()` が存在しない(ドキュメントとの乖離)
|
|
23
|
+
|
|
24
|
+
**状況**: 他のパーサー系クラスで `as_depends()` ファクトリパターンを使うケースがあり(FT83 など)、`PaginationQueryParser.as_depends()` を試みたが `AttributeError`。
|
|
25
|
+
|
|
26
|
+
正しい使い方は:
|
|
27
|
+
```python
|
|
28
|
+
# ✅ Annotated スタイル
|
|
29
|
+
pagination: Annotated[PaginationQueryParser, Depends()]
|
|
30
|
+
|
|
31
|
+
# ❌ as_depends() は存在しない
|
|
32
|
+
pagination: PaginationQueryParser = Depends(PaginationQueryParser.as_depends())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**影響**: `Depends()` の使い方が複数あるため混乱しやすい。how-to ガイドに明記が必要。
|
|
36
|
+
|
|
37
|
+
### FP2: `Depends()` スタイルとデフォルト値の混在でシンタックスエラー
|
|
38
|
+
|
|
39
|
+
**状況**: 複数の `Depends()` パラメーターを持つハンドラーで、`= Depends(...)` スタイルと `Annotated[T, Depends()]` スタイルを混在させると `SyntaxError: parameter without a default follows parameter with a default` が出る。
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# ❌ SyntaxError
|
|
43
|
+
def list_articles(
|
|
44
|
+
filter_: ArticleFilter = Depends(get_article_filter), # デフォルト値あり
|
|
45
|
+
pagination: Annotated[PaginationQueryParser, Depends()], # デフォルト値なし
|
|
46
|
+
) -> JSONResponse: ...
|
|
47
|
+
|
|
48
|
+
# ✅ Annotated スタイルに統一
|
|
49
|
+
def list_articles(
|
|
50
|
+
filter_: Annotated[ArticleFilter, Depends(get_article_filter)],
|
|
51
|
+
pagination: Annotated[PaginationQueryParser, Depends()],
|
|
52
|
+
) -> JSONResponse: ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**影響**: エラーメッセージが直感的でなく、原因がわかりにくい。
|
|
56
|
+
|
|
57
|
+
## まとめ
|
|
58
|
+
|
|
59
|
+
摩擦の原因はドキュメント不足。FP1・FP2 を how-to ガイドに追記する Issue を起票する。コード修正は不要。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Field Trial 102: response_model と PaginationResponse の型整合性
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`response_model` と `PaginationResponse` / `JSONResponse` の組み合わせパターンを実際に動かして、OpenAPI スキーマへの影響と型整合性を検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft102-response-model/` に 4 パターンを実装:
|
|
10
|
+
|
|
11
|
+
1. **v1**: `response_model` なし + `JSONResponse` + `PaginationResponse`
|
|
12
|
+
2. **v2**: `response_model=ProductListResponse` + Pydantic モデル直返し
|
|
13
|
+
3. **v3**: `response_model=ProductListResponse` + `dict` 返却(FastAPI が変換)
|
|
14
|
+
4. **v4**: `response_model=ProductListResponse` + `JSONResponse`(検証はされない)
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 9 テスト通過。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
摩擦なし。
|
|
23
|
+
|
|
24
|
+
`response-patterns.md` How-to(PR #403 で追加済み)がこのパターンを説明しており、スムーズに実装できた。
|
|
25
|
+
|
|
26
|
+
- パターン1(JSONResponse + PaginationResponse)は OpenAPI スキーマなしで OK
|
|
27
|
+
- パターン2(Pydantic 直返し + response_model)は OpenAPI スキーマあり
|
|
28
|
+
- パターン4(JSONResponse + response_model)は response_model があってもバリデーション非実施
|
|
29
|
+
|
|
30
|
+
前回の摩擦(`problem_details_response()` と Pydantic 直返しの非一貫性)は PR #403 で文書化済み。
|
|
31
|
+
|
|
32
|
+
## まとめ
|
|
33
|
+
|
|
34
|
+
ドキュメント整備が功を奏し、摩擦ゼロで実装完了。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Field Trial 103: カスタムミドルウェアで認証情報をリクエストスコープに格納
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
JWT ミドルウェアで検証後の認証情報(`AuthUser`)を `request.state` に格納し、ハンドラーで `Depends()` を通じて取得するパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft103-request-state-auth/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `JwtAuthMiddleware` — JWT を検証して `request.state.user` に `AuthUser` を格納
|
|
12
|
+
- `get_current_user()` — `request.state.user` から `AuthUser` を取得する Depends ファクトリ
|
|
13
|
+
- `require_admin()` — `admin` ロールチェック依存
|
|
14
|
+
- `EXCLUDE_PATHS` で `/health` をスキップ
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 7 テスト通過。`InsecureKeyLengthWarning` はテスト用の短いシークレットによるもの(想定内)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `request.state.user` アクセスに `type: ignore[attr-defined]` が必要
|
|
23
|
+
|
|
24
|
+
**状況**: `request.state` は `starlette.datastructures.State` で動的属性を持つ。ミドルウェアで `request.state.user = AuthUser(...)` と設定しても、Depends ファクトリで `request.state.user` を参照する際に mypy が `attr-defined` エラーを出す。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
def get_current_user(request: Request) -> AuthUser:
|
|
28
|
+
user: AuthUser = request.state.user # type: ignore[attr-defined] # reason: middleware で確実に設定済み
|
|
29
|
+
return user
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
これは `app.state` でも同様(FT100 で確認済み)。
|
|
33
|
+
|
|
34
|
+
**影響**: 低。`type: ignore` + `reason` コメントで対処可能。
|
|
35
|
+
|
|
36
|
+
### FP2: `BearerTokenMiddleware` のトークン文字列が `request.state` に格納されない
|
|
37
|
+
|
|
38
|
+
**状況**: nene2 の `BearerTokenMiddleware` は JWT/API キーの検証後にトークン文字列を返す(`make_require_auth()` Depends 経由)が、検証済みのペイロードやユーザー情報を `request.state` に自動格納する機能がない。
|
|
39
|
+
|
|
40
|
+
カスタム JWT ミドルウェアで対応したが、`BearerTokenMiddleware` + `request.state` の統合パターンがない。
|
|
41
|
+
|
|
42
|
+
**影響**: 中。`BearerTokenMiddleware` を使いたいが `request.state` にユーザー情報を格納したい場合、自前ミドルウェアを書き直す必要がある。
|
|
43
|
+
|
|
44
|
+
## まとめ
|
|
45
|
+
|
|
46
|
+
FP1 は既知の Starlette 制約(low)。FP2 は nene2 の認証統合に関する中程度の摩擦。
|
|
47
|
+
docs として「request.state を使った認証情報伝播パターン」を how-to に追加する。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Field Trial 104: AsyncIterator を返す UseCase + StreamingResponse
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
UseCase が `AsyncIterator` を返し、FastAPI の `StreamingResponse` でストリーミングする本格的なパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft104-streaming-usecase/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `StreamLogsUseCase` — `AsyncIterator[LogEntry]` を返す UseCase
|
|
12
|
+
- `ExportCsvUseCase` — CSV 行を `AsyncIterator[str]` で返す UseCase
|
|
13
|
+
- NDJSON / SSE / CSV の 3 形式にストリーミング変換
|
|
14
|
+
- 6 テスト通過
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 6 テスト通過(修正後)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `TestClient.stream()` 内で `r.text` が使えない
|
|
23
|
+
|
|
24
|
+
**状況**: `with client.stream("GET", "/export/users.csv") as r:` コンテキスト内で `r.text` にアクセスすると `httpx.ResponseNotRead` が発生する。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# ❌ ResponseNotRead
|
|
28
|
+
with client.stream("GET", "/export/csv") as r:
|
|
29
|
+
content = r.text # エラー!
|
|
30
|
+
|
|
31
|
+
# ✅ iter_text() でチャンク収集
|
|
32
|
+
with client.stream("GET", "/export/csv") as r:
|
|
33
|
+
content = "".join(r.iter_text())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**影響**: ストリーミングレスポンスのテストで直感に反するエラーが出る。`streaming.md` How-to に `iter_text()` パターンを追記する必要がある。
|
|
37
|
+
|
|
38
|
+
### FP2: `AsyncUseCaseProtocol` は `AsyncIterator` を返す UseCase に対応していない
|
|
39
|
+
|
|
40
|
+
**状況**: `AsyncUseCaseProtocol` は `async def execute(self, input_: I) -> O` を定義しており、`O = AsyncIterator[T]` として使うことは技術的には可能だが、`O` 型が `AsyncIterator` であることを表現するのが難しい。
|
|
41
|
+
|
|
42
|
+
現在の実装では `AsyncUseCaseProtocol` を使わず、独立した UseCase クラスとして実装した。
|
|
43
|
+
|
|
44
|
+
**影響**: 低。ストリーミング UseCase は専用の Protocol を定義するか、型注釈なしで書くのが現実的。
|
|
45
|
+
|
|
46
|
+
## まとめ
|
|
47
|
+
|
|
48
|
+
FP1 をドキュメント修正で対応。FP2 は将来の検討事項として記録。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Field Trial 105: マルチテナント DB 接続
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`X-Tenant-Id` ヘッダーでテナントを切り替え、リクエストごとに異なる SQLite DB に接続するマルチテナントパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft105-multitenant/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `TENANT_DB_URLS` マップでテナント DB URL を管理
|
|
12
|
+
- `get_tenant_session()` Depends でテナント特定・セッション生成
|
|
13
|
+
- テナント分離テスト(A と B は独立した DB)
|
|
14
|
+
|
|
15
|
+
## テスト結果
|
|
16
|
+
|
|
17
|
+
全 6 テスト通過。
|
|
18
|
+
|
|
19
|
+
## Friction Points
|
|
20
|
+
|
|
21
|
+
### FP1: `create_engine()` をリクエストごとに呼ぶのはパフォーマンス上の問題
|
|
22
|
+
|
|
23
|
+
**状況**: `get_tenant_session()` がリクエストごとに `create_engine()` を呼ぶ。SQLAlchemy では `create_engine()` はアプリ起動時に一度だけ呼ぶべきで、コネクションプールを使い回す設計が推奨される。
|
|
24
|
+
|
|
25
|
+
nene2 にテナントごとのエンジンキャッシュパターンがない。`TtlCache[Session]` で代替できるが、セッションはリクエストスコープで閉じる必要があるため、エンジンをキャッシュすべき。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# 推奨パターン
|
|
29
|
+
_engine_cache: dict[str, Engine] = {}
|
|
30
|
+
|
|
31
|
+
def get_engine(tenant_id: str) -> Engine:
|
|
32
|
+
if tenant_id not in _engine_cache:
|
|
33
|
+
_engine_cache[tenant_id] = create_engine(TENANT_DB_URLS[tenant_id], ...)
|
|
34
|
+
return _engine_cache[tenant_id]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### FP2: `SqlAlchemyQueryExecutor` はシングルエンジン想定
|
|
38
|
+
|
|
39
|
+
**状況**: `nene2.database.SqlAlchemyQueryExecutor` はアプリ起動時に単一エンジンで初期化することを前提としている。マルチテナントでリクエストごとに異なるエンジンを使う場合、`SqlAlchemyQueryExecutor` を使わず直接 `Session` を操作する必要がある。
|
|
40
|
+
|
|
41
|
+
## まとめ
|
|
42
|
+
|
|
43
|
+
FP1 は `TtlCache` を使ったエンジンキャッシュパターンとして docs に追記する。FP2 はアーキテクチャ設計上の制約として記録。コード修正は不要。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Field Trial 106: Idempotency Key パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`Idempotency-Key` ヘッダーを使って POST リクエストを冪等にするパターンを検証する。
|
|
6
|
+
`nene2.cache.TtlCache` の実用例として組み込む。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft106-idempotency/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `TtlCache[dict[str, Any]]` を `app.state.idempotency_cache` として lifespan で初期化
|
|
13
|
+
- `POST /orders` で `Idempotency-Key` ヘッダーをチェック
|
|
14
|
+
- 既存キャッシュがあれば `X-Idempotency-Replayed: true` ヘッダーと共にキャッシュから返す
|
|
15
|
+
- キャッシュなければ UseCase を実行してキャッシュに保存
|
|
16
|
+
- 7 テスト通過
|
|
17
|
+
|
|
18
|
+
## テスト結果
|
|
19
|
+
|
|
20
|
+
全 7 テスト通過。
|
|
21
|
+
|
|
22
|
+
## Friction Points
|
|
23
|
+
|
|
24
|
+
### FP1: Idempotency Key 同一キー + 異なるボディの扱いが未定義
|
|
25
|
+
|
|
26
|
+
**状況**: Stripe などの実装では、同じ `Idempotency-Key` で異なるリクエストボディを送ると `422 Unprocessable Entity` を返す。現在の実装ではキャッシュされた最初のレスポンスを無条件に返すため、ボディが変わっても同じレスポンスが返る。
|
|
27
|
+
|
|
28
|
+
**影響**: 中。金融系 API では重要だが、一般的な API では省略可。
|
|
29
|
+
|
|
30
|
+
### FP2: Idempotency Key ユーティリティがない
|
|
31
|
+
|
|
32
|
+
**状況**: `TtlCache` を使って実装できたが、`get_idempotency_cache()` Depends パターンや `X-Idempotency-Replayed` ヘッダーの付与は完全に自前実装。Stripe / Square などで標準化されたパターンであるため、nene2 に軽量ユーティリティがあると便利。
|
|
33
|
+
|
|
34
|
+
**影響**: 低。`TtlCache` + ハンドラーコードで完結可能。
|
|
35
|
+
|
|
36
|
+
## まとめ
|
|
37
|
+
|
|
38
|
+
`TtlCache` の実用例として Idempotency Key パターンはスムーズに実装できた。摩擦は小さい。
|
|
39
|
+
docs として how-to を追加する。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Field Trial 107: Bulk Operations(一括作成・削除)
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`POST /items/bulk` と `DELETE /items/bulk` による一括操作パターンを検証する。
|
|
6
|
+
部分成功(一部成功・一部失敗)を 207 Multi-Status で返す方法も確認する。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft107-bulk-ops/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `POST /items/bulk` — 一括作成(価格制限でビジネスバリデーション、部分失敗対応)
|
|
13
|
+
- `DELETE /items/bulk` — 一括削除(存在しない ID は failed に入れる)
|
|
14
|
+
- 207 Multi-Status レスポンスに `succeeded` / `failed` を含める
|
|
15
|
+
- 8 テスト通過(修正後)
|
|
16
|
+
|
|
17
|
+
## テスト結果
|
|
18
|
+
|
|
19
|
+
全 8 テスト通過(修正後)。
|
|
20
|
+
|
|
21
|
+
## Friction Points
|
|
22
|
+
|
|
23
|
+
### FP1: `TestClient.delete()` が `json` パラメーターを受け付けない
|
|
24
|
+
|
|
25
|
+
**状況**: `DELETE` リクエストにリクエストボディを付ける場合、`client.delete(url, json=body)` は `TypeError` になる。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# ❌ TypeError: unexpected keyword argument 'json'
|
|
29
|
+
r = client.delete("/items/bulk", json={"ids": [1, 2]})
|
|
30
|
+
|
|
31
|
+
# ✅ request() を使う
|
|
32
|
+
r = client.request("DELETE", "/items/bulk", json={"ids": [1, 2]})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**影響**: 中。DELETE + ボディはやや非標準だが、一括削除では一般的なパターン。テストコードが `request()` を直接使う必要があるため直感的でない。
|
|
36
|
+
|
|
37
|
+
**代替案**: ボディを持つ DELETE の代わりに `POST /items/bulk-delete` にする方が REST 的にクリーン(ボディを持つ DELETE は RFC 9110 で「意味がないわけではないが、推奨されない」)。
|
|
38
|
+
|
|
39
|
+
### FP2: 207 Multi-Status のレスポンス型が OpenAPI スキーマに表現しにくい
|
|
40
|
+
|
|
41
|
+
**状況**: `response_model` で 207 のスキーマを定義しようとすると、succeeded/failed の型が複雑になる。実用的には `response_model` なしで `JSONResponse` を直接返すのが現実的。
|
|
42
|
+
|
|
43
|
+
## まとめ
|
|
44
|
+
|
|
45
|
+
FP1 は how-to に追記(`TestClient` の HTTP メソッドと `json` パラメーターの注意点)。
|
|
46
|
+
FP2 はドキュメント摩擦(bulk 操作は `response_model` を省略して OK)。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Field Trial 108: Pydantic computed_field と property パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
Pydantic v2 の `@computed_field` + `@property` を使って、ストアドフィールドから計算されるプロパティを
|
|
6
|
+
OpenAPI スキーマに自動的に含めるパターンを検証する。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft108-computed-field/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `ProductResponse` — `price_dollars`, `is_in_stock`, `display_name` の computed fields
|
|
13
|
+
- `OrderLineResponse` — ネストした computed fields (`line_total_cents`, `line_total_dollars`)
|
|
14
|
+
- `/order-preview` エンドポイントで `JSONResponse(line.model_dump(mode="json"))` パターン確認
|
|
15
|
+
- 6 テスト通過
|
|
16
|
+
|
|
17
|
+
## テスト結果
|
|
18
|
+
|
|
19
|
+
全 6 テスト通過。
|
|
20
|
+
|
|
21
|
+
## Friction Points
|
|
22
|
+
|
|
23
|
+
### FP1: `model_dump()` が datetime を Python オブジェクトのまま返す → `JSONResponse` で 500 エラー
|
|
24
|
+
|
|
25
|
+
**状況**: `OrderLineResponse` のネストモデルには `created_at: datetime` フィールドがある。
|
|
26
|
+
`JSONResponse(line.model_dump())` を使うと、`json.dumps` が `datetime` を直列化できずに
|
|
27
|
+
`TypeError: Object of type datetime is not JSON serializable` で 500 エラーになる。
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# ❌ model_dump() は datetime をそのまま返す → JSONResponse でエラー
|
|
31
|
+
return JSONResponse(line.model_dump())
|
|
32
|
+
|
|
33
|
+
# ✅ mode="json" を指定すると datetime を ISO 8601 文字列に変換する
|
|
34
|
+
return JSONResponse(line.model_dump(mode="json"))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**影響**: 大。`response_model=` を使う通常のルートは FastAPI が自動変換するため問題ないが、
|
|
38
|
+
`JSONResponse` を直接返すルート(207 Multi-Status, /order-preview など)でこのパターンを
|
|
39
|
+
使い忘れると本番で 500 エラーになる。
|
|
40
|
+
|
|
41
|
+
**代替案**: `jsonable_encoder(line.model_dump())` でも変換できるが、`mode="json"` の方が Pydantic 標準。
|
|
42
|
+
|
|
43
|
+
## まとめ
|
|
44
|
+
|
|
45
|
+
`@computed_field` + `@property` は摩擦ゼロで OpenAPI スキーマに含められる優れたパターン。
|
|
46
|
+
FP1 は `JSONResponse` を直接使う場合の注意点として how-to に追記する。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Field Trial 109: API バージョニング(v1/v2 ルーティング)
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
FastAPI の `APIRouter` を使って `/v1` と `/v2` でエンドポイントを分離するパターンを検証する。
|
|
6
|
+
- フィールド名の変更(`email` → `contact_email`)
|
|
7
|
+
- フィールドの追加(`age` を v2 で追加)
|
|
8
|
+
- `full_name` から `first_name`/`last_name` への分割
|
|
9
|
+
- OpenAPI スキーマが両バージョンを正確に反映するか
|
|
10
|
+
|
|
11
|
+
## 実施内容
|
|
12
|
+
|
|
13
|
+
`/home/xi/docker/nene2-python-FT/ft109-api-versioning/` に以下を実装:
|
|
14
|
+
|
|
15
|
+
- ドメイン `User` dataclass(バージョン非依存)
|
|
16
|
+
- `UserResponseV1` — `full_name`, `email`
|
|
17
|
+
- `UserResponseV2` — `first_name`, `last_name`, `contact_email`, `age`
|
|
18
|
+
- `v1_router = APIRouter(prefix="/v1", tags=["v1"])`
|
|
19
|
+
- `v2_router = APIRouter(prefix="/v2", tags=["v2"])`
|
|
20
|
+
- 9 テスト通過
|
|
21
|
+
|
|
22
|
+
## テスト結果
|
|
23
|
+
|
|
24
|
+
全 9 テスト一発通過。摩擦ゼロ。
|
|
25
|
+
|
|
26
|
+
## Friction Points
|
|
27
|
+
|
|
28
|
+
なし。`APIRouter(prefix="/v1")` でルーターを分離し `app.include_router()` で登録するだけで
|
|
29
|
+
バージョン分離が完成する。OpenAPI スキーマも `UserResponseV1` / `UserResponseV2` を
|
|
30
|
+
個別に定義することで両バージョンのスキーマが正確に生成される。
|
|
31
|
+
|
|
32
|
+
## 観察
|
|
33
|
+
|
|
34
|
+
### O1: バージョン間の共有ロジックはドメイン層に置く
|
|
35
|
+
|
|
36
|
+
`find_user()` などのクエリ関数はバージョン非依存のドメイン層に置き、
|
|
37
|
+
各バージョンのハンドラーから呼ぶ設計が自然に機能した。
|
|
38
|
+
`from_domain()` クラスメソッドで変換することでドメインと HTTP を分離できた。
|
|
39
|
+
|
|
40
|
+
### O2: OpenAPI タグでバージョンを整理できる
|
|
41
|
+
|
|
42
|
+
`tags=["v1"]` / `tags=["v2"]` を `APIRouter` に指定すると、
|
|
43
|
+
Swagger UI でバージョンごとにグループ化されて見やすくなる。
|
|
44
|
+
|
|
45
|
+
### O3: `APIRouter` prefix は重複しない
|
|
46
|
+
|
|
47
|
+
`app.include_router(v1_router)` で登録すると `/v1/users` になる。
|
|
48
|
+
ルーター内のパスに `/v1` を書く必要はない(二重にならない)。
|
|
49
|
+
|
|
50
|
+
## まとめ
|
|
51
|
+
|
|
52
|
+
FT109 は摩擦ゼロ確認。APIバージョニングは FastAPI の `APIRouter` + `prefix` で
|
|
53
|
+
自然に実現できる。how-to ドキュメントに記録のみ。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# How-to: API バージョニング
|
|
2
|
+
|
|
3
|
+
FastAPI の `APIRouter` + `prefix` でエンドポイントを `/v1`, `/v2` に分離するパターン。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本構成
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import APIRouter, FastAPI
|
|
11
|
+
|
|
12
|
+
app = FastAPI()
|
|
13
|
+
|
|
14
|
+
v1_router = APIRouter(prefix="/v1", tags=["v1"])
|
|
15
|
+
v2_router = APIRouter(prefix="/v2", tags=["v2"])
|
|
16
|
+
|
|
17
|
+
@v1_router.get("/users")
|
|
18
|
+
def list_users_v1() -> list[UserResponseV1]:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@v2_router.get("/users")
|
|
22
|
+
def list_users_v2() -> list[UserResponseV2]:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
app.include_router(v1_router)
|
|
26
|
+
app.include_router(v2_router)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`APIRouter` の `prefix` はルーター内のパスに **自動的に付加**される。
|
|
30
|
+
ルーター内のパスに `/v1` を書く必要はない(二重にならない)。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ドメイン層は共有、HTTP 層でバージョン差分を吸収
|
|
35
|
+
|
|
36
|
+
バージョン間の共有ロジックはドメイン層に置き、各バージョンの Pydantic モデルで差分を表現する。
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# ドメイン層(バージョン非依存)
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class User:
|
|
42
|
+
user_id: int
|
|
43
|
+
first_name: str
|
|
44
|
+
last_name: str
|
|
45
|
+
email: str
|
|
46
|
+
age: int
|
|
47
|
+
|
|
48
|
+
# v1: full_name に結合
|
|
49
|
+
class UserResponseV1(BaseModel):
|
|
50
|
+
user_id: int
|
|
51
|
+
full_name: str
|
|
52
|
+
email: str
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_domain(cls, user: User) -> "UserResponseV1":
|
|
56
|
+
return cls(
|
|
57
|
+
user_id=user.user_id,
|
|
58
|
+
full_name=f"{user.first_name} {user.last_name}",
|
|
59
|
+
email=user.email,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# v2: first_name/last_name を分離 + age を追加 + email → contact_email
|
|
63
|
+
class UserResponseV2(BaseModel):
|
|
64
|
+
user_id: int
|
|
65
|
+
first_name: str
|
|
66
|
+
last_name: str
|
|
67
|
+
contact_email: str
|
|
68
|
+
age: int
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_domain(cls, user: User) -> "UserResponseV2":
|
|
72
|
+
return cls(
|
|
73
|
+
user_id=user.user_id,
|
|
74
|
+
first_name=user.first_name,
|
|
75
|
+
last_name=user.last_name,
|
|
76
|
+
contact_email=user.email,
|
|
77
|
+
age=user.age,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## OpenAPI スキーマ
|
|
84
|
+
|
|
85
|
+
`tags=["v1"]` / `tags=["v2"]` を `APIRouter` に指定すると Swagger UI でバージョンごとに
|
|
86
|
+
グループ化される。スキーマには `UserResponseV1` / `UserResponseV2` が個別に定義される。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 参照
|
|
91
|
+
|
|
92
|
+
- FT109: `docs/field-trials/2026-05-field-trial-109.md`
|