nene2-python 1.8.3__tar.gz → 1.8.4__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.3 → nene2_python-1.8.4}/CHANGELOG.md +16 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/PKG-INFO +1 -1
- nene2_python-1.8.4/docs/field-trials/2026-05-field-trial-33.md +111 -0
- nene2_python-1.8.4/docs/field-trials/2026-05-field-trial-34.md +119 -0
- nene2_python-1.8.4/docs/field-trials/2026-05-field-trial-35.md +90 -0
- nene2_python-1.8.4/docs/field-trials/2026-05-field-trial-36.md +92 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/configure-auth.md +49 -2
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/run-tests.md +17 -0
- nene2_python-1.8.4/docs/how-to/validation.md +101 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/pyproject.toml +1 -1
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/http/__init__.py +9 -1
- nene2_python-1.8.4/src/nene2/http/health.py +106 -0
- nene2_python-1.8.4/tests/nene2/http/test_health.py +139 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/uv.lock +1 -1
- nene2_python-1.8.3/src/nene2/http/health.py +0 -52
- nene2_python-1.8.3/tests/nene2/http/test_health.py +0 -66
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.env.example +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.gitignore +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/AGENTS.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/CLAUDE.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/Dockerfile +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/LICENSE +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/README.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/alembic/README +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/alembic/env.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/alembic.ini +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/compose.yaml +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/de/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/fr/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/reference/api.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/roadmap.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/todo/current.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/zh/index.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/package-lock.json +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/package.json +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/__main__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/app.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/mcp.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/schema.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.3 → nene2_python-1.8.4}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.4] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT33〜FT36 フィールドトライアル — バリデーション・DB整合性・混合認証・非同期ヘルスチェック改善。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `AsyncHealthCheckProtocol` — `async def check() -> HealthStatus` の Protocol (FT36)
|
|
14
|
+
- `AsyncCompositeHealthCheck` — 複数の非同期ヘルスチェックを `asyncio.gather` で並列実行して集約するクラス (FT36)
|
|
15
|
+
- `docs/how-to/validation.md` — `ValidationCode(StrEnum)` パターンと複数フィールドバリデーションの how-to ガイドを追加 (FT33)
|
|
16
|
+
- Field trial reports: `docs/field-trials/2026-05-field-trial-33.md` 〜 `docs/field-trials/2026-05-field-trial-36.md`
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `docs/how-to/run-tests.md` — インメモリ SQLite テスト用 `StaticPool` パターンを追記 (FT34)
|
|
20
|
+
- `docs/how-to/configure-auth.md` — AND / OR 条件の違いと `EitherOrAuthMiddleware` パターンを追記 (FT35)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
8
24
|
## [1.8.3] — 2026-05-20
|
|
9
25
|
|
|
10
26
|
FT31〜FT32 フィールドトライアル — HealthCheck・SecurityHeaders 改善。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.4
|
|
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,111 @@
|
|
|
1
|
+
# Field Trial 33: ValidationException カスタムエラーコード実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.3 時点
|
|
5
|
+
**テーマ**: `ValidationException` と `ValidationError.code` フィールドを活用した型安全なエラーコードの実運用パターン検証
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
ユーザー登録 API を題材に、`ValidationCode(StrEnum)` パターンでドメイン固有のバリデーションコードを定義し、
|
|
12
|
+
`ValidationException` / `ValidationError` と組み合わせて複数フィールドの検証エラーをクライアントに返すフローを検証した。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 実装内容
|
|
17
|
+
|
|
18
|
+
`/home/xi/docker/nene2-python-FT/ft33-validation-codes/` に以下を作成:
|
|
19
|
+
|
|
20
|
+
- **`app.py`** — `ValidationCode(StrEnum)` + `ValidationException` によるユーザー登録 API
|
|
21
|
+
- **`test_app.py`** — 正常系・各バリデーションエラー・複数エラー収集の動作テスト (5 件)
|
|
22
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (3 件)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
class ValidationCode(StrEnum):
|
|
26
|
+
REQUIRED = "required"
|
|
27
|
+
INVALID_FORMAT = "invalid_format"
|
|
28
|
+
TOO_SHORT = "too_short"
|
|
29
|
+
TOO_LONG = "too_long"
|
|
30
|
+
ALREADY_EXISTS = "already_exists"
|
|
31
|
+
OUT_OF_RANGE = "out_of_range"
|
|
32
|
+
|
|
33
|
+
def _validate_registration(body: RegisterBody) -> list[ValidationError]:
|
|
34
|
+
errors: list[ValidationError] = []
|
|
35
|
+
if len(body.username) < 3:
|
|
36
|
+
errors.append(ValidationError("username", "3文字以上必要です", ValidationCode.TOO_SHORT))
|
|
37
|
+
if "@" not in body.email:
|
|
38
|
+
errors.append(ValidationError("email", "有効なメールアドレスを入力してください", ValidationCode.INVALID_FORMAT))
|
|
39
|
+
if body.age < 0 or body.age > 150:
|
|
40
|
+
errors.append(ValidationError("age", "年齢は 0〜150 の範囲で入力してください", ValidationCode.OUT_OF_RANGE))
|
|
41
|
+
return errors
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**テスト結果**: 8 件全通過 ✅
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 摩擦点
|
|
49
|
+
|
|
50
|
+
### FP33-1: フレームワークが標準エラーコードを定義していない
|
|
51
|
+
|
|
52
|
+
**分類**: 設計通り(摩擦なし)
|
|
53
|
+
|
|
54
|
+
`required` / `invalid_format` / `too_short` / `too_long` 等のよく使うコードを
|
|
55
|
+
フレームワーク側が `ValidationCode` として提供していない。
|
|
56
|
+
各プロジェクトで自前定義が必要。
|
|
57
|
+
|
|
58
|
+
**判断**: ドメイン固有のコードはプロジェクトで定義するのが適切。フレームワークが網羅的に
|
|
59
|
+
定義すると過剰な依存・名前衝突・拡張困難になる。`StrEnum` パターンを how-to ドキュメントに
|
|
60
|
+
追記することで、新規プロジェクトの立ち上がりをサポートする。
|
|
61
|
+
|
|
62
|
+
**対応**: `docs/how-to/validation.md` への `StrEnum` パターン記載(ドキュメントのみ)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### FP33-2: `ValidationException.single()` の `code` パラメータにデフォルト値がない
|
|
67
|
+
|
|
68
|
+
**分類**: 既知の設計制約(摩擦なし)
|
|
69
|
+
|
|
70
|
+
`code` パラメータは必須。`"required"` 等よく使うコードでもデフォルト値なし。
|
|
71
|
+
|
|
72
|
+
**判断**: 明示的なコードが可読性・型安全性を高める設計上の意図。
|
|
73
|
+
デフォルト値をつけると「コードなし」での利用が増え、クライアント側でのエラーハンドリングが困難になる。
|
|
74
|
+
これは摩擦ではなく既知の制約として受け入れる。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### FP33-3: ネストフィールドのエラーパスに点記法ヘルパーがない
|
|
79
|
+
|
|
80
|
+
**分類**: 軽微な摩擦
|
|
81
|
+
|
|
82
|
+
`body.email` のようなネストパスを `ValidationError.field` に渡す際、正規化ヘルパーがない。
|
|
83
|
+
任意文字列として渡すのみ。
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
error = ValidationError(field="address.city", message="必須です", code="required")
|
|
87
|
+
# 正規化なし — 任意の文字列を受け入れる
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**判断**: ネストパスの表現方法(ドット区切り・スラッシュ・配列表記)はプロジェクトにより異なるため、
|
|
91
|
+
フレームワーク側でヘルパーを提供するメリットは小さい。
|
|
92
|
+
ドキュメントでの推奨パターン(ドット区切り)記載で対応。
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 所感
|
|
97
|
+
|
|
98
|
+
`ValidationCode(StrEnum)` パターンは極めて自然に機能する。
|
|
99
|
+
`ValidationError` コンストラクタに `StrEnum` 値をそのまま渡せるため、型安全性と
|
|
100
|
+
JSON シリアライズ(文字列として出力)が同時に確保される。複数フィールドのエラー収集も
|
|
101
|
+
リストに積み上げるだけで直感的。
|
|
102
|
+
|
|
103
|
+
フレームワーク側で追加の変更は不要。how-to ドキュメントへの `StrEnum` パターン追記のみ対応する。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 関連
|
|
108
|
+
|
|
109
|
+
- `nene2.validation.ValidationException`
|
|
110
|
+
- `nene2.validation.ValidationError`
|
|
111
|
+
- FT13 (ValidationException実運用, v1.6.0) の後継検証
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Field Trial 34: DatabaseIntegrityException + SimpleDomainHandler 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.3 時点
|
|
5
|
+
**テーマ**: UNIQUE 制約違反 → `DatabaseIntegrityException` → `SimpleDomainHandler` → 409 Problem Details のパイプライン実運用検証
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
SQLite UNIQUE 制約を持つ `users` テーブルに `SqlAlchemyQueryExecutor` でアクセスし、
|
|
12
|
+
重複 username の登録が自動的に `DatabaseIntegrityException` へ変換され、
|
|
13
|
+
`SimpleDomainHandler` が 409 Conflict Problem Details を返す完全なパイプラインを検証した。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装内容
|
|
18
|
+
|
|
19
|
+
`/home/xi/docker/nene2-python-FT/ft34-db-integrity/` に以下を作成:
|
|
20
|
+
|
|
21
|
+
- **`app.py`** — SQLite インメモリ DB + `SqlAlchemyQueryExecutor` + `SimpleDomainHandler` による UNIQUE 違反ハンドリング
|
|
22
|
+
- **`test_app.py`** — 正常系・409・Problem Details 構造・バリデーションエラー混在 (6 件)
|
|
23
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
handlers = [
|
|
27
|
+
SimpleDomainHandler(
|
|
28
|
+
DatabaseIntegrityException,
|
|
29
|
+
"username-already-taken",
|
|
30
|
+
"Username Already Taken",
|
|
31
|
+
409,
|
|
32
|
+
detail="このユーザー名はすでに使用されています",
|
|
33
|
+
),
|
|
34
|
+
]
|
|
35
|
+
app.add_middleware(ErrorHandlerMiddleware, domain_handlers=handlers)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**テスト結果**: 10 件全通過 ✅
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 摩擦点
|
|
43
|
+
|
|
44
|
+
### FP34-1: `DatabaseIntegrityException` のメッセージが SQLAlchemy の生テキスト
|
|
45
|
+
|
|
46
|
+
**分類**: 既知の制約(摩擦あり・軽微)
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
(sqlite3.IntegrityError) UNIQUE constraint failed: users.username
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
どのフィールドが重複したかを取り出すには文字列パースが必要。
|
|
53
|
+
DB エンジンごとにメッセージ形式が異なる(SQLite / MySQL / PostgreSQL)。
|
|
54
|
+
|
|
55
|
+
**判断**: フレームワーク側でフィールド抽出 API を提供することも可能だが、
|
|
56
|
+
エンジン依存のパースロジックを組み込むと移植性が下がる。
|
|
57
|
+
アプリ側でドメイン固有の例外(`UsernameTakenException` など)に変換するパターンを推奨。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### FP34-2: 制約種別を示すサブクラスがない
|
|
62
|
+
|
|
63
|
+
**分類**: 既知の制約(摩擦あり・軽微)
|
|
64
|
+
|
|
65
|
+
`DatabaseIntegrityException` は UNIQUE / FK / CHECK 制約を区別しない。
|
|
66
|
+
違反種別で異なるレスポンスを返したい場合は文字列パースが必要。
|
|
67
|
+
|
|
68
|
+
**判断**: 制約種別サブクラスの追加は有用だが、DB エンジンごとの判定ロジックが複雑になる。
|
|
69
|
+
現時点はアプリ側で try/except してドメイン例外に変換する方針を how-to ドキュメントで説明。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### FP34-3: テスト用インメモリ SQLite に `StaticPool` が必要
|
|
74
|
+
|
|
75
|
+
**分類**: 摩擦あり → **ドキュメント対応**
|
|
76
|
+
|
|
77
|
+
`SqlAlchemyQueryExecutor` はクエリごとに `engine.begin()` / `engine.connect()` で
|
|
78
|
+
コネクションを開く。`sqlite:///:memory:` はコネクションごとに独立した DB を作成するため、
|
|
79
|
+
`StaticPool` なしではクエリ間でテーブルが見えなくなる場合がある。
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# 推奨パターン
|
|
83
|
+
engine = create_engine(
|
|
84
|
+
"sqlite:///:memory:",
|
|
85
|
+
connect_args={"check_same_thread": False},
|
|
86
|
+
poolclass=StaticPool,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**対応**: `docs/how-to/run-tests.md` に `StaticPool` 使用を明記。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### FP34-4: `SimpleDomainHandler` は制約種別を区別できない
|
|
95
|
+
|
|
96
|
+
**分類**: 設計通り(摩擦なし)
|
|
97
|
+
|
|
98
|
+
`SimpleDomainHandler` は例外クラスのみで判定する。
|
|
99
|
+
UNIQUE 違反と FK 違反を別々の HTTP ステータスにマップしたい場合は
|
|
100
|
+
`DomainExceptionHandlerProtocol` を実装する必要がある。
|
|
101
|
+
|
|
102
|
+
**判断**: `SimpleDomainHandler` はシンプルケース専用のヘルパーという設計意図通り。
|
|
103
|
+
複雑なケースには Protocol 実装を使うのが正しい。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## フレームワーク変更
|
|
108
|
+
|
|
109
|
+
- `docs/how-to/run-tests.md` に `StaticPool` パターンを追記 (FP34-3 対応)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 関連
|
|
114
|
+
|
|
115
|
+
- `nene2.database.DatabaseIntegrityException`
|
|
116
|
+
- `nene2.database.SqlAlchemyQueryExecutor`
|
|
117
|
+
- `nene2.middleware.SimpleDomainHandler`
|
|
118
|
+
- FT16 (DatabaseIntegrityException 実装, v1.7.0)
|
|
119
|
+
- FT21 (SimpleDomainHandler 実装, v1.8.0)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Field Trial 35: 混合認証(Bearer Token OR API Key)実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.3 時点
|
|
5
|
+
**テーマ**: `BearerTokenMiddleware` と `ApiKeyAuthMiddleware` の組み合わせパターン、
|
|
6
|
+
および「いずれか一方で可(OR 条件)」認証の実装コストを確認する
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
Bearer Token 専用・API Key 専用の各ベースラインを確認した後、
|
|
13
|
+
2 つのミドルウェアをスタックした場合の挙動と、
|
|
14
|
+
OR 条件認証のカスタム実装を比較した。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
`/home/xi/docker/nene2-python-FT/ft35-mixed-auth/` に以下を作成:
|
|
21
|
+
|
|
22
|
+
- **`app.py`** — 4 パターンのアプリ(Bearer のみ・API Key のみ・両方スタック・EitherOr カスタム)
|
|
23
|
+
- **`test_app.py`** — 全パターンの動作検証 (13 件)
|
|
24
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (3 件)
|
|
25
|
+
|
|
26
|
+
**テスト結果**: 16 件全通過 ✅
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 摩擦点
|
|
31
|
+
|
|
32
|
+
### FP35-1: ミドルウェアを2つスタックすると AND 条件になる
|
|
33
|
+
|
|
34
|
+
**分類**: 摩擦あり(設計上の制約)
|
|
35
|
+
|
|
36
|
+
`BearerTokenMiddleware` と `ApiKeyAuthMiddleware` を両方 `add_middleware` すると、
|
|
37
|
+
各ミドルウェアが独立して検証するため「両方必須(AND)」になる。
|
|
38
|
+
Bearer Token のみ・API Key のみの場合どちらも 401。
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# この設定は AND 条件(両方必須)
|
|
42
|
+
app.add_middleware(ApiKeyAuthMiddleware, ...)
|
|
43
|
+
app.add_middleware(BearerTokenMiddleware, ...)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**判断**: Starlette のミドルウェアスタックの仕様通り。
|
|
47
|
+
`configure-auth.md` に明記し、AND と OR の違いを説明する。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### FP35-2: OR 条件の認証にはカスタムミドルウェアの実装が必要
|
|
52
|
+
|
|
53
|
+
**分類**: 摩擦あり(実装コスト発生)
|
|
54
|
+
|
|
55
|
+
「Bearer または API Key のいずれかで可」を実現するには
|
|
56
|
+
`BaseHTTPMiddleware` を継承してカスタム実装する必要がある。
|
|
57
|
+
フレームワークに汎用 OR ミドルウェアはない。
|
|
58
|
+
|
|
59
|
+
ただし、`LocalTokenVerifier` / `TokenVerifierProtocol` は再利用可能なため、
|
|
60
|
+
カスタム実装のコードは約 30 行と軽量。
|
|
61
|
+
|
|
62
|
+
**判断**: OR 条件の仕様はプロジェクトによって多様すぎるため、
|
|
63
|
+
フレームワークが汎用実装を提供するより、パターンをドキュメントで示す方が適切。
|
|
64
|
+
|
|
65
|
+
**対応**: `docs/how-to/configure-auth.md` に `EitherOrAuthMiddleware` パターンを追記。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### FP35-3: TokenVerifierProtocol の再利用性が高い(好印象)
|
|
70
|
+
|
|
71
|
+
**分類**: 良い設計の確認
|
|
72
|
+
|
|
73
|
+
`LocalTokenVerifier` はそのままカスタムミドルウェア内で使えるため、
|
|
74
|
+
トークン比較ロジック(`secrets.compare_digest`)を重複実装する必要がない。
|
|
75
|
+
Protocol 分離の設計が効いている。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## フレームワーク変更
|
|
80
|
+
|
|
81
|
+
- `docs/how-to/configure-auth.md` に「AND / OR の違い」と `EitherOrAuthMiddleware` パターンを追記 (FP35-1, FP35-2 対応)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 関連
|
|
86
|
+
|
|
87
|
+
- `nene2.auth.BearerTokenMiddleware`
|
|
88
|
+
- `nene2.auth.ApiKeyAuthMiddleware`
|
|
89
|
+
- `nene2.auth.LocalTokenVerifier`
|
|
90
|
+
- FT11 (exclude_paths, LocalTokenVerifier.from_env, v1.4.0)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Field Trial 36: CompositeHealthCheck 実運用検証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**バージョン**: v1.8.3 時点
|
|
5
|
+
**テーマ**: `CompositeHealthCheck` で複数の依存サービス(DB・外部 API・キャッシュ)の健全性を集約し、`HealthStatus.http_status_code` を使って `/health` エンドポイントを実装するパターン。および非同期ヘルスチェックのギャップを発見・修正。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
SQLite DB・外部 API(モック)・キャッシュ(モック)の 3 つの依存を `CompositeHealthCheck` で集約し、
|
|
12
|
+
部分障害時に個別のチェック名と全体 `"error"` ステータスを返すパターンを検証した。
|
|
13
|
+
|
|
14
|
+
主な発見: 非同期チェックに対応するプロトコルとクラスがなく、外部 HTTP API を非同期で確認するヘルスチェックが書けない摩擦を発見。`AsyncHealthCheckProtocol` と `AsyncCompositeHealthCheck` を追加して対応。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装内容
|
|
19
|
+
|
|
20
|
+
`/home/xi/docker/nene2-python-FT/ft36-composite-health/` に以下を作成:
|
|
21
|
+
|
|
22
|
+
- **`app.py`** — DB / 外部 API / キャッシュの 3 チェックを集約した `/health` エンドポイント
|
|
23
|
+
- **`test_app.py`** — 全正常・DB 障害・部分障害・各チェック名の確認 (5 件)
|
|
24
|
+
- **`test_friction.py`** — 摩擦点の確認テスト (4 件)
|
|
25
|
+
|
|
26
|
+
**テスト結果**: 9 件全通過 ✅
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 摩擦点
|
|
31
|
+
|
|
32
|
+
### FP36-1: 非同期ヘルスチェックに対応するプロトコルがない
|
|
33
|
+
|
|
34
|
+
**分類**: 摩擦あり → **実装で対応**
|
|
35
|
+
|
|
36
|
+
`HealthCheckProtocol.check()` は同期のみ。外部 HTTP API への非同期 `httpx.AsyncClient` 呼び出しや
|
|
37
|
+
非同期 DB クライアントを使うヘルスチェックを書けない。
|
|
38
|
+
|
|
39
|
+
**対応**: `AsyncHealthCheckProtocol` と `AsyncCompositeHealthCheck` を `nene2.http` に追加 (#254)。
|
|
40
|
+
`asyncio.gather` で全チェックを並列実行するため、レスポンスタイムも最適化される。
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
class AsyncApiHealthCheck:
|
|
44
|
+
async def check(self) -> HealthStatus:
|
|
45
|
+
async with httpx.AsyncClient() as client:
|
|
46
|
+
r = await client.get("https://api.example.com/health")
|
|
47
|
+
status = "ok" if r.status_code == 200 else "error"
|
|
48
|
+
return HealthStatus(status=status, checks={"external_api": status})
|
|
49
|
+
|
|
50
|
+
composite = AsyncCompositeHealthCheck([db_check, AsyncApiHealthCheck()])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### FP36-2: チェックの名前を集約時に指定できない
|
|
56
|
+
|
|
57
|
+
**分類**: 設計通り(摩擦なし)
|
|
58
|
+
|
|
59
|
+
チェックの名前は `HealthStatus.checks` の dict キーで決まる(チェック実装側が定義する)。
|
|
60
|
+
集約側での名前 override はできない。
|
|
61
|
+
|
|
62
|
+
**判断**: チェック実装がその名前を知っているべきという責務分離の観点で正しい設計。
|
|
63
|
+
集約側での名前指定を可能にすると DRY 違反になりやすい。
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### FP36-3: 同名キーを持つチェックが複数あると後勝ちになる
|
|
68
|
+
|
|
69
|
+
**分類**: 軽微な摩擦(ドキュメント対応)
|
|
70
|
+
|
|
71
|
+
2 つのチェックが同じキーを `checks` に返した場合、`dict.update()` で後のチェックが勝つ。
|
|
72
|
+
衝突検出の仕組みはない。
|
|
73
|
+
|
|
74
|
+
**判断**: 各チェックがユニークな名前を使う規約で対応。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## フレームワーク変更
|
|
79
|
+
|
|
80
|
+
- `nene2.http.AsyncHealthCheckProtocol` — `async def check() -> HealthStatus` の Protocol を追加 (FP36-1)
|
|
81
|
+
- `nene2.http.AsyncCompositeHealthCheck` — 並列実行の集約クラスを追加 (FP36-1)
|
|
82
|
+
- テスト 6 件追加
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 関連
|
|
87
|
+
|
|
88
|
+
- `nene2.http.CompositeHealthCheck`
|
|
89
|
+
- `nene2.http.HealthStatus`
|
|
90
|
+
- FT22 (CompositeHealthCheck 実装, v1.8.0)
|
|
91
|
+
- FT31 (HealthStatus.http_status_code, v1.8.3)
|
|
92
|
+
- Issue #254
|
|
@@ -45,9 +45,56 @@ API_KEYS=["key1","key2"]
|
|
|
45
45
|
curl -H "X-Api-Key: key1" http://localhost:8080/notes
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
## Using both at once
|
|
48
|
+
## Using both at once (AND condition)
|
|
49
49
|
|
|
50
|
-
When both
|
|
50
|
+
When both middlewares are added via `add_middleware`, requests must pass **both** checks (AND condition). A Bearer token alone or an API key alone will both result in 401.
|
|
51
|
+
|
|
52
|
+
## Either-or authentication (Bearer Token OR API Key)
|
|
53
|
+
|
|
54
|
+
If you want to accept **either** a Bearer token or an API key — whichever is present — the built-in middlewares cannot express this. Implement a custom middleware that reuses the existing verifiers:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
58
|
+
from starlette.requests import Request
|
|
59
|
+
from starlette.responses import Response
|
|
60
|
+
|
|
61
|
+
from nene2.auth import LocalTokenVerifier
|
|
62
|
+
from nene2.auth.exceptions import TokenVerificationException
|
|
63
|
+
from nene2.http.problem_details import problem_details_response
|
|
64
|
+
|
|
65
|
+
class EitherOrAuthMiddleware(BaseHTTPMiddleware):
|
|
66
|
+
def __init__(self, app, *, bearer_tokens: list[str], api_keys: list[str],
|
|
67
|
+
exclude_paths: list[str] | None = None) -> None:
|
|
68
|
+
super().__init__(app)
|
|
69
|
+
self._bearer = LocalTokenVerifier(bearer_tokens)
|
|
70
|
+
self._api_key = LocalTokenVerifier(api_keys)
|
|
71
|
+
self._exclude_paths = set(exclude_paths or [])
|
|
72
|
+
|
|
73
|
+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
74
|
+
if request.url.path in self._exclude_paths:
|
|
75
|
+
return await call_next(request)
|
|
76
|
+
|
|
77
|
+
auth = request.headers.get("Authorization", "")
|
|
78
|
+
if auth.startswith("Bearer "):
|
|
79
|
+
try:
|
|
80
|
+
if self._bearer.verify(auth[len("Bearer "):]):
|
|
81
|
+
return await call_next(request)
|
|
82
|
+
except TokenVerificationException:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
api_key = request.headers.get("X-Api-Key", "")
|
|
86
|
+
if api_key:
|
|
87
|
+
try:
|
|
88
|
+
if self._api_key.verify(api_key):
|
|
89
|
+
return await call_next(request)
|
|
90
|
+
except TokenVerificationException:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return problem_details_response(
|
|
94
|
+
"unauthorized", "Unauthorized", 401,
|
|
95
|
+
"A valid Bearer token or X-Api-Key header is required.",
|
|
96
|
+
)
|
|
97
|
+
```
|
|
51
98
|
|
|
52
99
|
## Disabling auth in tests
|
|
53
100
|
|
|
@@ -86,6 +86,23 @@ async def test_async_list_notes() -> None:
|
|
|
86
86
|
assert result.total == 0
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## In-memory SQLite for integration tests
|
|
90
|
+
|
|
91
|
+
When using `SqlAlchemyQueryExecutor` or `SqlAlchemyTransactionManager` with an in-memory SQLite database, always pass `poolclass=StaticPool`. Without it, SQLAlchemy may open a new physical connection that sees an empty database.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from sqlalchemy import create_engine
|
|
95
|
+
from sqlalchemy.pool import StaticPool
|
|
96
|
+
|
|
97
|
+
engine = create_engine(
|
|
98
|
+
"sqlite:///:memory:",
|
|
99
|
+
connect_args={"check_same_thread": False},
|
|
100
|
+
poolclass=StaticPool,
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`StaticPool` guarantees all logical connections share the same underlying SQLite connection, so tables created in one operation are visible to the next.
|
|
105
|
+
|
|
89
106
|
## Coverage requirements
|
|
90
107
|
|
|
91
108
|
| Scope | Target |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# How-to: バリデーションエラーを扱う
|
|
2
|
+
|
|
3
|
+
`nene2.validation` の `ValidationException` と `ValidationError` を使って、
|
|
4
|
+
ドメインバリデーションエラーをクライアントに返す方法を説明する。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. ValidationCode を StrEnum で定義する
|
|
9
|
+
|
|
10
|
+
フレームワークは標準エラーコードを定義しない。プロジェクトごとに `StrEnum` で定義する。
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
|
|
15
|
+
class ValidationCode(StrEnum):
|
|
16
|
+
REQUIRED = "required"
|
|
17
|
+
INVALID_FORMAT = "invalid_format"
|
|
18
|
+
TOO_SHORT = "too_short"
|
|
19
|
+
TOO_LONG = "too_long"
|
|
20
|
+
ALREADY_EXISTS = "already_exists"
|
|
21
|
+
OUT_OF_RANGE = "out_of_range"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`StrEnum` を使うと:
|
|
25
|
+
- `ValidationError` コンストラクタに直接渡せる(型安全)
|
|
26
|
+
- JSON シリアライズ時に文字列値として出力される
|
|
27
|
+
- IDE の補完・静的解析が効く
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. 複数フィールドのバリデーション
|
|
32
|
+
|
|
33
|
+
リストにエラーを積み上げて `ValidationException` をまとめて raise する。
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from nene2.validation import ValidationError, ValidationException
|
|
37
|
+
|
|
38
|
+
def validate_registration(username: str, email: str, age: int) -> None:
|
|
39
|
+
errors: list[ValidationError] = []
|
|
40
|
+
|
|
41
|
+
if len(username) < 3:
|
|
42
|
+
errors.append(ValidationError("username", "3文字以上必要です", ValidationCode.TOO_SHORT))
|
|
43
|
+
if "@" not in email:
|
|
44
|
+
errors.append(ValidationError("email", "有効なメールアドレスを入力してください", ValidationCode.INVALID_FORMAT))
|
|
45
|
+
if age < 0 or age > 150:
|
|
46
|
+
errors.append(ValidationError("age", "年齢は 0〜150 の範囲で入力してください", ValidationCode.OUT_OF_RANGE))
|
|
47
|
+
|
|
48
|
+
if errors:
|
|
49
|
+
raise ValidationException(errors)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
レスポンス例 (422):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"type": "https://example.com/problems/validation-failed",
|
|
57
|
+
"title": "Validation Failed",
|
|
58
|
+
"status": 422,
|
|
59
|
+
"errors": [
|
|
60
|
+
{"field": "username", "message": "3文字以上必要です", "code": "too_short"},
|
|
61
|
+
{"field": "email", "message": "有効なメールアドレスを入力してください", "code": "invalid_format"}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. 単一フィールドのバリデーション
|
|
69
|
+
|
|
70
|
+
`ValidationException.single()` で 1 行で raise できる。
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nene2.validation import ValidationException
|
|
74
|
+
|
|
75
|
+
raise ValidationException.single("email", "メールアドレスは必須です", ValidationCode.REQUIRED)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 4. ネストフィールドのパス
|
|
81
|
+
|
|
82
|
+
ネストしたフィールドのパスはドット区切りの文字列で渡す(正規化ヘルパーはない)。
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
ValidationError("address.city", "必須です", ValidationCode.REQUIRED)
|
|
86
|
+
ValidationError("items.0.quantity", "1 以上を入力してください", ValidationCode.OUT_OF_RANGE)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. ErrorHandlerMiddleware との連携
|
|
92
|
+
|
|
93
|
+
`nene2.middleware.ErrorHandlerMiddleware` をアプリに追加すると、
|
|
94
|
+
`ValidationException` が自動で 422 Problem Details レスポンスに変換される。
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from nene2.middleware import ErrorHandlerMiddleware
|
|
98
|
+
|
|
99
|
+
app = FastAPI()
|
|
100
|
+
app.add_middleware(ErrorHandlerMiddleware)
|
|
101
|
+
```
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"""HTTP helpers — JSON responses, pagination, problem details, health."""
|
|
2
2
|
|
|
3
|
-
from .health import
|
|
3
|
+
from .health import (
|
|
4
|
+
AsyncCompositeHealthCheck,
|
|
5
|
+
AsyncHealthCheckProtocol,
|
|
6
|
+
CompositeHealthCheck,
|
|
7
|
+
HealthCheckProtocol,
|
|
8
|
+
HealthStatus,
|
|
9
|
+
)
|
|
4
10
|
from .pagination import PaginationQuery, PaginationQueryParser, PaginationResponse
|
|
5
11
|
from .problem_details import configure_problem_details, problem_details_response
|
|
6
12
|
|
|
7
13
|
__all__ = [
|
|
14
|
+
"AsyncCompositeHealthCheck",
|
|
15
|
+
"AsyncHealthCheckProtocol",
|
|
8
16
|
"CompositeHealthCheck",
|
|
9
17
|
"HealthCheckProtocol",
|
|
10
18
|
"HealthStatus",
|