nene2-python 1.8.22__tar.gz → 1.8.23__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.22 → nene2_python-1.8.23}/CHANGELOG.md +12 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/PKG-INFO +1 -1
- nene2_python-1.8.23/docs/field-trials/2026-05-field-trial-77.md +179 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/pyproject.toml +1 -1
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/api_key.py +30 -3
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/bearer_token.py +28 -4
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_api_key.py +69 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_bearer_token.py +81 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/uv.lock +1 -1
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.env.example +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.gitignore +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/AGENTS.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/CLAUDE.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/Dockerfile +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/LICENSE +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/README.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/README +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/env.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic.ini +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/compose.yaml +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/de/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/fr/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/api.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/roadmap.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/todo/current.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/zh/index.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/package-lock.json +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/package.json +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/__main__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/app.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/mcp.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/schema.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.22] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT76 フィールドトライアル — async def + sync DB ブロッキング問題と run_in_threadpool 追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `run_in_threadpool` を `nene2.use_case` から re-export (#326) (FT76)
|
|
14
|
+
— Starlette の `run_in_threadpool` を nene2 公開 API として公開し、
|
|
15
|
+
`async def` ハンドラーから同期 DB 処理を安全にスレッドプールへオフロードできる
|
|
16
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-76.md` (FT76)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
8
20
|
## [1.8.21] — 2026-05-20
|
|
9
21
|
|
|
10
22
|
FT75 フィールドトライアル — ミドルウェアスタック順序問題の発見と根本解決。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.23
|
|
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,179 @@
|
|
|
1
|
+
# FT77: BearerToken + ApiKey 混在認証
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: 同一アプリで認証方式を混在させる — `/admin/*` は JWT Bearer、`/webhook/*` は API Key
|
|
5
|
+
**バージョン**: v1.8.22
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft77-mixed-auth/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
実運用では「認証方式がルートによって異なる」は非常に一般的なニーズ。
|
|
13
|
+
nene2 の `BearerTokenMiddleware` と `ApiKeyAuthMiddleware` を混在させる方法を検証した。
|
|
14
|
+
ミドルウェアはアプリ全体をカバーするため、`exclude_paths` を使った回避策が必要で、
|
|
15
|
+
ここに大きな摩擦があることが判明した。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装したパターン
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/admin/* → BearerTokenMiddleware(JWT Bearer 必須)
|
|
23
|
+
/webhook/* → ApiKeyAuthMiddleware(X-Api-Key 必須)
|
|
24
|
+
/public/* → 認証不要
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 設定コード
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
app.add_middleware(
|
|
31
|
+
BearerTokenMiddleware,
|
|
32
|
+
verifier=admin_verifier,
|
|
33
|
+
exclude_paths=[
|
|
34
|
+
"/docs", "/openapi.json", "/redoc", "/health",
|
|
35
|
+
"/public/hello", "/public/status",
|
|
36
|
+
"/webhook/event", "/webhook/ping", # ← webhook は除外
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
app.add_middleware(
|
|
41
|
+
ApiKeyAuthMiddleware,
|
|
42
|
+
verifier=webhook_verifier,
|
|
43
|
+
exclude_paths=[
|
|
44
|
+
"/docs", "/openapi.json", "/redoc", "/health",
|
|
45
|
+
"/public/hello", "/public/status",
|
|
46
|
+
"/admin/dashboard", "/admin/users", # ← admin は除外
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 発見した問題
|
|
54
|
+
|
|
55
|
+
### 問題1: exclude_paths の二重管理
|
|
56
|
+
|
|
57
|
+
認証対象でないパスを **各ミドルウェアに個別に列挙** しなければならない。
|
|
58
|
+
|
|
59
|
+
- 新しい `/admin/settings` ルートを追加 → `ApiKeyAuthMiddleware` の `exclude_paths` にも追加必要
|
|
60
|
+
- 忘れると: Bearer トークンを持っていても、API Key がないため 401 になる
|
|
61
|
+
- **サイレント障害**: 認証エラーなので「なぜ 401?」と混乱しやすい
|
|
62
|
+
|
|
63
|
+
### 問題2: exclude_paths はルートではなく完全一致パス
|
|
64
|
+
|
|
65
|
+
`exclude_paths` はプレフィックスマッチングをサポートしない。
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# ❌ プレフィックスマッチしない
|
|
69
|
+
exclude_paths=["/admin"] # /admin/dashboard にマッチしない
|
|
70
|
+
|
|
71
|
+
# ✅ 完全一致のみ
|
|
72
|
+
exclude_paths=["/admin/dashboard", "/admin/users"] # 各パスを列挙
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
ルート数が増えると管理が煩雑になる。
|
|
76
|
+
|
|
77
|
+
### 問題3: per-route 認証デコレーターがない
|
|
78
|
+
|
|
79
|
+
FastAPI の Depends() パターンでルートごとに認証を指定することが nene2 では直接できない。
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# FastAPI 標準 (nene2 提供なし)
|
|
83
|
+
from fastapi.security import HTTPBearer
|
|
84
|
+
security = HTTPBearer()
|
|
85
|
+
|
|
86
|
+
@app.get("/admin/dashboard")
|
|
87
|
+
def admin(token = Depends(security)):
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
nene2 でこれをやるには生の FastAPI `Depends()` を使う必要があり、
|
|
92
|
+
nene2 の `TokenVerifierProtocol` との統合方法が不明瞭。
|
|
93
|
+
|
|
94
|
+
### 問題4: 「どちらかで認証」が実現できない
|
|
95
|
+
|
|
96
|
+
Bearer でも API Key でも OK という「OR 認証」がミドルウェア方式では実現困難。
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
# 現状では:
|
|
100
|
+
BearerMiddleware: 対象パスはBearer必須
|
|
101
|
+
ApiKeyMiddleware: 対象パスはApiKey必須
|
|
102
|
+
|
|
103
|
+
# 実現できない:
|
|
104
|
+
"Bearer OR ApiKey どちらかがあれば OK"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## テスト結果(全17件パス)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
test_public_hello_no_auth PASSED # 公開エンドポイント
|
|
113
|
+
test_public_status_no_auth PASSED
|
|
114
|
+
test_health_no_auth PASSED
|
|
115
|
+
test_admin_with_valid_bearer PASSED # 正常認証
|
|
116
|
+
test_admin_without_auth_returns_401 PASSED
|
|
117
|
+
test_admin_with_invalid_bearer_returns_401 PASSED
|
|
118
|
+
test_admin_with_api_key_instead_of_bearer_returns_401 PASSED # 認証方式が違う
|
|
119
|
+
test_admin_users_with_valid_bearer PASSED
|
|
120
|
+
test_webhook_with_valid_api_key PASSED
|
|
121
|
+
test_webhook_without_auth_returns_401 PASSED
|
|
122
|
+
test_webhook_with_invalid_api_key_returns_401 PASSED
|
|
123
|
+
test_webhook_with_bearer_instead_of_api_key_returns_401 PASSED
|
|
124
|
+
test_admin_with_both_headers_bearer_wins PASSED # 両方送ると適切に処理
|
|
125
|
+
test_webhook_with_both_headers_apikey_wins PASSED
|
|
126
|
+
test_friction_new_admin_route_needs_exclude_update PASSED # 摩擦の記録
|
|
127
|
+
test_friction_exclude_paths_is_per_middleware PASSED
|
|
128
|
+
test_nene2_has_no_per_route_auth_decorator PASSED # per-route 機能なし確認
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 摩擦ポイント一覧
|
|
134
|
+
|
|
135
|
+
| ID | 内容 | 深刻度 |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| F77-1 | exclude_paths の二重管理 — 新ルートを追加するたびに各ミドルウェアを更新 | 高 |
|
|
138
|
+
| F77-2 | exclude_paths がプレフィックスマッチをサポートしない(完全一致のみ)| 中 |
|
|
139
|
+
| F77-3 | per-route 認証デコレーターがない — Depends() との統合方法が不明 | 中 |
|
|
140
|
+
| F77-4 | Bearer OR ApiKey の OR 認証が実現できない | 中 |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 使用感(主観評価)
|
|
145
|
+
|
|
146
|
+
### 直感性 ★★☆☆☆
|
|
147
|
+
|
|
148
|
+
「ミドルウェアは全体をカバーする」という原則は理解できる。
|
|
149
|
+
しかし「特定のルートだけ認証したい」という非常に一般的なニーズに、
|
|
150
|
+
`exclude_paths` という「否定のリスト」で対応するのは直感に反する。
|
|
151
|
+
|
|
152
|
+
Express の `passport.authenticate()` や Spring Security の `requestMatchers()` は
|
|
153
|
+
「守りたいパスを指定」するモデル。nene2 は「除外するパスを指定」という逆のモデルで混乱しやすい。
|
|
154
|
+
|
|
155
|
+
### 実害の深刻さ ★★★★☆
|
|
156
|
+
|
|
157
|
+
新しいルートを追加したとき、`exclude_paths` の更新を忘れると **サイレントな認証エラー** が発生する。
|
|
158
|
+
本番でユーザーが急に 401 になり、コードを読んでも一見問題がないように見える。
|
|
159
|
+
ミドルウェアの設定を疑わないと根本原因に辿り着けない。
|
|
160
|
+
|
|
161
|
+
### 修正のしやすさ ★★★☆☆
|
|
162
|
+
|
|
163
|
+
根本解決は「プレフィックスマッチ対応」または「per-route auth」の提供。
|
|
164
|
+
プレフィックスマッチは小さな変更で実現できる。
|
|
165
|
+
per-route auth の提供は FastAPI Depends() との統合が必要で中程度の工数。
|
|
166
|
+
|
|
167
|
+
### 総合コメント
|
|
168
|
+
|
|
169
|
+
「守りたいパスのプレフィックスを指定する」モデルへの転換が最も効果的。
|
|
170
|
+
`include_paths: list[str] | None` または `path_prefix: str` パラメーターを追加するだけで、
|
|
171
|
+
「`/admin` 以下は全部 Bearer 必須」と書けるようになり、UX が大幅に改善する。
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 推奨アクション
|
|
176
|
+
|
|
177
|
+
1. **Issue**: `BearerTokenMiddleware` / `ApiKeyAuthMiddleware` に `include_paths` または `path_prefixes` パラメーターを追加
|
|
178
|
+
2. **Issue**: `exclude_paths` にプレフィックスマッチ(`/admin/*` 形式)をサポート
|
|
179
|
+
3. **将来**: `nene2.auth` に FastAPI `Depends()` 統合ヘルパーを提供
|
|
@@ -17,10 +17,30 @@ _DEFAULT_API_KEY_HEADER = "X-Api-Key"
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ApiKeyAuthMiddleware(BaseHTTPMiddleware):
|
|
20
|
-
"""Require a valid API key header on
|
|
20
|
+
"""Require a valid API key header on matching requests.
|
|
21
21
|
|
|
22
|
-
The header name defaults to ``X-Api-Key`` but can be customised
|
|
22
|
+
The header name defaults to ``X-Api-Key`` but can be customised.
|
|
23
23
|
|
|
24
|
+
**Path filtering** — two complementary options (mutually exclusive):
|
|
25
|
+
|
|
26
|
+
- ``include_paths``: only protect paths whose prefix matches one of these values.
|
|
27
|
+
All other paths pass through without authentication.
|
|
28
|
+
Ideal for protecting a specific sub-tree (e.g. ``["/webhook"]``).
|
|
29
|
+
- ``exclude_paths``: protect every path **except** these exact paths.
|
|
30
|
+
Ideal for skipping docs / health endpoints.
|
|
31
|
+
|
|
32
|
+
When both are provided, ``include_paths`` takes precedence.
|
|
33
|
+
|
|
34
|
+
Examples::
|
|
35
|
+
|
|
36
|
+
# Protect only /webhook/* routes (prefix match)
|
|
37
|
+
app.add_middleware(
|
|
38
|
+
ApiKeyAuthMiddleware,
|
|
39
|
+
verifier=LocalTokenVerifier(api_keys),
|
|
40
|
+
include_paths=["/webhook"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Protect everything except docs/health (exact match)
|
|
24
44
|
app.add_middleware(
|
|
25
45
|
ApiKeyAuthMiddleware,
|
|
26
46
|
verifier=LocalTokenVerifier(api_keys),
|
|
@@ -36,14 +56,21 @@ class ApiKeyAuthMiddleware(BaseHTTPMiddleware):
|
|
|
36
56
|
verifier: TokenVerifierProtocol,
|
|
37
57
|
header_name: str = _DEFAULT_API_KEY_HEADER,
|
|
38
58
|
exclude_paths: list[str] | None = None,
|
|
59
|
+
include_paths: list[str] | None = None,
|
|
39
60
|
) -> None:
|
|
40
61
|
super().__init__(app) # type: ignore[arg-type]
|
|
41
62
|
self._verifier = verifier
|
|
42
63
|
self._header_name = header_name
|
|
43
64
|
self._exclude_paths = set(exclude_paths or [])
|
|
65
|
+
self._include_paths = list(include_paths or [])
|
|
66
|
+
|
|
67
|
+
def _should_authenticate(self, path: str) -> bool:
|
|
68
|
+
if self._include_paths:
|
|
69
|
+
return any(path.startswith(prefix) for prefix in self._include_paths)
|
|
70
|
+
return path not in self._exclude_paths
|
|
44
71
|
|
|
45
72
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
46
|
-
if request.url.path
|
|
73
|
+
if not self._should_authenticate(request.url.path):
|
|
47
74
|
return await call_next(request)
|
|
48
75
|
api_key = request.headers.get(self._header_name, "")
|
|
49
76
|
try:
|
|
@@ -17,11 +17,28 @@ _WWW_AUTH = 'Bearer realm="api"'
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class BearerTokenMiddleware(BaseHTTPMiddleware):
|
|
20
|
-
"""Require a valid Bearer token on
|
|
20
|
+
"""Require a valid Bearer token on matching requests.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
health-check endpoints or API documentation::
|
|
22
|
+
**Path filtering** — two complementary options (mutually exclusive):
|
|
24
23
|
|
|
24
|
+
- ``include_paths``: only protect paths whose prefix matches one of these values.
|
|
25
|
+
All other paths pass through without authentication.
|
|
26
|
+
Ideal for protecting a specific sub-tree (e.g. ``["/admin"]``).
|
|
27
|
+
- ``exclude_paths``: protect every path **except** these exact paths.
|
|
28
|
+
Ideal for skipping docs / health endpoints.
|
|
29
|
+
|
|
30
|
+
When both are provided, ``include_paths`` takes precedence.
|
|
31
|
+
|
|
32
|
+
Examples::
|
|
33
|
+
|
|
34
|
+
# Protect only /admin/* routes (prefix match)
|
|
35
|
+
app.add_middleware(
|
|
36
|
+
BearerTokenMiddleware,
|
|
37
|
+
verifier=LocalTokenVerifier(tokens),
|
|
38
|
+
include_paths=["/admin"],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Protect everything except docs/health (exact match)
|
|
25
42
|
app.add_middleware(
|
|
26
43
|
BearerTokenMiddleware,
|
|
27
44
|
verifier=LocalTokenVerifier(tokens),
|
|
@@ -35,13 +52,20 @@ class BearerTokenMiddleware(BaseHTTPMiddleware):
|
|
|
35
52
|
*,
|
|
36
53
|
verifier: TokenVerifierProtocol,
|
|
37
54
|
exclude_paths: list[str] | None = None,
|
|
55
|
+
include_paths: list[str] | None = None,
|
|
38
56
|
) -> None:
|
|
39
57
|
super().__init__(app) # type: ignore[arg-type]
|
|
40
58
|
self._verifier = verifier
|
|
41
59
|
self._exclude_paths = set(exclude_paths or [])
|
|
60
|
+
self._include_paths = list(include_paths or [])
|
|
61
|
+
|
|
62
|
+
def _should_authenticate(self, path: str) -> bool:
|
|
63
|
+
if self._include_paths:
|
|
64
|
+
return any(path.startswith(prefix) for prefix in self._include_paths)
|
|
65
|
+
return path not in self._exclude_paths
|
|
42
66
|
|
|
43
67
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
44
|
-
if request.url.path
|
|
68
|
+
if not self._should_authenticate(request.url.path):
|
|
45
69
|
return await call_next(request)
|
|
46
70
|
auth = request.headers.get("Authorization", "")
|
|
47
71
|
if not auth.startswith("Bearer "):
|
|
@@ -122,3 +122,72 @@ def test_custom_header_name_in_error_message() -> None:
|
|
|
122
122
|
response = client.get("/secret")
|
|
123
123
|
assert response.status_code == 401
|
|
124
124
|
assert "X-Internal-Key" in response.json().get("detail", "")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# include_paths tests
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
def test_include_paths_protects_matching_prefix() -> None:
|
|
131
|
+
app = FastAPI()
|
|
132
|
+
app.add_middleware(
|
|
133
|
+
ApiKeyAuthMiddleware,
|
|
134
|
+
verifier=LocalTokenVerifier(["key"]),
|
|
135
|
+
include_paths=["/webhook"],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@app.get("/webhook/event")
|
|
139
|
+
async def webhook_event() -> JSONResponse:
|
|
140
|
+
return JSONResponse({"received": True})
|
|
141
|
+
|
|
142
|
+
@app.get("/public/hello")
|
|
143
|
+
async def public_hello() -> JSONResponse:
|
|
144
|
+
return JSONResponse({"hello": True})
|
|
145
|
+
|
|
146
|
+
client = TestClient(app)
|
|
147
|
+
assert client.get("/webhook/event").status_code == 401
|
|
148
|
+
assert client.get("/webhook/event", headers={"X-Api-Key": "key"}).status_code == 200
|
|
149
|
+
assert client.get("/public/hello").status_code == 200
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_include_paths_multiple_prefixes() -> None:
|
|
153
|
+
app = FastAPI()
|
|
154
|
+
app.add_middleware(
|
|
155
|
+
ApiKeyAuthMiddleware,
|
|
156
|
+
verifier=LocalTokenVerifier(["key"]),
|
|
157
|
+
include_paths=["/webhook", "/internal"],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@app.get("/webhook/x")
|
|
161
|
+
async def webhook_x() -> JSONResponse:
|
|
162
|
+
return JSONResponse({"ok": True})
|
|
163
|
+
|
|
164
|
+
@app.get("/internal/y")
|
|
165
|
+
async def internal_y() -> JSONResponse:
|
|
166
|
+
return JSONResponse({"ok": True})
|
|
167
|
+
|
|
168
|
+
@app.get("/public/z")
|
|
169
|
+
async def public_z() -> JSONResponse:
|
|
170
|
+
return JSONResponse({"ok": True})
|
|
171
|
+
|
|
172
|
+
client = TestClient(app)
|
|
173
|
+
assert client.get("/webhook/x").status_code == 401
|
|
174
|
+
assert client.get("/internal/y").status_code == 401
|
|
175
|
+
assert client.get("/public/z").status_code == 200
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_include_paths_takes_precedence_over_exclude_paths() -> None:
|
|
179
|
+
"""両方指定されたときは include_paths が優先される。"""
|
|
180
|
+
app = FastAPI()
|
|
181
|
+
app.add_middleware(
|
|
182
|
+
ApiKeyAuthMiddleware,
|
|
183
|
+
verifier=LocalTokenVerifier(["key"]),
|
|
184
|
+
include_paths=["/webhook"],
|
|
185
|
+
exclude_paths=["/webhook/open"], # include_paths があるので無視される
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@app.get("/webhook/open")
|
|
189
|
+
async def webhook_open() -> JSONResponse:
|
|
190
|
+
return JSONResponse({"ok": True})
|
|
191
|
+
|
|
192
|
+
client = TestClient(app)
|
|
193
|
+
assert client.get("/webhook/open").status_code == 401
|
|
@@ -126,3 +126,84 @@ def test_local_verifier_accepts_frozenset() -> None:
|
|
|
126
126
|
verifier = LocalTokenVerifier(frozenset({"tok-x", "tok-y"}))
|
|
127
127
|
assert verifier.verify("tok-x") is True
|
|
128
128
|
assert verifier.verify("unknown") is False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# include_paths tests
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
def _make_app_with_include_paths(tokens: list[str], include_paths: list[str]) -> FastAPI:
|
|
135
|
+
app = FastAPI()
|
|
136
|
+
app.add_middleware(
|
|
137
|
+
BearerTokenMiddleware,
|
|
138
|
+
verifier=LocalTokenVerifier(tokens),
|
|
139
|
+
include_paths=include_paths,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@app.get("/admin/dashboard")
|
|
143
|
+
async def admin_dashboard() -> JSONResponse:
|
|
144
|
+
return JSONResponse({"admin": True})
|
|
145
|
+
|
|
146
|
+
@app.get("/public/hello")
|
|
147
|
+
async def public_hello() -> JSONResponse:
|
|
148
|
+
return JSONResponse({"hello": True})
|
|
149
|
+
|
|
150
|
+
return app
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_include_paths_protects_matching_prefix() -> None:
|
|
154
|
+
client = TestClient(_make_app_with_include_paths(["tok"], ["/admin"]))
|
|
155
|
+
assert client.get("/admin/dashboard").status_code == 401
|
|
156
|
+
assert (
|
|
157
|
+
client.get("/admin/dashboard", headers={"Authorization": "Bearer tok"}).status_code == 200
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_include_paths_skips_non_matching_prefix() -> None:
|
|
162
|
+
"""include_paths にないパスは認証なしで通過する。"""
|
|
163
|
+
client = TestClient(_make_app_with_include_paths(["tok"], ["/admin"]))
|
|
164
|
+
assert client.get("/public/hello").status_code == 200
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_include_paths_multiple_prefixes() -> None:
|
|
168
|
+
app = FastAPI()
|
|
169
|
+
app.add_middleware(
|
|
170
|
+
BearerTokenMiddleware,
|
|
171
|
+
verifier=LocalTokenVerifier(["tok"]),
|
|
172
|
+
include_paths=["/admin", "/private"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@app.get("/admin/x")
|
|
176
|
+
async def admin_x() -> JSONResponse:
|
|
177
|
+
return JSONResponse({"ok": True})
|
|
178
|
+
|
|
179
|
+
@app.get("/private/y")
|
|
180
|
+
async def private_y() -> JSONResponse:
|
|
181
|
+
return JSONResponse({"ok": True})
|
|
182
|
+
|
|
183
|
+
@app.get("/public/z")
|
|
184
|
+
async def public_z() -> JSONResponse:
|
|
185
|
+
return JSONResponse({"ok": True})
|
|
186
|
+
|
|
187
|
+
client = TestClient(app)
|
|
188
|
+
assert client.get("/admin/x").status_code == 401
|
|
189
|
+
assert client.get("/private/y").status_code == 401
|
|
190
|
+
assert client.get("/public/z").status_code == 200
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_include_paths_takes_precedence_over_exclude_paths() -> None:
|
|
194
|
+
"""両方指定されたときは include_paths が優先される。"""
|
|
195
|
+
app = FastAPI()
|
|
196
|
+
app.add_middleware(
|
|
197
|
+
BearerTokenMiddleware,
|
|
198
|
+
verifier=LocalTokenVerifier(["tok"]),
|
|
199
|
+
include_paths=["/admin"],
|
|
200
|
+
exclude_paths=["/admin/open"], # include_paths があるので無視される
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@app.get("/admin/open")
|
|
204
|
+
async def admin_open() -> JSONResponse:
|
|
205
|
+
return JSONResponse({"ok": True})
|
|
206
|
+
|
|
207
|
+
client = TestClient(app)
|
|
208
|
+
# include_paths=["/admin"] が優先 → /admin/open も保護される
|
|
209
|
+
assert client.get("/admin/open").status_code == 401
|
|
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.22 → nene2_python-1.8.23}/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
|