nene2-python 1.8.31__tar.gz → 1.8.33__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.31 → nene2_python-1.8.33}/CHANGELOG.md +37 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/PKG-INFO +1 -1
- nene2_python-1.8.33/docs/field-trials/2026-05-field-trial-100.md +62 -0
- nene2_python-1.8.33/docs/field-trials/2026-05-field-trial-97.md +87 -0
- nene2_python-1.8.33/docs/field-trials/2026-05-field-trial-98.md +97 -0
- nene2_python-1.8.33/docs/field-trials/2026-05-field-trial-99.md +57 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/async-use-case.md +41 -0
- nene2_python-1.8.33/docs/how-to/background-tasks.md +101 -0
- nene2_python-1.8.33/docs/how-to/cors.md +87 -0
- nene2_python-1.8.33/docs/how-to/domain-events.md +119 -0
- nene2_python-1.8.33/docs/how-to/file-upload.md +142 -0
- nene2_python-1.8.33/docs/how-to/lifespan-and-app-state.md +109 -0
- nene2_python-1.8.33/docs/how-to/response-patterns.md +110 -0
- nene2_python-1.8.33/docs/how-to/streaming.md +135 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/validation.md +35 -1
- nene2_python-1.8.33/docs/how-to/webhook.md +151 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/pyproject.toml +1 -1
- nene2_python-1.8.33/src/nene2/cache/__init__.py +5 -0
- nene2_python-1.8.33/src/nene2/cache/ttl.py +57 -0
- nene2_python-1.8.33/src/nene2/security/__init__.py +5 -0
- nene2_python-1.8.33/src/nene2/security/webhook.py +29 -0
- nene2_python-1.8.33/tests/nene2/cache/test_ttl.py +76 -0
- nene2_python-1.8.33/tests/nene2/security/test_webhook.py +62 -0
- nene2_python-1.8.33/tests/nene2/validation/__init__.py +0 -0
- nene2_python-1.8.33/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/uv.lock +1 -1
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.env.example +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.gitignore +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/AGENTS.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/CLAUDE.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/Dockerfile +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/LICENSE +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/README.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/alembic/README +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/alembic/env.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/alembic.ini +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/compose.yaml +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/de/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/fr/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/reference/api.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/roadmap.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/todo/current.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/zh/index.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/package-lock.json +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/package.json +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/__main__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/app.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/mcp.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/schema.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.31/tests/nene2/config → nene2_python-1.8.33/tests/nene2/cache}/__init__.py +0 -0
- {nene2_python-1.8.31/tests/nene2/database → nene2_python-1.8.33/tests/nene2/config}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.31/tests/nene2/http → nene2_python-1.8.33/tests/nene2/database}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.31/tests/nene2/log → nene2_python-1.8.33/tests/nene2/http}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.31/tests/nene2/mcp → nene2_python-1.8.33/tests/nene2/log}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.31/tests/nene2/middleware → nene2_python-1.8.33/tests/nene2/mcp}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.31/tests/nene2/use_case → nene2_python-1.8.33/tests/nene2/middleware}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.31/tests/nene2/validation → nene2_python-1.8.33/tests/nene2/security}/__init__.py +0 -0
- {nene2_python-1.8.31/tests/scripts → nene2_python-1.8.33/tests/nene2/use_case}/__init__.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.31 → nene2_python-1.8.33}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,43 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.33] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT100 フィールドトライアル — In-memory TTL レスポンスキャッシュパターン検証と nene2.cache モジュール追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `nene2.cache` モジュールを新設 (#409) (FT100)
|
|
14
|
+
- `TtlCache[V]` — TTL 付きインメモリキャッシュ(ジェネリック型)
|
|
15
|
+
- `time.monotonic()` ベースの TTL で NTP 調整の影響を受けない
|
|
16
|
+
- `get()`, `set()`, `delete()`, `clear()`, `size()` API
|
|
17
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-100.md` (FT100)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [1.8.32] — 2026-05-20
|
|
22
|
+
|
|
23
|
+
FT99 フィールドトライアル — Webhook HMAC-SHA256 署名検証パターン検証と nene2.security モジュール追加。
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `nene2.security` モジュールを新設 (#404) (FT99)
|
|
27
|
+
- `verify_hmac_signature(body, secret, signature, *, prefix="")` — GitHub/Stripe 方式の Webhook HMAC-SHA256 署名を timing-safe に検証
|
|
28
|
+
- `hmac.compare_digest()` による timing attack 対策済み
|
|
29
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-99.md` (FT99)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [1.8.31] — 2026-05-20
|
|
34
|
+
|
|
35
|
+
FT97 フィールドトライアル — HTTP キャッシュヘッダーパターン検証と generate_etag() 追加。
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- `nene2.http.generate_etag(data)` — ETag 生成ユーティリティ関数を追加 (#397) (FT97)
|
|
39
|
+
— dict / list / str / bytes から RFC 9110 形式の ETag 文字列を生成
|
|
40
|
+
— HTTP キャッシュ (`If-None-Match` / `304 Not Modified`) パターンで利用
|
|
41
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-92.md` 〜 `2026-05-field-trial-97.md` (FT92〜FT97)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
8
45
|
## [1.8.30] — 2026-05-20
|
|
9
46
|
|
|
10
47
|
FT87 フィールドトライアル — カスタムレスポンスヘッダーパターン検証と problem_details_response() 改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.33
|
|
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,87 @@
|
|
|
1
|
+
# Field Trial 97: HTTP キャッシュヘッダー (ETag / Cache-Control)
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: ETag・Cache-Control・304 Not Modified パターンを nene2 で実装
|
|
5
|
+
**バージョン**: v1.8.30
|
|
6
|
+
**結果**: 摩擦あり(コード修正なし)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 目的
|
|
11
|
+
|
|
12
|
+
HTTP キャッシュ機構(`ETag`, `Cache-Control`, `If-None-Match`, `304 Not Modified`)を nene2 ベースの API で実装するパターンを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
`/home/xi/docker/nene2-python-FT/ft97-http-caching/` に以下を実装:
|
|
19
|
+
|
|
20
|
+
- `app.py` — ETag 付き GET エンドポイント、`If-None-Match` による 304 返却、Cache-Control ヘッダー
|
|
21
|
+
- 13 テスト(全 PASS)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 確認できた良好な動作
|
|
26
|
+
|
|
27
|
+
### ETag + 304 のパターン
|
|
28
|
+
|
|
29
|
+
`If-None-Match` ヘッダーと ETag を比較して 304 を返すパターンは問題なく動作する。
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
@app.get("/articles/{article_id}")
|
|
33
|
+
def get_article(article_id: int, request: Request) -> Response:
|
|
34
|
+
data = _article_to_dict(article)
|
|
35
|
+
etag = _compute_etag(data)
|
|
36
|
+
|
|
37
|
+
if request.headers.get("if-none-match") == etag:
|
|
38
|
+
return Response(status_code=304, headers={"ETag": etag})
|
|
39
|
+
|
|
40
|
+
return JSONResponse(data, headers={"ETag": etag, "Cache-Control": "max-age=60"})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 304 レスポンスにも X-Request-Id が付く
|
|
44
|
+
|
|
45
|
+
`Response(status_code=304)` を返しても、外側の `RequestIdMiddleware` が `X-Request-Id` を付与する。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 摩擦点
|
|
50
|
+
|
|
51
|
+
### F97-1: nene2 に ETag 生成ユーティリティがない
|
|
52
|
+
|
|
53
|
+
ETag 生成ロジック(MD5 ハッシュ)を各プロジェクトで手動実装する必要がある。各エンドポイントで繰り返し実装することになり DRY でない。
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
def _compute_etag(data: object) -> str:
|
|
57
|
+
content = json.dumps(data, sort_keys=True, ensure_ascii=False)
|
|
58
|
+
return f'"{hashlib.md5(content.encode(), usedforsecurity=False).hexdigest()}"'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
nene2 が `generate_etag(data)` などのユーティリティ関数を提供すれば再実装不要になる。
|
|
62
|
+
|
|
63
|
+
### F97-2: `hashlib.md5()` に `usedforsecurity=False` が必要
|
|
64
|
+
|
|
65
|
+
ruff のセキュリティルール `S324`(MD5 使用禁止)により、ETag 生成で `hashlib.md5()` を使うと lint エラーになる。ETag は暗号セキュリティ用途ではないが、明示的に `usedforsecurity=False` を指定する必要がある。
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# ❌ ruff S324 エラー
|
|
69
|
+
hashlib.md5(content.encode()).hexdigest()
|
|
70
|
+
|
|
71
|
+
# ✅ 正しい
|
|
72
|
+
hashlib.md5(content.encode(), usedforsecurity=False).hexdigest()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
nene2 のユーティリティ関数でラップすればプロジェクトごとに対処不要になる。
|
|
76
|
+
|
|
77
|
+
### F97-3: Cache-Control ヘッダーの付与はエンドポイントごとに手動
|
|
78
|
+
|
|
79
|
+
`JSONResponse(headers={"Cache-Control": "max-age=60"})` のように各エンドポイントで手動指定が必要。
|
|
80
|
+
ミドルウェアでのデフォルト付与(例: 全 GET に `Cache-Control: no-cache` を付与する設定)がないため、付け忘れが起きやすい。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 結論
|
|
85
|
+
|
|
86
|
+
HTTP キャッシュヘッダーは nene2 と問題なく実装できるが、ETag 生成の共通化がない。
|
|
87
|
+
`generate_etag()` などのユーティリティ関数を nene2 に追加することで摩擦を解消できる。
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Field Trial 98: PATCH / Partial Update パターン
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: Pydantic v2 の `exclude_unset=True` を使った部分更新パターン
|
|
5
|
+
**バージョン**: v1.8.31
|
|
6
|
+
**結果**: 摩擦あり(コード修正なし)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 目的
|
|
11
|
+
|
|
12
|
+
HTTP PATCH で「送信されたフィールドのみ更新する」パターンを Pydantic v2 + nene2 で実装し、`None`(明示的 null)と「未送信」の区別ができることを検証する。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実施内容
|
|
17
|
+
|
|
18
|
+
`/home/xi/docker/nene2-python-FT/ft98-patch-partial-update/` に以下を実装:
|
|
19
|
+
|
|
20
|
+
- `app.py` — `PUT`(完全更新)と `PATCH`(部分更新)を持つユーザー CRUD
|
|
21
|
+
- 12 テスト(全 PASS)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 確認できた良好な動作
|
|
26
|
+
|
|
27
|
+
### `exclude_unset=True` で未送信フィールドを除外
|
|
28
|
+
|
|
29
|
+
`PatchUserBody` の全フィールドを `Optional` にし、`model_dump(exclude_unset=True)` で送信されたフィールドだけを取り出す。
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
class PatchUserBody(BaseModel):
|
|
33
|
+
name: str | None = Field(default=None, max_length=100)
|
|
34
|
+
email: str | None = Field(default=None, max_length=200)
|
|
35
|
+
bio: str | None = Field(default=None, max_length=500)
|
|
36
|
+
|
|
37
|
+
@app.patch("/users/{user_id}")
|
|
38
|
+
def patch_user(user_id: int, body: PatchUserBody) -> JSONResponse:
|
|
39
|
+
updates = body.model_dump(exclude_unset=True)
|
|
40
|
+
updated = User(
|
|
41
|
+
user_id=user_id,
|
|
42
|
+
name=updates.get("name", current.name),
|
|
43
|
+
email=updates.get("email", current.email),
|
|
44
|
+
bio=updates.get("bio", current.bio),
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `{}` と `{"bio": null}` を区別できる
|
|
49
|
+
|
|
50
|
+
`exclude_unset=True` により、空ボディ `{}` と `{"bio": null}` は正しく区別される。
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
body_without_bio = PatchUserBody(name="Alice")
|
|
54
|
+
assert "bio" not in body_without_bio.model_dump(exclude_unset=True) # 未送信
|
|
55
|
+
# {"bio": null} を送ると exclude_unset でも "bio" が含まれる → None に更新
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 摩擦点
|
|
61
|
+
|
|
62
|
+
### F98-1: PATCH ボディの全フィールドが Optional なため、バリデーションが緩くなる
|
|
63
|
+
|
|
64
|
+
`PUT` ボディは必須フィールドあり(空だと 422)。`PATCH` ボディは全フィールド Optional なため、空ボディ `{}` も有効に受け入れられる。
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# PUT: name 省略 → 422
|
|
68
|
+
client.put("/users/1", json={"email": "..."}) # 422
|
|
69
|
+
|
|
70
|
+
# PATCH: 全フィールド省略 → 200(空更新)
|
|
71
|
+
client.patch("/users/1", json={}) # 200(何も変わらない)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
意図的な設計だが、API クライアント側が誤って空 PATCH を送っても検知できない。
|
|
75
|
+
|
|
76
|
+
### F98-2: `model_dump()` と `model_dump(exclude_unset=True)` の違いを意識する必要がある
|
|
77
|
+
|
|
78
|
+
`default=None` で定義したフィールドは、`model_dump()` では常に含まれる(`{"bio": None}`)。`exclude_unset=True` を忘れると「未送信」と「null 送信」の区別ができなくなる。
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# ❌ exclude_unset=True を忘れると未送信フィールドも含まれる
|
|
82
|
+
updates = body.model_dump() # {"name": None, "email": None, "bio": None}
|
|
83
|
+
|
|
84
|
+
# ✅ 送信されたフィールドのみ
|
|
85
|
+
updates = body.model_dump(exclude_unset=True) # {}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### F98-3: `str | None` フィールドで「クリアしたい」と「変更なし」が混同しやすい
|
|
89
|
+
|
|
90
|
+
`bio: str | None = None` の PATCH ボディでは、`{"bio": null}` が「bio を null にしたい」なのか「変更なし」なのかをクライアント側の意図だけで区別する必要がある。`exclude_unset=True` により `{"bio": null}` は「null に更新」として正しく扱えるが、API ドキュメントでこの挙動を明記する必要がある。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 結論
|
|
95
|
+
|
|
96
|
+
Pydantic v2 の `exclude_unset=True` + 全フィールド `Optional` の PATCH パターンは nene2 と問題なく組み合わせられる。
|
|
97
|
+
主な摩擦はバリデーションの緩さと `model_dump` の使い分け。コード修正は不要。
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Field Trial 99: Webhook HMAC-SHA256 署名検証
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
外部サービス(GitHub / Stripe 方式)からの Webhook を HMAC-SHA256 署名で検証するパターンを nene2 上で実装する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft99-webhook-hmac/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- GitHub 方式: `X-Hub-Signature-256: sha256=<hex>` ヘッダーを HMAC-SHA256 で検証
|
|
12
|
+
- Stripe 方式: `Stripe-Signature: t=<timestamp>,v1=<hex>` 形式の署名を検証
|
|
13
|
+
- `hmac.compare_digest()` による timing-safe 比較
|
|
14
|
+
- `await request.body()` + `await request.json()` の二重読み取りパターン
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 7 テスト通過。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: nene2 に Webhook 署名検証ユーティリティがない
|
|
23
|
+
|
|
24
|
+
**状況**: GitHub/Stripe 方式の HMAC 署名検証は頻出パターンだが、nene2 に `verify_webhook_signature()` のようなユーティリティが存在しない。毎回 `hmac` モジュールを直接扱う必要がある。
|
|
25
|
+
|
|
26
|
+
**影響**: `hmac.new()` / `hmac.compare_digest()` を知らない開発者が `==` 比較を使い、timing attack に脆弱な実装をしてしまうリスクがある。
|
|
27
|
+
|
|
28
|
+
**期待する API**:
|
|
29
|
+
```python
|
|
30
|
+
from nene2.security import verify_hmac_signature
|
|
31
|
+
|
|
32
|
+
# GitHub 方式
|
|
33
|
+
verify_hmac_signature(body, secret, header_value, prefix="sha256=")
|
|
34
|
+
|
|
35
|
+
# Stripe 方式
|
|
36
|
+
verify_stripe_signature(body, secret, header_value)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### FP2: `await request.body()` → `await request.json()` の二重読み取りがドキュメント化されていない
|
|
40
|
+
|
|
41
|
+
**状況**: Webhook 署名検証では生バイト(`request.body()`)を HMAC に通してから、JSONとしてパース(`request.json()`)する二重読み取りが必要。FastAPI は内部でボディをキャッシュするため動作するが、この挙動はフレームワークに依存した暗黙の知識。
|
|
42
|
+
|
|
43
|
+
**影響**: 「一度 `body()` を読んだら `json()` は使えない」と誤解して `json.loads(body)` を書く開発者が出る。
|
|
44
|
+
|
|
45
|
+
**期待するドキュメント**: how-to に「Webhook ハンドラーでの生ボディ + JSON 二重読み取りパターン」を追加。
|
|
46
|
+
|
|
47
|
+
### FP3: BearerTokenMiddleware は Webhook 認証に使えない
|
|
48
|
+
|
|
49
|
+
**状況**: nene2 の `BearerTokenMiddleware` は Bearer トークン認証に特化しており、HMAC 署名検証(リクエストボディを使った認証)には対応しない。Webhook エンドポイントでは `exclude_paths` で除外して自前検証するか、専用の Depends 関数を書く必要がある。
|
|
50
|
+
|
|
51
|
+
**影響**: 摩擦は低いが、「nene2 の認証機構でWebhookも守れる」という誤解が生まれやすい。
|
|
52
|
+
|
|
53
|
+
**期待するドキュメント**: how-to に「Webhook 署名検証 vs Bearer Token 認証の使い分け」を明記。
|
|
54
|
+
|
|
55
|
+
## まとめ
|
|
56
|
+
|
|
57
|
+
Webhook HMAC 検証自体は Python 標準ライブラリで実装できるが、セキュアな実装(timing-safe 比較)のためのユーティリティがないため FP1 として Issue を起票する。FP2・FP3 はドキュメント摩擦。
|
|
@@ -119,3 +119,44 @@ inspect.iscoroutinefunction(use_case.execute) # → True/False
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
型安全性は `mypy --strict` の静的解析で保証します。詳細は ADR-0010 を参照してください。
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 同期 DB 呼び出しのブロッキング問題
|
|
126
|
+
|
|
127
|
+
`async def` ハンドラーで同期の DB 呼び出し(SQLAlchemy sync API 等)を行うと、イベントループをブロックして他のリクエストが詰まる。
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# ❌ async def 内での同期 DB 呼び出しはブロッキング
|
|
131
|
+
@app.get("/notes")
|
|
132
|
+
async def list_notes() -> JSONResponse:
|
|
133
|
+
notes = session.execute(select(Note)).scalars().all() # ブロック!
|
|
134
|
+
return JSONResponse(...)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**解決策1: `run_in_threadpool` でスレッドプールで実行する**
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from nene2.middleware import run_in_threadpool
|
|
141
|
+
|
|
142
|
+
@app.get("/notes")
|
|
143
|
+
async def list_notes() -> JSONResponse:
|
|
144
|
+
notes = await run_in_threadpool(session.execute, select(Note))
|
|
145
|
+
return JSONResponse(...)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**解決策2: `def`(同期)ハンドラーを使う**
|
|
149
|
+
|
|
150
|
+
同期 DB を使う場合は、ハンドラーを `async def` にしない。FastAPI が自動でスレッドプールで実行する。
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# ✅ def ハンドラー + 同期 DB = 問題なし
|
|
154
|
+
@app.get("/notes")
|
|
155
|
+
def list_notes() -> JSONResponse:
|
|
156
|
+
notes = session.execute(select(Note)).scalars().all()
|
|
157
|
+
return JSONResponse(...)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**解決策3: SQLAlchemy async API に移行する**
|
|
161
|
+
|
|
162
|
+
長期的には SQLAlchemy の async API(`AsyncSession`)への移行を検討する。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# How-to: BackgroundTasks
|
|
2
|
+
|
|
3
|
+
FastAPI の `BackgroundTasks` を使ってレスポンス後に処理を実行するパターンを説明する。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 基本パターン
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import BackgroundTasks, FastAPI
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
|
|
13
|
+
app = FastAPI()
|
|
14
|
+
|
|
15
|
+
def send_notification(message: str) -> None:
|
|
16
|
+
# 時間がかかる処理(メール送信・外部 API 呼び出し等)
|
|
17
|
+
print(f"Sending: {message}")
|
|
18
|
+
|
|
19
|
+
@app.post("/orders", status_code=201)
|
|
20
|
+
def create_order(
|
|
21
|
+
body: CreateOrderBody,
|
|
22
|
+
background_tasks: BackgroundTasks,
|
|
23
|
+
) -> JSONResponse:
|
|
24
|
+
order = process_order(body)
|
|
25
|
+
background_tasks.add_task(send_notification, f"Order {order.order_id} created")
|
|
26
|
+
return JSONResponse({"order_id": order.order_id}, status_code=201)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. UseCase との分離
|
|
32
|
+
|
|
33
|
+
UseCase を HTTP 非依存に保つため、`BackgroundTasks` は UseCase に渡さない。ハンドラー層でイベントを受け取り、BackgroundTasks に追加する。
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# ✅ UseCase は BackgroundTasks を知らない
|
|
37
|
+
class CreateOrderUseCase:
|
|
38
|
+
def execute(self, body: CreateOrderInput) -> CreateOrderOutput:
|
|
39
|
+
order = Order(...)
|
|
40
|
+
return CreateOrderOutput(order_id=order.order_id, notify_email=body.email)
|
|
41
|
+
|
|
42
|
+
# ハンドラー層で BackgroundTasks を使う
|
|
43
|
+
@app.post("/orders", status_code=201)
|
|
44
|
+
def create_order(
|
|
45
|
+
body: CreateOrderBody,
|
|
46
|
+
background_tasks: BackgroundTasks,
|
|
47
|
+
use_case: CreateOrderUseCase = Depends(get_use_case),
|
|
48
|
+
) -> JSONResponse:
|
|
49
|
+
result = use_case.execute(CreateOrderInput(email=body.email))
|
|
50
|
+
background_tasks.add_task(send_notification, result.notify_email)
|
|
51
|
+
return JSONResponse({"order_id": result.order_id}, status_code=201)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. TestClient での挙動
|
|
57
|
+
|
|
58
|
+
`TestClient` では `BackgroundTasks` がレスポンス返却 **前** に同期的に実行される。
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
executed: list[str] = []
|
|
62
|
+
|
|
63
|
+
def track_task(msg: str) -> None:
|
|
64
|
+
executed.append(msg)
|
|
65
|
+
|
|
66
|
+
# テストでは BackgroundTasks が同期実行される
|
|
67
|
+
r = client.post("/orders", json={"email": "alice@example.com"})
|
|
68
|
+
assert r.status_code == 201
|
|
69
|
+
assert len(executed) == 1 # すでに実行済み
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
本番環境では非同期実行(レスポンス後)だが、テストでは同期実行されることに注意。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 4. 失敗しても 500 にならない
|
|
77
|
+
|
|
78
|
+
`BackgroundTasks` 内で例外が発生しても、レスポンスはすでに送信済みのため 500 にはならない。エラーはログに記録される。
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def risky_task() -> None:
|
|
82
|
+
raise RuntimeError("Background task failed")
|
|
83
|
+
|
|
84
|
+
# レスポンスは 201 で返る(バックグラウンドエラーは隠れる)
|
|
85
|
+
background_tasks.add_task(risky_task)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
重要な処理は BackgroundTasks に頼らず、ジョブキュー(Celery・ARQ 等)を使う。
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. async def との組み合わせ
|
|
93
|
+
|
|
94
|
+
`background_tasks.add_task()` には同期・非同期どちらの関数も渡せる。
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
async def async_notification(email: str) -> None:
|
|
98
|
+
await send_email_async(email)
|
|
99
|
+
|
|
100
|
+
background_tasks.add_task(async_notification, user.email)
|
|
101
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# How-to: CORS 設定
|
|
2
|
+
|
|
3
|
+
`setup_middlewares()` の `cors_allowed_origins` パラメーターで CORS を有効化する方法を説明する。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 基本: 単一オリジンを許可
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from nene2.middleware import setup_middlewares
|
|
11
|
+
|
|
12
|
+
app = FastAPI()
|
|
13
|
+
setup_middlewares(app, cors_allowed_origins=["https://example.com"])
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`cors_allowed_origins` を指定しない(デフォルト `None`)と CORS ミドルウェアは追加されない。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. 複数オリジンを許可
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
setup_middlewares(app, cors_allowed_origins=[
|
|
24
|
+
"https://app.example.com",
|
|
25
|
+
"https://admin.example.com",
|
|
26
|
+
])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. 開発環境: localhost を許可
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
origins = ["https://app.example.com"]
|
|
37
|
+
if os.getenv("APP_ENV") == "local":
|
|
38
|
+
origins += ["http://localhost:3000", "http://localhost:5173"]
|
|
39
|
+
|
|
40
|
+
setup_middlewares(app, cors_allowed_origins=origins)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**`allow_origins=["*"]` は禁止**。CLAUDE.md のセキュリティポリシーにより、ワイルドカードオリジンは開発環境でも使用不可。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. credentials(Cookie・Authorization ヘッダー)を許可
|
|
48
|
+
|
|
49
|
+
`setup_middlewares()` は内部で `allow_credentials=True` を設定しない。credentials が必要な場合は `CORSMiddleware` を直接追加する。
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
53
|
+
|
|
54
|
+
app = FastAPI()
|
|
55
|
+
setup_middlewares(app) # 他のミドルウェア(RequestId 等)は通常通り設定
|
|
56
|
+
app.add_middleware(
|
|
57
|
+
CORSMiddleware,
|
|
58
|
+
allow_origins=["https://app.example.com"],
|
|
59
|
+
allow_credentials=True,
|
|
60
|
+
allow_methods=["*"],
|
|
61
|
+
allow_headers=["*"],
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**注意**: `add_middleware` は LIFO のため、`CORSMiddleware` を後から追加すると最外側に配置される。`setup_middlewares()` の後に呼ぶことで、CORS が最も外側で処理される。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 5. CORS とプリフライトリクエスト
|
|
70
|
+
|
|
71
|
+
`OPTIONS` リクエスト(プリフライト)は `CORSMiddleware` が自動で処理する。`@app.options(...)` を定義する必要はない。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 6. テストでの CORS ヘッダー確認
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
def test_cors_header() -> None:
|
|
79
|
+
with TestClient(app) as client:
|
|
80
|
+
r = client.get("/items", headers={"Origin": "https://app.example.com"})
|
|
81
|
+
assert r.headers.get("access-control-allow-origin") == "https://app.example.com"
|
|
82
|
+
|
|
83
|
+
def test_cors_not_allowed_for_unknown_origin() -> None:
|
|
84
|
+
with TestClient(app) as client:
|
|
85
|
+
r = client.get("/items", headers={"Origin": "https://evil.com"})
|
|
86
|
+
assert "access-control-allow-origin" not in r.headers
|
|
87
|
+
```
|