nene2-python 1.8.20__tar.gz → 1.8.22__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.20 → nene2_python-1.8.22}/CHANGELOG.md +14 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/CLAUDE.md +29 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/PKG-INFO +1 -1
- nene2_python-1.8.22/docs/field-trials/2026-05-field-trial-73.md +69 -0
- nene2_python-1.8.22/docs/field-trials/2026-05-field-trial-74.md +76 -0
- nene2_python-1.8.22/docs/field-trials/2026-05-field-trial-75.md +128 -0
- nene2_python-1.8.22/docs/field-trials/2026-05-field-trial-76.md +152 -0
- nene2_python-1.8.22/docs/how-to/middleware-stack.md +111 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/pyproject.toml +1 -1
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/__init__.py +2 -0
- nene2_python-1.8.22/src/nene2/middleware/setup.py +142 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/use_case/__init__.py +3 -1
- nene2_python-1.8.22/tests/nene2/middleware/test_setup_middlewares.py +124 -0
- nene2_python-1.8.22/tests/nene2/use_case/test_run_in_threadpool.py +61 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/uv.lock +1 -1
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.env.example +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.gitignore +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/AGENTS.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/Dockerfile +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/LICENSE +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/README.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/alembic/README +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/alembic/env.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/alembic.ini +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/compose.yaml +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/de/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/fr/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/reference/api.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/roadmap.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/todo/current.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/zh/index.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/package-lock.json +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/package.json +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/__main__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/app.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/mcp.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/schema.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.20 → nene2_python-1.8.22}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.21] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT75 フィールドトライアル — ミドルウェアスタック順序問題の発見と根本解決。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `setup_middlewares(app, ...)` ユーティリティ関数を追加 (#320 #321) (FT75)
|
|
14
|
+
— 全ミドルウェアを正しい順序(RequestId 最外側・ErrorHandler 最内側)で一括登録し、
|
|
15
|
+
エラーレスポンスにも X-Request-Id とセキュリティヘッダーが確実に付与される
|
|
16
|
+
- `docs/how-to/middleware-stack.md` — ミドルウェア順序の解説ガイドを追加
|
|
17
|
+
- CLAUDE.md セクション 8 に推奨 `add_middleware` 順序を追記
|
|
18
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-75.md` (FT75)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
8
22
|
## [1.8.20] — 2026-05-20
|
|
9
23
|
|
|
10
24
|
FT72 フィールドトライアル — DatabaseIntegrityException + ErrorHandlerMiddleware.install() 改善。
|
|
@@ -233,6 +233,35 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
|
|
|
233
233
|
- `nene2.http.problem_details_response()` で RFC 9457 エラー応答
|
|
234
234
|
- `nene2.http.PaginationQueryParser` でページネーション
|
|
235
235
|
|
|
236
|
+
### ミドルウェアスタック順序(重要)
|
|
237
|
+
|
|
238
|
+
`app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
|
|
239
|
+
直感と逆なので注意 — 「外側に置きたいものを後から追加する」。
|
|
240
|
+
|
|
241
|
+
**推奨 `add_middleware` 呼び出し順**(最初が最内側・最後が最外側):
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
# ✅ 正しい順序
|
|
245
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最内側: ハンドラー例外を捕捉
|
|
246
|
+
app.add_middleware(RequestLoggingMiddleware) # ↑
|
|
247
|
+
app.add_middleware(ThrottleMiddleware, ...) # |
|
|
248
|
+
app.add_middleware(RequestSizeLimitMiddleware, ...) # |
|
|
249
|
+
app.add_middleware(SecurityHeadersMiddleware) # ↓ 全レスポンスにヘッダー付与
|
|
250
|
+
app.add_middleware(RequestIdMiddleware) # 最外側: 全レスポンスに X-Request-Id 付与
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# ❌ よくある間違い — ErrorHandler を最後(最外側)に追加
|
|
255
|
+
app.add_middleware(RequestIdMiddleware)
|
|
256
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最外側にすると...
|
|
257
|
+
# → 500 エラーに X-Request-Id が付かない(内側のミドルウェアをバイパスするため)
|
|
258
|
+
# → 500 エラーにセキュリティヘッダーが付かない
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`ErrorHandlerMiddleware` が例外を捕捉して新しい Response を返すとき、
|
|
262
|
+
それより内側のミドルウェアはバイパスされる(Starlette の `BaseHTTPMiddleware` の仕様)。
|
|
263
|
+
`RequestIdMiddleware` と `SecurityHeadersMiddleware` は必ず **ErrorHandler より外側** に置くこと。
|
|
264
|
+
|
|
236
265
|
### REST 規約
|
|
237
266
|
- リソース名は複数形: `/notes`, `/tags`
|
|
238
267
|
- ID はパスパラメータ: `/notes/{note_id}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.22
|
|
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,69 @@
|
|
|
1
|
+
# FT73: PaginationQueryParser.parse() 静的メソッド実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: `PaginationQueryParser.parse(Request)` レガシーパターンと `ErrorHandlerMiddleware.install()` の連携確認
|
|
5
|
+
**バージョン**: v1.8.20
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft73-pagination-parse/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
`PaginationQueryParser.parse(request)` 静的メソッド(`Depends()` を使わないパターン)を検証した。
|
|
13
|
+
カスタム `default_limit` / `max_limit` の動作と、
|
|
14
|
+
v1.8.20 で追加した `ErrorHandlerMiddleware.install()` との連携も確認した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
- `GET /items` — `PaginationQueryParser.parse(request)` でデフォルト (limit=20, max=100)
|
|
21
|
+
- `GET /items/custom` — `parse(request, default_limit=5, max_limit=10)` でカスタム値
|
|
22
|
+
- `ErrorHandlerMiddleware.install(app)` を使用(v1.8.20 の新機能)
|
|
23
|
+
- `ValidationException` による 422 を nene2 Problem Details 形式で返す
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## テスト結果
|
|
28
|
+
|
|
29
|
+
**11/11 passed**
|
|
30
|
+
|
|
31
|
+
| テスト | 結果 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `test_default_pagination_returns_20_items` | PASSED |
|
|
34
|
+
| `test_custom_limit_and_offset` | PASSED |
|
|
35
|
+
| `test_limit_too_large_returns_422` | PASSED |
|
|
36
|
+
| `test_limit_zero_returns_422` | PASSED |
|
|
37
|
+
| `test_negative_offset_returns_422` | PASSED |
|
|
38
|
+
| `test_non_integer_limit_returns_422` | PASSED |
|
|
39
|
+
| `test_non_integer_offset_returns_422` | PASSED |
|
|
40
|
+
| `test_custom_default_limit_applied` | PASSED |
|
|
41
|
+
| `test_custom_max_limit_enforced` | PASSED |
|
|
42
|
+
| `test_custom_max_limit_at_boundary` | PASSED |
|
|
43
|
+
| `test_last_page_returns_remaining_items` | PASSED |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Friction Points
|
|
48
|
+
|
|
49
|
+
なし。
|
|
50
|
+
|
|
51
|
+
**特筆点**:
|
|
52
|
+
- `PaginationQueryParser.parse()` は `ValidationException` を raise するため、
|
|
53
|
+
`ErrorHandlerMiddleware.install(app)` と組み合わせると
|
|
54
|
+
非整数値・範囲外の入力が自動的に 422 nene2 Problem Details で返る。
|
|
55
|
+
- `parse()` が返す `PaginationQuery` (named dataclass) は `limit` / `offset` を保持し、
|
|
56
|
+
`Depends()` パターンの `PaginationQueryParser` インスタンスと同じインターフェースで使える。
|
|
57
|
+
- `default_limit` / `max_limit` のカスタマイズが `parse()` の引数で完結するため、
|
|
58
|
+
ルートごとに異なるページネーション制限を設定しやすい。
|
|
59
|
+
- `ErrorHandlerMiddleware.install(app)` が v1.8.20 で正式追加され、
|
|
60
|
+
`ValidationException` の 422 フォーマット統一が一行で完了するようになった。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 結論
|
|
65
|
+
|
|
66
|
+
`PaginationQueryParser.parse()` は `Depends()` パターンが使えないシナリオ
|
|
67
|
+
(例: ミドルウェアや `WebSocket` ハンドラー内)で有効な代替手段。
|
|
68
|
+
`default_limit` / `max_limit` のカスタマイズと `ValidationException` の自動 422 変換が
|
|
69
|
+
`ErrorHandlerMiddleware.install()` との組み合わせで自然に機能する。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# FT74: カスタム HealthCheckProtocol 実装の実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: ユーザー定義 `HealthCheckProtocol` / `AsyncHealthCheckProtocol` 実装と `CompositeHealthCheck` の連携
|
|
5
|
+
**バージョン**: v1.8.20
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft74-custom-health/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
ユーザーが `HealthCheckProtocol` を実装する実際のシナリオ(メモリ・DB・外部サービス)を
|
|
13
|
+
`CompositeHealthCheck` / `AsyncCompositeHealthCheck` に組み合わせて動作を検証した。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装内容
|
|
18
|
+
|
|
19
|
+
### カスタム同期ヘルスチェック
|
|
20
|
+
- `AlwaysOkCheck` — 常に ok を返すベースライン
|
|
21
|
+
- `ToggleableCheck` — テストで on/off できるトグル可能なチェック
|
|
22
|
+
- `MemoryCheck` — 閾値ベースのメモリ使用量チェック(psutil なしでシミュレート)
|
|
23
|
+
|
|
24
|
+
### カスタム非同期ヘルスチェック
|
|
25
|
+
- `AsyncAlwaysOkCheck` — 常に ok を返す非同期版
|
|
26
|
+
- `AsyncToggleableCheck` — 非同期版トグルチェック
|
|
27
|
+
|
|
28
|
+
### エンドポイント
|
|
29
|
+
- `GET /health` — `CompositeHealthCheck` (同期)
|
|
30
|
+
- `GET /health/async` — `AsyncCompositeHealthCheck` (非同期)
|
|
31
|
+
- `create_app(db_healthy, cache_healthy, memory_usage_pct)` でテストシナリオを切り替え
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## テスト結果
|
|
36
|
+
|
|
37
|
+
**13/13 passed**
|
|
38
|
+
|
|
39
|
+
| テスト | 結果 | 種別 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `test_always_ok_check_returns_ok` | PASSED | 単体 |
|
|
42
|
+
| `test_toggleable_check_returns_error_when_unhealthy` | PASSED | 単体 |
|
|
43
|
+
| `test_memory_check_ok_under_threshold` | PASSED | 単体 |
|
|
44
|
+
| `test_memory_check_error_over_threshold` | PASSED | 単体 |
|
|
45
|
+
| `test_composite_ok_when_all_checks_pass` | PASSED | 単体 |
|
|
46
|
+
| `test_composite_error_when_any_check_fails` | PASSED | 単体 |
|
|
47
|
+
| `test_health_status_http_code_200_when_ok` | PASSED | 単体 |
|
|
48
|
+
| `test_health_status_http_code_503_when_error` | PASSED | 単体 |
|
|
49
|
+
| `test_sync_health_endpoint_returns_200_when_all_ok` | PASSED | HTTP 統合 |
|
|
50
|
+
| `test_sync_health_endpoint_returns_503_when_db_down` | PASSED | HTTP 統合 |
|
|
51
|
+
| `test_sync_health_endpoint_returns_503_when_memory_high` | PASSED | HTTP 統合 |
|
|
52
|
+
| `test_async_health_endpoint_returns_200_when_all_ok` | PASSED | HTTP 統合 |
|
|
53
|
+
| `test_async_health_endpoint_returns_503_when_cache_down` | PASSED | HTTP 統合 |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Friction Points
|
|
58
|
+
|
|
59
|
+
なし。
|
|
60
|
+
|
|
61
|
+
**特筆点**:
|
|
62
|
+
- `HealthCheckProtocol` / `AsyncHealthCheckProtocol` はどちらも `@runtime_checkable Protocol` なので、
|
|
63
|
+
特定の基底クラスを継承せずとも `check()` メソッドを実装するだけで準拠できる。
|
|
64
|
+
- `CompositeHealthCheck` は各チェックの `checks` dict をフラットマージするため、
|
|
65
|
+
複数チェックが同一キーを持つと後のチェックが上書きする。キーの命名が重要。
|
|
66
|
+
- `HealthStatus.http_status_code` は 200 / 503 を自動で返すため、
|
|
67
|
+
HTTP ハンドラーで `status_code=status.http_status_code` とするだけで正しいステータスコードになる。
|
|
68
|
+
- `create_app(db_healthy=False)` の DI パターンで HTTP テストにおける「障害シミュレーション」が簡潔に書ける。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 結論
|
|
73
|
+
|
|
74
|
+
`HealthCheckProtocol` の実装は `check() -> HealthStatus` を持つだけで完了する。
|
|
75
|
+
`CompositeHealthCheck` がフラットマージすることを把握した上で、
|
|
76
|
+
各チェックのキー命名を一意にすれば、複数チェックの組み合わせは完全に直感的に動作する。
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# FT75: ミドルウェアスタック順序依存性の実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: Starlette LIFO ミドルウェア順序の落とし穴 — エラーレスポンスにヘッダーが付かない問題
|
|
5
|
+
**バージョン**: v1.8.20
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft75-middleware-order/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
nene2 が提供する 6 つのミドルウェアを組み合わせた際の順序依存バグを検証した。
|
|
13
|
+
「ErrorHandler を最外側に」という直感が **間違い** であることを実際のテストで実証し、
|
|
14
|
+
全レスポンスに X-Request-Id とセキュリティヘッダーを付与するための正しい順序を確認した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 発見した問題
|
|
19
|
+
|
|
20
|
+
### Starlette BaseHTTPMiddleware の動作原理
|
|
21
|
+
|
|
22
|
+
`app.add_middleware(X)` は **LIFO**(後から追加したものが外側になる)で積まれる。
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
app.add_middleware(A) # 内側
|
|
26
|
+
app.add_middleware(B) # 外側
|
|
27
|
+
# スタック: B(A(Router))
|
|
28
|
+
# リクエスト: B → A → Router
|
|
29
|
+
# レスポンス: Router → A → B
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 落とし穴: ErrorHandler を最外側にすると何が起きるか
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# 「直感」による書き方
|
|
36
|
+
app.add_middleware(RequestIdMiddleware) # 内側
|
|
37
|
+
app.add_middleware(SecurityHeadersMiddleware) # 内側
|
|
38
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最外側(最後に追加)
|
|
39
|
+
|
|
40
|
+
# スタック: ErrorHandler(SecurityHeaders(RequestId(Router)))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
ハンドラーが例外を raise すると:
|
|
44
|
+
1. `ErrorHandlerMiddleware.dispatch` が例外を捕捉
|
|
45
|
+
2. `problem_details_response(...)` で **新しい Response を直接 return**
|
|
46
|
+
3. この Response は内側の `SecurityHeaders` も `RequestId` も **通過しない**
|
|
47
|
+
4. 結果: **500 エラーに X-Request-Id もセキュリティヘッダーも付かない**
|
|
48
|
+
|
|
49
|
+
### 正しい順序
|
|
50
|
+
|
|
51
|
+
ErrorHandler を **最内側** に置き、RequestId と SecurityHeaders を **外側** に置く。
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# 正しい書き方(最初に add するものが最内側)
|
|
55
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最内側
|
|
56
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
57
|
+
app.add_middleware(ThrottleMiddleware, ...)
|
|
58
|
+
app.add_middleware(RequestSizeLimitMiddleware, ...)
|
|
59
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
60
|
+
app.add_middleware(RequestIdMiddleware) # 最外側
|
|
61
|
+
|
|
62
|
+
# スタック: RequestId(SecurityHeaders(SizeLimit(Throttle(RequestLogging(ErrorHandler(Router))))))
|
|
63
|
+
# 全レスポンス(エラー含む)が SecurityHeaders と RequestId を通過する ✓
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## テスト結果
|
|
69
|
+
|
|
70
|
+
**10/10 passed**
|
|
71
|
+
|
|
72
|
+
| テスト | 結果 | 観察内容 |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `test_correct_order_500_is_problem_details` | PASSED | ErrorHandler は内側でも 500 を捕捉できる |
|
|
75
|
+
| `test_correct_order_500_has_request_id` | PASSED | RequestId が外側 → 500 にも付く |
|
|
76
|
+
| `test_correct_order_500_has_security_headers` | PASSED | SecurityHeaders が外側 → 500 にも付く |
|
|
77
|
+
| `test_correct_order_413_is_problem_details` | PASSED | SizeLimitMiddleware は内部で直接 problem_details_response を返す |
|
|
78
|
+
| `test_correct_order_413_has_request_id` | PASSED | 413 にも X-Request-Id が付く |
|
|
79
|
+
| `test_correct_order_413_has_security_headers` | PASSED | 413 にもセキュリティヘッダーが付く |
|
|
80
|
+
| `test_naive_order_500_missing_request_id` | PASSED | 直感的順序だと 500 に X-Request-Id が**付かない**ことを実証 |
|
|
81
|
+
| `test_naive_order_500_missing_security_headers` | PASSED | 直感的順序だと 500 にセキュリティヘッダーが**付かない**ことを実証 |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Friction Points
|
|
86
|
+
|
|
87
|
+
### 🔴 重大: ミドルウェア推奨順序がドキュメント化されていない
|
|
88
|
+
|
|
89
|
+
`add_middleware` の呼び出し順序について nene2 のドキュメントに推奨順序が存在しない。
|
|
90
|
+
Starlette の LIFO 動作は非直感的であり、「ErrorHandler が最外側にあるべき」という誤解を招きやすい。
|
|
91
|
+
|
|
92
|
+
実際の影響:
|
|
93
|
+
- **ErrorHandler を最外側に置く(最後に add する)と** 500 エラーに X-Request-Id が付かない
|
|
94
|
+
- **Security audit で「エラーレスポンスにセキュリティヘッダーがない」と指摘される**
|
|
95
|
+
- 本番環境で気づかずに運用してしまう可能性が高い
|
|
96
|
+
|
|
97
|
+
**推奨順序(コメント付き)をドキュメントに追加すべき。**
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 使用感(主観評価)
|
|
102
|
+
|
|
103
|
+
**直感性: ★★☆☆☆**
|
|
104
|
+
LIFO の仕組みを知っていても、「外側に置きたいものを後から add する」という逆転発想は
|
|
105
|
+
毎回確認しないと間違える。Express.js でも同じ罠があり、FastAPI/Starlette ユーザーが
|
|
106
|
+
最も頻繁にハマる問題のひとつ。
|
|
107
|
+
|
|
108
|
+
**実害の深刻さ: ★★★★★**
|
|
109
|
+
単に動かない(すぐ気づく)ではなく、**動くが一部の非機能要件が欠落する**パターン。
|
|
110
|
+
Security headers がエラーページだけ欠落していても CI は通るし、
|
|
111
|
+
X-Request-Id がエラーレスポンスにないことはログ追跡するまで気づかない。
|
|
112
|
+
|
|
113
|
+
**修正のしやすさ: ★★★★★**
|
|
114
|
+
順序を正しくするだけなので、原因がわかれば修正は 1 分。
|
|
115
|
+
問題は「原因に気づく」のが遅いこと。
|
|
116
|
+
|
|
117
|
+
**フレームワーク側で改善できること**:
|
|
118
|
+
CLAUDE.md や how-to ガイドに推奨スタック順序を明記するだけで解決できる。
|
|
119
|
+
ミドルウェアを `ErrorHandlerMiddleware.install()` のように一括登録する
|
|
120
|
+
`setup_middlewares(app)` ユーティリティがあると事故を防げる。
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 結論
|
|
125
|
+
|
|
126
|
+
ミドルウェア順序のバグは **動作するが静かに壊れている** 類の問題で、
|
|
127
|
+
実際の本番事故に直結しやすい。正しい順序(ErrorHandler 最内側・RequestId 最外側)を
|
|
128
|
+
nene2 の公式ドキュメントと CLAUDE.md に追記することを強く推奨する。
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# FT76: async ハンドラー + sync SQLAlchemy のイベントループブロッキング
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: FastAPI の async def ハンドラー内で同期 DB 処理を呼ぶとどうなるか
|
|
5
|
+
**バージョン**: v1.8.21
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft76-async-sync-db/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
FastAPI は `async def` と `def` を混在できるが、同期処理の扱いが根本的に異なる。
|
|
13
|
+
nene2 の DB 層(`SqlAlchemyQueryExecutor` / `SqlAlchemyTransactionManager`)は同期実装のため、
|
|
14
|
+
`async def` ハンドラー内から直接呼ぶとイベントループをブロックする。
|
|
15
|
+
このFTでは3パターンの挙動を実測し、nene2 として何を提供すべきかを検証した。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## テスト対象の3パターン
|
|
20
|
+
|
|
21
|
+
| パターン | 実装 | 動作 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| A | `async def` 内で `time.sleep()` / 同期DB直呼び | **イベントループをブロック** |
|
|
24
|
+
| B | `async def` 内で `run_in_executor()` 経由 | スレッドプールにオフロード(安全) |
|
|
25
|
+
| C | `def` ハンドラー(同期) | FastAPI が自動でスレッドプールに退避(安全) |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 発見した問題
|
|
30
|
+
|
|
31
|
+
### 問題1: async ハンドラー + sync DB = サイレントブロッキング
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# ❌ よくある間違い
|
|
35
|
+
@app.post("/tasks")
|
|
36
|
+
async def create_task(body: TaskBody) -> JSONResponse:
|
|
37
|
+
result = _create_task_sync(body.title) # time.sleep / Session(...) など
|
|
38
|
+
return JSONResponse(result, status_code=201)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`async def` 内で `time.sleep()` や同期 `Session` を呼ぶと、実行中は asyncio のイベントループが
|
|
42
|
+
完全に停止する。他のリクエストはそのリクエストが完了するまで待たされる。
|
|
43
|
+
|
|
44
|
+
**特に危険なのは「少量のデータなら問題が出ない」点**。
|
|
45
|
+
開発中 / ステージング環境では顕在化せず、本番で同時アクセスが増えてから遅延が爆発する。
|
|
46
|
+
|
|
47
|
+
### 問題2: nene2 に async DB 層がない
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# v1.8.21 時点: 存在しない
|
|
51
|
+
from nene2.database import AsyncSqlAlchemyQueryExecutor # ImportError
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`SqlAlchemyQueryExecutor.fetch_all()` / `SqlAlchemyTransactionManager.begin()` は
|
|
55
|
+
すべて同期実装。async def ハンドラーと組み合わせるには自前で `run_in_executor` を書く必要がある。
|
|
56
|
+
|
|
57
|
+
### 問題3: run_in_executor パターンが冗長で発見しにくい
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# ✅ 正しいが、毎回これを書くのは辛い
|
|
61
|
+
@app.post("/tasks")
|
|
62
|
+
async def create_task(body: TaskBody) -> JSONResponse:
|
|
63
|
+
loop = asyncio.get_event_loop()
|
|
64
|
+
result = await loop.run_in_executor(None, _create_task_sync, body.title, body.simulate_ms)
|
|
65
|
+
return JSONResponse(result, status_code=201)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `asyncio.get_event_loop()` は Python 3.10+ で deprecation warning を出すことがある
|
|
69
|
+
- `None` の意味(default executor = ThreadPoolExecutor)が自明でない
|
|
70
|
+
- 複数の引数を渡すには `functools.partial` か lambda が必要になりさらに冗長になる
|
|
71
|
+
|
|
72
|
+
### 問題4: `def` ハンドラーが実は最もシンプルで安全
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# ✅ sync def は FastAPI が自動でスレッドプールに退避する
|
|
76
|
+
@app.post("/tasks")
|
|
77
|
+
def create_task(body: TaskBody) -> JSONResponse:
|
|
78
|
+
result = _create_task_sync(body.title) # 安全
|
|
79
|
+
return JSONResponse(result, status_code=201)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`async def` を書く必然性がないなら `def` を使うべきだが、
|
|
83
|
+
「FastAPI = async」というイメージから全ハンドラーを `async def` にするユーザーが多い。
|
|
84
|
+
nene2 のドキュメントがこれを明示していない。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## テスト結果(全12件パス)
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
test_sync_in_async_creates_task PASSED # パターンA: 機能はする(ブロッキングだが)
|
|
92
|
+
test_executor_creates_task PASSED # パターンB: executor 経由
|
|
93
|
+
test_sync_def_creates_task PASSED # パターンC: sync def
|
|
94
|
+
test_list_tasks PASSED
|
|
95
|
+
test_sync_def_does_not_block_event_loop PASSED # イベントループが自由であることを確認
|
|
96
|
+
test_sync_in_async_blocks_when_slow PASSED # 200ms スリープがそのまま待機時間になる
|
|
97
|
+
test_executor_avoids_blocking PASSED
|
|
98
|
+
test_middlewares_work_with_all_patterns PASSED # setup_middlewares() は全パターンで動作
|
|
99
|
+
test_422_on_invalid_title_length PASSED
|
|
100
|
+
test_422_on_negative_simulate_ms PASSED
|
|
101
|
+
test_no_async_db_layer_in_nene2 PASSED # 非同期DB層が存在しないことを確認
|
|
102
|
+
test_threadpool_pattern_is_verbose PASSED # 冗長さのドキュメンタリーテスト
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 摩擦ポイント一覧
|
|
108
|
+
|
|
109
|
+
| ID | 内容 | 深刻度 |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| F76-1 | nene2 に async DB 層がなく、async ハンドラーでは run_in_executor が必要 | 高 |
|
|
112
|
+
| F76-2 | async def + sync DB のブロッキングがサイレント(警告なし) | 高 |
|
|
113
|
+
| F76-3 | run_in_executor の書き方が冗長で、正しい引数渡しが分かりにくい | 中 |
|
|
114
|
+
| F76-4 | ドキュメントが async def vs def の使い分けを説明していない | 中 |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 使用感(主観評価)
|
|
119
|
+
|
|
120
|
+
### 直感性 ★★☆☆☆
|
|
121
|
+
|
|
122
|
+
「FastAPI を使っているので全ハンドラーを async def にする」のは自然な発想。
|
|
123
|
+
しかし nene2 の DB 層は同期なので、これが罠になる。
|
|
124
|
+
`async def` で書いたコードが正常に動き、テストも全部通る — でも本番では爆発する。
|
|
125
|
+
「動いているから正しい」と思ってしまう構造が非常にやっかいで直感に反する。
|
|
126
|
+
|
|
127
|
+
### 実害の深刻さ ★★★★☆
|
|
128
|
+
|
|
129
|
+
本番環境で同時アクセスが増えてからはじめて顕在化する。
|
|
130
|
+
レスポンスが遅い → サーバーがダウン、という流れをたどる典型的なパフォーマンス地雷。
|
|
131
|
+
「なぜか重い」「スケールしない」という症状で現れるため、根本原因の特定も遅れやすい。
|
|
132
|
+
|
|
133
|
+
### 修正のしやすさ ★★☆☆☆
|
|
134
|
+
|
|
135
|
+
`run_in_executor` パターンを知っていれば直せるが、記述が冗長で毎回ハンドラーに書くのはつらい。
|
|
136
|
+
根本解決(async DB 層の実装)はSQLAlchemy async対応が必要で、工数が大きい。
|
|
137
|
+
`def` ハンドラーへの移行が最もシンプルだが、`async` 依存のロジックが混在していると難しい。
|
|
138
|
+
|
|
139
|
+
### 総合コメント
|
|
140
|
+
|
|
141
|
+
FastAPI + SQLAlchemy の組み合わせは最も多いユースケースの一つ。
|
|
142
|
+
「nene2 を使えばすぐ始められる」が「パフォーマンス問題で詰まる」という流れは
|
|
143
|
+
ユーザー離れを引き起こす可能性がある。
|
|
144
|
+
`def` vs `async def` の使い分けガイドラインと、将来的な async DB サポートが必要。
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 推奨アクション
|
|
149
|
+
|
|
150
|
+
1. **Issue**: `async def` ハンドラー内での `SqlAlchemy` 同期呼び出し警告をドキュメントに追加
|
|
151
|
+
2. **Issue**: `run_in_threadpool()` ヘルパー(または `asyncify()` パターン)の提供
|
|
152
|
+
3. **将来**: `AsyncSqlAlchemyQueryExecutor` の実装(SQLAlchemy 2.0 async 対応)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# How-To: ミドルウェアスタックの正しい設定
|
|
2
|
+
|
|
3
|
+
## 結論(TL;DR)
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# この順序で add_middleware を呼ぶ
|
|
7
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最内側
|
|
8
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
9
|
+
app.add_middleware(ThrottleMiddleware, limit=100, window=60)
|
|
10
|
+
app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
|
|
11
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
12
|
+
app.add_middleware(RequestIdMiddleware) # 最外側
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## なぜこの順序なのか
|
|
18
|
+
|
|
19
|
+
### Starlette の LIFO ルール
|
|
20
|
+
|
|
21
|
+
`app.add_middleware()` は **後から追加したものが外側**(LIFO)になる。
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
add_middleware(A) → B(A(Router))
|
|
25
|
+
add_middleware(B)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
リクエストは外側から内側へ(B → A → Router)、
|
|
29
|
+
レスポンスは内側から外側へ(Router → A → B)流れる。
|
|
30
|
+
|
|
31
|
+
### ErrorHandler を最外側にすると何が壊れるか
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# ❌ 間違い
|
|
35
|
+
app.add_middleware(RequestIdMiddleware)
|
|
36
|
+
app.add_middleware(ErrorHandlerMiddleware) # 最外側
|
|
37
|
+
# スタック: ErrorHandler(RequestId(Router))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
ハンドラーが例外を raise したとき:
|
|
41
|
+
|
|
42
|
+
1. `ErrorHandlerMiddleware.dispatch` が例外を捕捉
|
|
43
|
+
2. `problem_details_response(...)` で **新しい Response を直接 return**
|
|
44
|
+
3. この Response は内側の `RequestId` ミドルウェアを **通過しない**
|
|
45
|
+
4. 結果: **500 エラーに `X-Request-Id` が付かない**
|
|
46
|
+
|
|
47
|
+
同じ理由で `SecurityHeadersMiddleware` が内側にあると、
|
|
48
|
+
エラーレスポンスにセキュリティヘッダーが付かない。
|
|
49
|
+
|
|
50
|
+
### 正しい順序のスタック図
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
RequestIdMiddleware ← 全レスポンス(200〜5xx)に X-Request-Id を付与
|
|
54
|
+
└─ SecurityHeadersMiddleware ← 全レスポンスにセキュリティヘッダーを付与
|
|
55
|
+
└─ RequestSizeLimitMiddleware ← 413 を直接返す(ErrorHandler 不要)
|
|
56
|
+
└─ ThrottleMiddleware ← 429 を直接返す(ErrorHandler 不要)
|
|
57
|
+
└─ RequestLoggingMiddleware
|
|
58
|
+
└─ ErrorHandlerMiddleware ← ハンドラー例外を 500 に変換
|
|
59
|
+
└─ Router (FastAPI handlers)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`RequestSizeLimitMiddleware` と `ThrottleMiddleware` は自身で `problem_details_response()` を
|
|
63
|
+
返すため、ErrorHandler の内外に置いても 413/429 の形式は変わらない。
|
|
64
|
+
ただし `X-Request-Id` が付くかどうかは `RequestIdMiddleware` の位置次第。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 使用しないミドルウェアがある場合
|
|
69
|
+
|
|
70
|
+
一部のミドルウェアを省略しても、残りの順序は同じルールに従う:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# ThrottleMiddleware と RequestLoggingMiddleware を省略した場合
|
|
74
|
+
app.add_middleware(ErrorHandlerMiddleware)
|
|
75
|
+
app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
|
|
76
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
77
|
+
app.add_middleware(RequestIdMiddleware)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ErrorHandlerMiddleware.install() を使う場合
|
|
83
|
+
|
|
84
|
+
`install()` は `add_middleware` と `add_exception_handler` をまとめて行うが、
|
|
85
|
+
他のミドルウェアとの順序設定は手動で行う必要がある:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# install() は最初に呼ぶ(最内側になる)
|
|
89
|
+
ErrorHandlerMiddleware.install(app) # 内側
|
|
90
|
+
|
|
91
|
+
# その後に他のミドルウェアを追加
|
|
92
|
+
app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
|
|
93
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
94
|
+
app.add_middleware(RequestIdMiddleware) # 外側
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## よくある質問
|
|
100
|
+
|
|
101
|
+
**Q: `RequestSizeLimitMiddleware` は ErrorHandler の内外どちらに置くべきか?**
|
|
102
|
+
|
|
103
|
+
A: 内外どちらでも機能するが、`RequestIdMiddleware` より内側にすることで
|
|
104
|
+
413 レスポンスにも `X-Request-Id` が付く。上記の推奨順序に従えばよい。
|
|
105
|
+
|
|
106
|
+
**Q: カスタムミドルウェアはどこに追加するか?**
|
|
107
|
+
|
|
108
|
+
A: そのミドルウェアの性質による:
|
|
109
|
+
- 全レスポンスに何かを追加したい → `RequestIdMiddleware` の直前(外側)
|
|
110
|
+
- ハンドラー例外をキャッチしたい → `ErrorHandlerMiddleware` の直後(内側)
|
|
111
|
+
- リクエストを早期拒否したい → `RequestSizeLimitMiddleware` や `ThrottleMiddleware` の近く
|
|
@@ -6,6 +6,7 @@ from .request_id import RequestIdMiddleware, get_request_id, request_id_var
|
|
|
6
6
|
from .request_logging import RequestLoggingMiddleware
|
|
7
7
|
from .request_size_limit import RequestSizeLimitMiddleware
|
|
8
8
|
from .security_headers import SecurityHeadersMiddleware
|
|
9
|
+
from .setup import setup_middlewares
|
|
9
10
|
from .throttle import ThrottleMiddleware
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"RequestLoggingMiddleware",
|
|
19
20
|
"RequestSizeLimitMiddleware",
|
|
20
21
|
"SecurityHeadersMiddleware",
|
|
22
|
+
"setup_middlewares",
|
|
21
23
|
"ThrottleMiddleware",
|
|
22
24
|
"request_id_var",
|
|
23
25
|
]
|