nene2-python 1.8.32__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.32 → nene2_python-1.8.33}/CHANGELOG.md +13 -0
- {nene2_python-1.8.32 → 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/how-to/webhook.md +151 -0
- {nene2_python-1.8.32 → 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/tests/nene2/cache/test_ttl.py +76 -0
- nene2_python-1.8.33/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/uv.lock +1 -1
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.env.example +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.gitignore +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/AGENTS.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/CLAUDE.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/Dockerfile +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/LICENSE +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/README.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/README +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/env.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic.ini +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/compose.yaml +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/de/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/fr/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/api.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/roadmap.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/todo/current.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/zh/index.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/package-lock.json +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/package.json +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/__main__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/app.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/mcp.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/schema.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.32/tests/nene2/config → nene2_python-1.8.33/tests/nene2/cache}/__init__.py +0 -0
- {nene2_python-1.8.32/tests/nene2/database → nene2_python-1.8.33/tests/nene2/config}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.32/tests/nene2/http → nene2_python-1.8.33/tests/nene2/database}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.32/tests/nene2/log → nene2_python-1.8.33/tests/nene2/http}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.32/tests/nene2/mcp → nene2_python-1.8.33/tests/nene2/log}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.32/tests/nene2/middleware → nene2_python-1.8.33/tests/nene2/mcp}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.32/tests/nene2/security → nene2_python-1.8.33/tests/nene2/middleware}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.32/tests/nene2/use_case → nene2_python-1.8.33/tests/nene2/security}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.32/tests/nene2/validation → nene2_python-1.8.33/tests/nene2/use_case}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.32/tests/scripts → nene2_python-1.8.33/tests/nene2/validation}/__init__.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,19 @@ 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
|
+
|
|
8
21
|
## [1.8.32] — 2026-05-20
|
|
9
22
|
|
|
10
23
|
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.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,151 @@
|
|
|
1
|
+
# How-to: Webhook 受信と HMAC-SHA256 署名検証
|
|
2
|
+
|
|
3
|
+
GitHub や Stripe などの外部サービスから Webhook を受信し、HMAC-SHA256 署名を検証するパターンを説明する。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 基本パターン(GitHub 方式)
|
|
8
|
+
|
|
9
|
+
GitHub は `X-Hub-Signature-256: sha256=<hex>` ヘッダーで署名を送る。
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from fastapi import FastAPI, Request
|
|
13
|
+
from fastapi.responses import JSONResponse
|
|
14
|
+
|
|
15
|
+
from nene2.security import verify_hmac_signature
|
|
16
|
+
|
|
17
|
+
WEBHOOK_SECRET = "your-secret-key"
|
|
18
|
+
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
|
|
21
|
+
@app.post("/webhooks/github")
|
|
22
|
+
async def github_webhook(request: Request) -> JSONResponse:
|
|
23
|
+
signature = request.headers.get("X-Hub-Signature-256", "")
|
|
24
|
+
body = await request.body()
|
|
25
|
+
|
|
26
|
+
if not signature:
|
|
27
|
+
return JSONResponse({"error": "Missing signature"}, status_code=400)
|
|
28
|
+
|
|
29
|
+
if not verify_hmac_signature(body, WEBHOOK_SECRET, signature, prefix="sha256="):
|
|
30
|
+
return JSONResponse({"error": "Invalid signature"}, status_code=401)
|
|
31
|
+
|
|
32
|
+
payload = await request.json()
|
|
33
|
+
event = request.headers.get("X-GitHub-Event", "unknown")
|
|
34
|
+
# ... イベント処理
|
|
35
|
+
return JSONResponse({"status": "received", "event": event})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Stripe 方式(timestamp 付き署名)
|
|
41
|
+
|
|
42
|
+
Stripe は `Stripe-Signature: t=<timestamp>,v1=<hex>` 形式で送る。timestamp + body を HMAC する。
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import hashlib
|
|
46
|
+
import hmac
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
@app.post("/webhooks/stripe")
|
|
50
|
+
async def stripe_webhook(request: Request) -> JSONResponse:
|
|
51
|
+
stripe_sig = request.headers.get("Stripe-Signature", "")
|
|
52
|
+
body = await request.body()
|
|
53
|
+
|
|
54
|
+
if not stripe_sig:
|
|
55
|
+
return JSONResponse({"error": "Missing Stripe-Signature"}, status_code=400)
|
|
56
|
+
|
|
57
|
+
parts = dict(item.split("=", 1) for item in stripe_sig.split(",") if "=" in item)
|
|
58
|
+
timestamp = parts.get("t", "")
|
|
59
|
+
v1_sig = parts.get("v1", "")
|
|
60
|
+
|
|
61
|
+
# Stripe 方式: "timestamp." + body を HMAC する
|
|
62
|
+
signed_payload = f"{timestamp}.".encode() + body
|
|
63
|
+
if not verify_hmac_signature(signed_payload, WEBHOOK_SECRET, v1_sig):
|
|
64
|
+
return JSONResponse({"error": "Invalid signature"}, status_code=401)
|
|
65
|
+
|
|
66
|
+
payload = await request.json()
|
|
67
|
+
return JSONResponse({"status": "received", "type": payload.get("type")})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 3. `await request.body()` → `await request.json()` の二重読み取り
|
|
73
|
+
|
|
74
|
+
署名検証では生バイト(`body()`)が必要だが、その後 JSON としてもパースしたい。
|
|
75
|
+
FastAPI はボディを内部でキャッシュするので、両方呼び出せる。
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
@app.post("/webhooks/example")
|
|
79
|
+
async def handler(request: Request) -> JSONResponse:
|
|
80
|
+
# ✅ body() を先に呼んでも json() は正常に動く
|
|
81
|
+
body = await request.body() # 生バイト取得(署名検証用)
|
|
82
|
+
payload = await request.json() # JSON パース(内部キャッシュを使う)
|
|
83
|
+
return JSONResponse({"size": len(body), "action": payload.get("action")})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`json.loads(body)` でも動作するが、`await request.json()` の方が Pydantic モデル変換と統一感がある。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. `verify_hmac_signature()` の API
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from nene2.security import verify_hmac_signature
|
|
94
|
+
|
|
95
|
+
verify_hmac_signature(
|
|
96
|
+
body: bytes, # 検証するバイト列
|
|
97
|
+
secret: str, # 共有シークレット
|
|
98
|
+
signature: str, # 検証対象の署名文字列(prefix 込み可)
|
|
99
|
+
*,
|
|
100
|
+
prefix: str = "", # 署名の prefix(例: "sha256=")
|
|
101
|
+
) -> bool
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`hmac.compare_digest()` で timing attack 対策済み。署名の比較に `==` を使わないこと。
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 5. BearerTokenMiddleware との使い分け
|
|
109
|
+
|
|
110
|
+
| パターン | 認証方法 | nene2 サポート |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| API クライアント認証 | `Authorization: Bearer <token>` | `BearerTokenMiddleware` |
|
|
113
|
+
| Webhook 署名検証 | リクエストボディ + シークレット | `verify_hmac_signature()` |
|
|
114
|
+
|
|
115
|
+
Webhook エンドポイントは `BearerTokenMiddleware` の `exclude_paths` に加えて、
|
|
116
|
+
自前の署名検証を行う。ミドルウェアでは raw body を読む関係で `BearerTokenMiddleware` は使用できない。
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from nene2.middleware import BearerTokenMiddleware
|
|
120
|
+
|
|
121
|
+
app.add_middleware(
|
|
122
|
+
BearerTokenMiddleware,
|
|
123
|
+
verifier=token_verifier,
|
|
124
|
+
exclude_paths=["/webhooks/"], # Webhook エンドポイントを除外
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 6. テスト
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import hashlib
|
|
134
|
+
import hmac
|
|
135
|
+
|
|
136
|
+
def make_github_sig(body: bytes, secret: str) -> str:
|
|
137
|
+
return "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
138
|
+
|
|
139
|
+
def test_webhook_valid() -> None:
|
|
140
|
+
payload = b'{"action": "opened"}'
|
|
141
|
+
r = client.post(
|
|
142
|
+
"/webhooks/github",
|
|
143
|
+
content=payload,
|
|
144
|
+
headers={
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"X-Hub-Signature-256": make_github_sig(payload, "your-secret-key"),
|
|
147
|
+
"X-GitHub-Event": "issues",
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
assert r.status_code == 200
|
|
151
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""TTL 付きインメモリキャッシュ."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class _Entry[V]:
|
|
9
|
+
value: V
|
|
10
|
+
expires_at: float
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TtlCache[V]:
|
|
15
|
+
"""TTL 付きインメモリキャッシュ。
|
|
16
|
+
|
|
17
|
+
asyncio コンテキストでの利用を想定。Python GIL により dict 操作は
|
|
18
|
+
アトミックで安全だが、get-then-set のような複合操作は排他しない。
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
ttl_seconds: キャッシュエントリの生存時間(秒)。
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
cache: TtlCache[dict[str, object]] = TtlCache(ttl_seconds=60.0)
|
|
25
|
+
cache.set("key", {"data": 42})
|
|
26
|
+
value = cache.get("key")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
ttl_seconds: float
|
|
30
|
+
_store: dict[str, _Entry[V]] = field(default_factory=dict, init=False, repr=False)
|
|
31
|
+
|
|
32
|
+
def get(self, key: str) -> V | None:
|
|
33
|
+
"""キーに対応する値を返す。TTL 切れの場合は None を返してエントリを削除する。"""
|
|
34
|
+
entry = self._store.get(key)
|
|
35
|
+
if entry is None:
|
|
36
|
+
return None
|
|
37
|
+
if time.monotonic() > entry.expires_at:
|
|
38
|
+
del self._store[key]
|
|
39
|
+
return None
|
|
40
|
+
return entry.value
|
|
41
|
+
|
|
42
|
+
def set(self, key: str, value: V) -> None:
|
|
43
|
+
"""キーと値を TTL 付きで格納する。"""
|
|
44
|
+
self._store[key] = _Entry(value=value, expires_at=time.monotonic() + self.ttl_seconds)
|
|
45
|
+
|
|
46
|
+
def delete(self, key: str) -> None:
|
|
47
|
+
"""キーに対応するエントリを削除する。存在しなくても例外は発生しない。"""
|
|
48
|
+
self._store.pop(key, None)
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None:
|
|
51
|
+
"""すべてのエントリを削除する。"""
|
|
52
|
+
self._store.clear()
|
|
53
|
+
|
|
54
|
+
def size(self) -> int:
|
|
55
|
+
"""TTL 切れを除いた有効なエントリ数を返す。"""
|
|
56
|
+
now = time.monotonic()
|
|
57
|
+
return sum(1 for e in self._store.values() if e.expires_at > now)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for nene2.cache.TtlCache."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from nene2.cache import TtlCache
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_set_and_get_returns_value() -> None:
|
|
11
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
12
|
+
cache.set("key", {"data": 42})
|
|
13
|
+
assert cache.get("key") == {"data": 42}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_get_missing_key_returns_none() -> None:
|
|
17
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
18
|
+
assert cache.get("nonexistent") is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_expired_entry_returns_none() -> None:
|
|
22
|
+
cache = TtlCache(ttl_seconds=0.01)
|
|
23
|
+
cache.set("key", "value")
|
|
24
|
+
time.sleep(0.02)
|
|
25
|
+
assert cache.get("key") is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_expired_entry_is_removed_from_store() -> None:
|
|
29
|
+
cache = TtlCache(ttl_seconds=0.01)
|
|
30
|
+
cache.set("key", "value")
|
|
31
|
+
time.sleep(0.02)
|
|
32
|
+
cache.get("key") # TTL 切れでエントリ削除
|
|
33
|
+
assert cache.size() == 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_delete_removes_entry() -> None:
|
|
37
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
38
|
+
cache.set("key", "value")
|
|
39
|
+
cache.delete("key")
|
|
40
|
+
assert cache.get("key") is None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_delete_missing_key_does_not_raise() -> None:
|
|
44
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
45
|
+
cache.delete("nonexistent") # should not raise
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_clear_removes_all_entries() -> None:
|
|
49
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
50
|
+
cache.set("a", 1)
|
|
51
|
+
cache.set("b", 2)
|
|
52
|
+
cache.clear()
|
|
53
|
+
assert cache.size() == 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_size_excludes_expired_entries() -> None:
|
|
57
|
+
cache = TtlCache(ttl_seconds=0.01)
|
|
58
|
+
cache.set("expired", "value")
|
|
59
|
+
time.sleep(0.02)
|
|
60
|
+
cache.set("fresh", "value")
|
|
61
|
+
assert cache.size() == 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_overwrite_resets_ttl() -> None:
|
|
65
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
66
|
+
cache.set("key", "old")
|
|
67
|
+
cache.set("key", "new")
|
|
68
|
+
assert cache.get("key") == "new"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("value", [None, 0, "", [], {}, False])
|
|
72
|
+
def test_falsy_values_are_stored_correctly(value: object) -> None:
|
|
73
|
+
cache = TtlCache(ttl_seconds=60.0)
|
|
74
|
+
cache.set("key", value)
|
|
75
|
+
assert cache.get("key") == value
|
|
76
|
+
assert cache.size() == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|