nene2-python 1.8.24__tar.gz → 1.8.26__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.24 → nene2_python-1.8.26}/CHANGELOG.md +22 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/PKG-INFO +1 -1
- nene2_python-1.8.26/docs/field-trials/2026-05-field-trial-79.md +186 -0
- nene2_python-1.8.26/docs/field-trials/2026-05-field-trial-80.md +176 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/pyproject.toml +1 -1
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/mcp/server.py +16 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/request_logging.py +24 -0
- nene2_python-1.8.26/tests/nene2/mcp/test_server.py +51 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_request_logging.py +78 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/uv.lock +1 -1
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.env.example +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.gitignore +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/AGENTS.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/CLAUDE.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/Dockerfile +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/LICENSE +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/README.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/alembic/README +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/alembic/env.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/alembic.ini +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/compose.yaml +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/de/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/fr/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/reference/api.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/roadmap.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/todo/current.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/zh/index.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/package-lock.json +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/package.json +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/__main__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/app.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/mcp.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/schema.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.24 → nene2_python-1.8.26}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.25] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT79 フィールドトライアル — RequestLoggingMiddleware の構造化ログ検証と context_extractor 追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `RequestLoggingMiddleware` に `context_extractor` コールバックパラメーターを追加 (#339) (FT79)
|
|
14
|
+
— リクエストごとの動的なログコンテキスト(user_id、テナントID等)を注入できる
|
|
15
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-79.md` (FT79)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.8.24] — 2026-05-20
|
|
20
|
+
|
|
21
|
+
FT78 フィールドトライアル — ThrottleMiddleware の境界動作検証とドキュメント強化。
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `ThrottleMiddleware` クラス docstring に Fixed Window バースト特性とマルチプロセス非対応の警告を追記 (#335) (FT78)
|
|
25
|
+
- `setup_middlewares()` の `throttle_limit` docstring にマルチプロセス警告を追記 (#335) (FT78)
|
|
26
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-78.md` (FT78)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
8
30
|
## [1.8.23] — 2026-05-20
|
|
9
31
|
|
|
10
32
|
FT77 フィールドトライアル — BearerToken + ApiKey 混在認証と include_paths 追加。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.26
|
|
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,186 @@
|
|
|
1
|
+
# FT79: RequestLoggingMiddleware と構造化ログ
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: リクエストログに何が含まれるか — 機密情報漏洩リスクとデバッグ適性の確認
|
|
5
|
+
**バージョン**: v1.8.24
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft79-request-logging/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
`RequestLoggingMiddleware` と `structlog` の組み合わせを検証した。
|
|
13
|
+
基本機能は期待通り動作し、セキュリティ設計(ボディをログしない)も適切。
|
|
14
|
+
一方で、リクエストボディの選択的ログ記録やリクエストごとの動的コンテキスト追加といった
|
|
15
|
+
デバッグ用途での柔軟性の欠如が摩擦点として判明した。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ログ出力の内容(確認済み)
|
|
20
|
+
|
|
21
|
+
### request.received
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"event": "request.received",
|
|
26
|
+
"level": "info",
|
|
27
|
+
"logger": "nene2.middleware.request_logging",
|
|
28
|
+
"method": "GET",
|
|
29
|
+
"path": "/api/users",
|
|
30
|
+
"request_id": "abc-123",
|
|
31
|
+
"service": "ft79-app",
|
|
32
|
+
"version": "1.0.0"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### request.completed
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"event": "request.completed",
|
|
41
|
+
"level": "info",
|
|
42
|
+
"logger": "nene2.middleware.request_logging",
|
|
43
|
+
"method": "GET",
|
|
44
|
+
"path": "/api/users",
|
|
45
|
+
"status_code": 200,
|
|
46
|
+
"duration_ms": 1.4,
|
|
47
|
+
"request_id": "abc-123",
|
|
48
|
+
"service": "ft79-app",
|
|
49
|
+
"version": "1.0.0"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## セキュリティ設計(良い点)
|
|
56
|
+
|
|
57
|
+
### リクエストボディをログしない ✅
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# POST /api/users {"name": "alice", "password": "super-secret-pw"}
|
|
61
|
+
# → ログには password が含まれない
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
nene2 は `url.path` のみをログし、ボディも Cookie も Authorization ヘッダーも記録しない。
|
|
65
|
+
Django の `request_finished` シグナルや Flask-Login のデフォルト設定で
|
|
66
|
+
機密情報がログに漏洩するケースと対照的に、nene2 はデフォルトで安全。
|
|
67
|
+
|
|
68
|
+
### クエリパラメーターをログしない ✅
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
GET /api/users?secret_token=hidden
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`url.path` は `/api/users` のみを返すため、クエリパラメーターはログに含まれない。
|
|
75
|
+
URLにAPIキーを含める(推奨されないが)パターンでも機密情報が漏洩しない。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 発見した問題
|
|
80
|
+
|
|
81
|
+
### 問題1: extra_context が静的のみ — リクエストごとの動的コンテキストが追加できない
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# 現状: アプリ起動時の静的な値のみ
|
|
85
|
+
app.add_middleware(
|
|
86
|
+
RequestLoggingMiddleware,
|
|
87
|
+
extra_context={"service": "my-api", "version": "1.0.0"},
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# 欲しいが実現できない: リクエストごとの動的な値
|
|
93
|
+
# → JWT から取り出した user_id をログに含めたい
|
|
94
|
+
# → テナントIDをログに含めたい
|
|
95
|
+
app.add_middleware(
|
|
96
|
+
RequestLoggingMiddleware,
|
|
97
|
+
context_extractor=lambda request: {"user_id": get_user_id(request)}, # 非対応
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`structlog.contextvars.bind_contextvars()` を直接使えば可能だが、
|
|
102
|
+
その場合はミドルウェアを自作する必要がある。
|
|
103
|
+
|
|
104
|
+
### 問題2: デバッグ時のリクエストボディログ方法が不明
|
|
105
|
+
|
|
106
|
+
本番では不要だが、開発時にリクエストボディをログしたいケースがある。
|
|
107
|
+
現状では nene2 でこれを行う方法がなく、
|
|
108
|
+
カスタムミドルウェアを追加するか `add_route_logging` のようなデコレーターを自作する必要がある。
|
|
109
|
+
|
|
110
|
+
### 問題3: configure_for_testing() を使わないと caplog でキャプチャできない
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# conftest.py に必須
|
|
114
|
+
from nene2.log import configure_for_testing
|
|
115
|
+
configure_for_testing()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
この設定を忘れると `caplog` で nene2 のログがキャプチャできず、
|
|
119
|
+
「テストしてるのにログが出ない」という混乱を招く。
|
|
120
|
+
ドキュメントへの明示が必要。
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## テスト結果(全14件パス)
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
test_request_received_logged PASSED
|
|
128
|
+
test_request_completed_logged PASSED
|
|
129
|
+
test_method_and_path_in_log PASSED
|
|
130
|
+
test_status_code_in_completed_log PASSED
|
|
131
|
+
test_duration_in_completed_log PASSED
|
|
132
|
+
test_extra_context_in_log PASSED
|
|
133
|
+
test_health_not_logged PASSED
|
|
134
|
+
test_request_body_not_in_log PASSED
|
|
135
|
+
test_request_query_params_in_nene2_log PASSED # nene2はクエリを含まない ✅
|
|
136
|
+
test_error_request_still_logged PASSED
|
|
137
|
+
test_error_request_has_status_500 PASSED
|
|
138
|
+
test_friction_no_request_body_logging_api PASSED
|
|
139
|
+
test_friction_no_response_body_logging PASSED
|
|
140
|
+
test_friction_no_user_id_in_log_without_custom_context PASSED
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 摩擦ポイント一覧
|
|
146
|
+
|
|
147
|
+
| ID | 内容 | 深刻度 |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| F79-1 | extra_context が静的のみ — リクエストごとの動的コンテキスト(user_id等)が追加できない | 中 |
|
|
150
|
+
| F79-2 | デバッグ用のリクエストボディログ方法がない | 低 |
|
|
151
|
+
| F79-3 | configure_for_testing() 未設定だと caplog でログがキャプチャできない | 中 |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 使用感(主観評価)
|
|
156
|
+
|
|
157
|
+
### 直感性 ★★★★★
|
|
158
|
+
|
|
159
|
+
`setup_middlewares(app)` で自動的にリクエストログが有効になるのは非常に快適。
|
|
160
|
+
`extra_context` で追加フィールドを注入できる設計もわかりやすい。
|
|
161
|
+
`exclude_paths` でヘルスチェックを除外できるのも実務的。
|
|
162
|
+
|
|
163
|
+
### 実害の深刻さ ★★☆☆☆
|
|
164
|
+
|
|
165
|
+
機密情報をログしないデフォルト設計は本番環境で非常に重要で、これは大きな強み。
|
|
166
|
+
`configure_for_testing()` の存在を知らないと pytest で詰まるが、
|
|
167
|
+
ドキュメントに追記すれば解決する軽い問題。
|
|
168
|
+
|
|
169
|
+
### 修正のしやすさ ★★★★☆
|
|
170
|
+
|
|
171
|
+
動的コンテキストの追加は `context_extractor` コールバックを追加すれば実現できる。
|
|
172
|
+
実装は `dispatch()` 内で `context_extractor(request)` を呼んで結果を `bind_contextvars()` に渡すだけ。
|
|
173
|
+
|
|
174
|
+
### 総合コメント
|
|
175
|
+
|
|
176
|
+
RequestLoggingMiddleware は実用的でセキュリティ設計も適切。
|
|
177
|
+
`configure_for_testing()` のドキュメント強化と、
|
|
178
|
+
`context_extractor` パラメーターの追加でさらに実用性が高まる。
|
|
179
|
+
全体的に「使っていて気持ちいい」ミドルウェア。
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 推奨アクション
|
|
184
|
+
|
|
185
|
+
1. **Issue**: `RequestLoggingMiddleware` に `context_extractor` コールバックパラメーターを追加
|
|
186
|
+
2. **Issue**: `configure_for_testing()` の使い方を README のテストセクションに追記
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# FT80: MCP E2E — LocalMcpServer + HttpxMcpClient
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: LocalMcpServer にツールを登録し FastAPI アプリと連携する完全往復の検証
|
|
5
|
+
**バージョン**: v1.8.25
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft80-mcp-e2e/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
nene2 の MCP 機能(`LocalMcpServer` / `HttpxMcpClient`)を実際に使って
|
|
13
|
+
FastAPI アプリと MCP ツールを組み合わせるパターンを検証した。
|
|
14
|
+
基本機能は期待通り動作するが、MCP ツールから型付きオブジェクトを返す際の
|
|
15
|
+
手動 JSON 変換の要求と、ツール一覧取得 API の欠如が摩擦点として判明した。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装パターン
|
|
20
|
+
|
|
21
|
+
### FastAPI + LocalMcpServer 共存
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from nene2.mcp import LocalMcpServer
|
|
25
|
+
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
mcp_server = LocalMcpServer("recipe-assistant", "Recipe management assistant")
|
|
28
|
+
|
|
29
|
+
@app.post("/recipes")
|
|
30
|
+
def create_recipe(body: RecipeBody) -> JSONResponse:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@mcp_server.tool("Create a new recipe")
|
|
34
|
+
def create_new_recipe(title: str, ingredients: list[str]) -> str:
|
|
35
|
+
r = api_client.post("/recipes", json={"title": title, "ingredients": ingredients})
|
|
36
|
+
return r.text # JSON 文字列を返す
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### McpHttpError を使ったエラーハンドリング
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@mcp_server.tool("Get a recipe by ID")
|
|
43
|
+
def get_recipe_by_id(recipe_id: int) -> str:
|
|
44
|
+
r = client.get(f"/recipes/{recipe_id}")
|
|
45
|
+
if r.status_code == 404:
|
|
46
|
+
raise McpHttpError(404, f"Recipe {recipe_id} not found")
|
|
47
|
+
return r.text
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### HttpxMcpClient をテストトランスポートでモック
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
transport = httpx.MockTransport(handler=lambda req: httpx.Response(200, json={}))
|
|
54
|
+
http_client = HttpxMcpClient(transport=transport)
|
|
55
|
+
response = http_client.get("http://testserver", "/recipes")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 発見した問題
|
|
61
|
+
|
|
62
|
+
### 問題1: MCP ツールは文字列しか返せない(手動 JSON 変換が必要)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# ❌ UseCase の Output dataclass を直接返せない
|
|
66
|
+
@mcp_server.tool("Get recipe")
|
|
67
|
+
def get_recipe() -> RecipeOutput: # 型エラー
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
# ✅ JSON 文字列に変換して返す(手動変換が必要)
|
|
71
|
+
@mcp_server.tool("Get recipe")
|
|
72
|
+
def get_recipe() -> str:
|
|
73
|
+
output = use_case.execute(input_)
|
|
74
|
+
return json.dumps(dataclasses.asdict(output))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
UseCase の型安全性が MCP ツール境界で失われる。
|
|
78
|
+
FastAPI の `response_model` のような仕組みが MCP にはない。
|
|
79
|
+
|
|
80
|
+
### 問題2: ツール一覧取得 API がない
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
mcp_server = LocalMcpServer("test-server")
|
|
84
|
+
|
|
85
|
+
@mcp_server.tool("Tool 1")
|
|
86
|
+
def tool_one() -> str:
|
|
87
|
+
return "one"
|
|
88
|
+
|
|
89
|
+
# 登録済みツールの確認手段がない
|
|
90
|
+
# mcp_server.list_tools() → 存在しない
|
|
91
|
+
# mcp_server.tools → 存在しない
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
デバッグ時に「このサーバーに何のツールが登録されているか」を確認できない。
|
|
95
|
+
|
|
96
|
+
### 問題3: Pydantic の `max_items` 非推奨警告
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# ⚠ PydanticDeprecatedSince20 警告
|
|
100
|
+
ingredients: list[str] = Field(max_items=20)
|
|
101
|
+
|
|
102
|
+
# ✅ 正しい書き方(nene2 ドキュメントに記載が必要)
|
|
103
|
+
ingredients: list[str] = Field(max_length=20)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
nene2 のドキュメント/例に `max_items` が残っている可能性がある。
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## テスト結果(全17件パス)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
test_create_recipe_returns_201 PASSED
|
|
114
|
+
test_get_recipe_returns_200 PASSED
|
|
115
|
+
test_get_nonexistent_recipe_returns_404 PASSED
|
|
116
|
+
test_list_recipes_empty PASSED
|
|
117
|
+
test_delete_recipe_returns_204 PASSED
|
|
118
|
+
test_mcp_get_all_recipes_returns_json_string PASSED # ツールが JSON 文字列を返す
|
|
119
|
+
test_mcp_create_recipe_tool PASSED
|
|
120
|
+
test_mcp_get_recipe_by_id_found PASSED
|
|
121
|
+
test_mcp_get_recipe_by_id_not_found_raises PASSED # McpHttpError 正常動作
|
|
122
|
+
test_mcp_delete_recipe_tool PASSED
|
|
123
|
+
test_mcp_delete_nonexistent_raises PASSED
|
|
124
|
+
test_mcp_server_is_importable PASSED
|
|
125
|
+
test_mcp_server_tool_registration PASSED
|
|
126
|
+
test_http_mcp_client_with_test_transport PASSED # MockTransport で DI テスト
|
|
127
|
+
test_http_mcp_client_raise_for_error PASSED
|
|
128
|
+
test_friction_mcp_tool_cannot_return_typed_object PASSED
|
|
129
|
+
test_friction_no_mcp_tool_discovery_api PASSED
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 摩擦ポイント一覧
|
|
135
|
+
|
|
136
|
+
| ID | 内容 | 深刻度 |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| F80-1 | MCP ツールは文字列を返す必要があり、UseCase の型付き Output を直接渡せない | 中 |
|
|
139
|
+
| F80-2 | LocalMcpServer に登録済みツールの一覧取得 API がない | 低 |
|
|
140
|
+
| F80-3 | Pydantic v2 の `max_items` 非推奨(`max_length` に変更必要) | 低 |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 使用感(主観評価)
|
|
145
|
+
|
|
146
|
+
### 直感性 ★★★★☆
|
|
147
|
+
|
|
148
|
+
`@mcp_server.tool("description")` デコレーターは非常に直感的で、
|
|
149
|
+
FastAPI の `@app.get("/path")` と同じ感覚で使える。
|
|
150
|
+
`McpHttpError` と `raise_for_error()` のパターンも明瞭。
|
|
151
|
+
|
|
152
|
+
### 実害の深刻さ ★★☆☆☆
|
|
153
|
+
|
|
154
|
+
文字列変換の必要性は設計上仕方ない(MCP プロトコルの制約)。
|
|
155
|
+
`json.dumps(dataclasses.asdict(output))` は1行で書けるので実際の摩擦は小さい。
|
|
156
|
+
ツール一覧確認はデバッグ時の不便さのみで、本番には影響しない。
|
|
157
|
+
|
|
158
|
+
### 修正のしやすさ ★★★★★
|
|
159
|
+
|
|
160
|
+
どれも小さな改善で対応できる:
|
|
161
|
+
- `list_tools()` メソッドを `LocalMcpServer` に追加するだけ
|
|
162
|
+
- ドキュメントに `json.dumps(dataclasses.asdict(output))` のパターンを記載するだけ
|
|
163
|
+
|
|
164
|
+
### 総合コメント
|
|
165
|
+
|
|
166
|
+
MCP 機能は他の Python フレームワークにはない nene2 の差別化ポイント。
|
|
167
|
+
FastAPI との共存パターン、`McpHttpError` のエラーハンドリング、
|
|
168
|
+
`HttpxMcpClient` の `MockTransport` DI テストパターンは非常によくできている。
|
|
169
|
+
「他のフレームワークに乗り換えようと思わない」と思わせる独自の価値がある。
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 推奨アクション
|
|
174
|
+
|
|
175
|
+
1. **Issue**: `LocalMcpServer` に `list_tools()` メソッドを追加(デバッグ用)
|
|
176
|
+
2. **minor**: Pydantic v2 の `max_items` → `max_length` への更新(ドキュメント確認)
|
|
@@ -27,6 +27,22 @@ class LocalMcpServer:
|
|
|
27
27
|
"""Register a function as an MCP tool."""
|
|
28
28
|
return self._mcp.tool(description=description)
|
|
29
29
|
|
|
30
|
+
def list_tools(self) -> list[str]:
|
|
31
|
+
"""Return the names of all registered tools.
|
|
32
|
+
|
|
33
|
+
Useful for debugging and introspection::
|
|
34
|
+
|
|
35
|
+
server = LocalMcpServer("my-server")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@server.tool("Get data")
|
|
39
|
+
def get_data() -> str: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
print(server.list_tools()) # ["get_data"]
|
|
43
|
+
"""
|
|
44
|
+
return [t.name for t in self._mcp._tool_manager.list_tools()]
|
|
45
|
+
|
|
30
46
|
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = "stdio") -> None:
|
|
31
47
|
"""Start the MCP server. Blocks until the client disconnects."""
|
|
32
48
|
self._mcp.run(transport=transport)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Request logging middleware using structlog."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import time
|
|
5
|
+
from collections.abc import Callable
|
|
4
6
|
|
|
5
7
|
import structlog
|
|
6
8
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
@@ -20,6 +22,21 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
20
22
|
Useful for high-frequency health-check endpoints where log noise is unwanted.
|
|
21
23
|
extra_context: Additional key-value pairs bound to every request log entry
|
|
22
24
|
(e.g. ``{"service": "my-api", "version": "1.0.0"}``).
|
|
25
|
+
context_extractor: Optional callable ``(request) -> dict[str, str]`` that
|
|
26
|
+
returns per-request dynamic context (e.g. user ID from JWT, tenant ID).
|
|
27
|
+
The returned dict is merged into the log context for each request::
|
|
28
|
+
|
|
29
|
+
def get_log_context(request: Request) -> dict[str, str]:
|
|
30
|
+
return {"user_id": request.headers.get("X-User-Id", "anonymous")}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app.add_middleware(
|
|
34
|
+
RequestLoggingMiddleware,
|
|
35
|
+
context_extractor=get_log_context,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
If the extractor raises an exception, it is silently skipped so that
|
|
39
|
+
logging failures never break request handling.
|
|
23
40
|
"""
|
|
24
41
|
|
|
25
42
|
def __init__(
|
|
@@ -27,10 +44,12 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
27
44
|
app: object,
|
|
28
45
|
exclude_paths: list[str] | None = None,
|
|
29
46
|
extra_context: dict[str, str] | None = None,
|
|
47
|
+
context_extractor: Callable[[Request], dict[str, str]] | None = None,
|
|
30
48
|
) -> None:
|
|
31
49
|
super().__init__(app) # type: ignore[arg-type]
|
|
32
50
|
self._exclude_paths = set(exclude_paths or [])
|
|
33
51
|
self._extra_context: dict[str, str] = extra_context or {}
|
|
52
|
+
self._context_extractor = context_extractor
|
|
34
53
|
|
|
35
54
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
36
55
|
if request.url.path in self._exclude_paths:
|
|
@@ -38,11 +57,16 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
38
57
|
|
|
39
58
|
start = time.perf_counter()
|
|
40
59
|
structlog.contextvars.clear_contextvars()
|
|
60
|
+
dynamic_context: dict[str, str] = {}
|
|
61
|
+
if self._context_extractor is not None:
|
|
62
|
+
with contextlib.suppress(Exception):
|
|
63
|
+
dynamic_context = self._context_extractor(request)
|
|
41
64
|
structlog.contextvars.bind_contextvars(
|
|
42
65
|
request_id=request_id_var.get(),
|
|
43
66
|
method=request.method,
|
|
44
67
|
path=request.url.path,
|
|
45
68
|
**self._extra_context,
|
|
69
|
+
**dynamic_context,
|
|
46
70
|
)
|
|
47
71
|
logger.info("request.received")
|
|
48
72
|
response = await call_next(request)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for LocalMcpServer."""
|
|
2
|
+
|
|
3
|
+
from nene2.mcp import LocalMcpServer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _make_server_with_tools() -> LocalMcpServer:
|
|
7
|
+
server = LocalMcpServer("test-server")
|
|
8
|
+
|
|
9
|
+
@server.tool("Get data")
|
|
10
|
+
def get_data() -> str:
|
|
11
|
+
return "data"
|
|
12
|
+
|
|
13
|
+
@server.tool("Post data")
|
|
14
|
+
def post_data(value: str) -> str:
|
|
15
|
+
return f"posted: {value}"
|
|
16
|
+
|
|
17
|
+
return server
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_list_tools_returns_registered_names() -> None:
|
|
21
|
+
server = _make_server_with_tools()
|
|
22
|
+
tools = server.list_tools()
|
|
23
|
+
assert "get_data" in tools
|
|
24
|
+
assert "post_data" in tools
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_list_tools_empty_server() -> None:
|
|
28
|
+
server = LocalMcpServer("empty-server")
|
|
29
|
+
assert server.list_tools() == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_list_tools_count() -> None:
|
|
33
|
+
server = _make_server_with_tools()
|
|
34
|
+
assert len(server.list_tools()) == 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_list_tools_returns_list_of_str() -> None:
|
|
38
|
+
server = _make_server_with_tools()
|
|
39
|
+
tools = server.list_tools()
|
|
40
|
+
assert all(isinstance(name, str) for name in tools)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_tool_function_still_callable() -> None:
|
|
44
|
+
"""tool() デコレーターが関数の呼び出し可能性を壊さない。"""
|
|
45
|
+
server = LocalMcpServer("callable-test")
|
|
46
|
+
|
|
47
|
+
@server.tool("Square")
|
|
48
|
+
def square(x: int) -> str:
|
|
49
|
+
return str(x * x)
|
|
50
|
+
|
|
51
|
+
assert square(3) == "9"
|
|
@@ -95,3 +95,81 @@ def test_exclude_paths_passes_requests_through() -> None:
|
|
|
95
95
|
client = TestClient(app)
|
|
96
96
|
assert client.get("/health").status_code == 200
|
|
97
97
|
assert client.get("/items").status_code == 200
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_context_extractor_adds_dynamic_context() -> None:
|
|
101
|
+
"""context_extractor が返した値がログコンテキストに含まれる。"""
|
|
102
|
+
captured: list[dict] = []
|
|
103
|
+
app = FastAPI()
|
|
104
|
+
app.add_middleware(
|
|
105
|
+
RequestLoggingMiddleware,
|
|
106
|
+
context_extractor=lambda req: {"user_id": req.headers.get("X-User-Id", "anon")},
|
|
107
|
+
)
|
|
108
|
+
app.add_middleware(RequestIdMiddleware)
|
|
109
|
+
|
|
110
|
+
@app.get("/ping")
|
|
111
|
+
async def ping() -> JSONResponse:
|
|
112
|
+
captured.append(dict(structlog.contextvars.get_contextvars()))
|
|
113
|
+
return JSONResponse({"ok": True})
|
|
114
|
+
|
|
115
|
+
client = TestClient(app)
|
|
116
|
+
client.get("/ping", headers={"X-User-Id": "user-42"})
|
|
117
|
+
assert captured[0]["user_id"] == "user-42"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_context_extractor_default_is_none() -> None:
|
|
121
|
+
"""context_extractor を省略した場合は従来通り動作する。"""
|
|
122
|
+
captured: list[dict] = []
|
|
123
|
+
app = FastAPI()
|
|
124
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
125
|
+
app.add_middleware(RequestIdMiddleware)
|
|
126
|
+
|
|
127
|
+
@app.get("/ping")
|
|
128
|
+
async def ping() -> JSONResponse:
|
|
129
|
+
captured.append(dict(structlog.contextvars.get_contextvars()))
|
|
130
|
+
return JSONResponse({"ok": True})
|
|
131
|
+
|
|
132
|
+
client = TestClient(app)
|
|
133
|
+
client.get("/ping")
|
|
134
|
+
assert "user_id" not in captured[0]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_context_extractor_exception_is_silently_skipped() -> None:
|
|
138
|
+
"""context_extractor が例外を投げてもリクエスト処理が続行される。"""
|
|
139
|
+
app = FastAPI()
|
|
140
|
+
|
|
141
|
+
def _exploding_extractor(req: object) -> dict[str, str]:
|
|
142
|
+
raise RuntimeError("extractor failure")
|
|
143
|
+
|
|
144
|
+
app.add_middleware(RequestLoggingMiddleware, context_extractor=_exploding_extractor)
|
|
145
|
+
app.add_middleware(RequestIdMiddleware)
|
|
146
|
+
|
|
147
|
+
@app.get("/ping")
|
|
148
|
+
async def ping() -> JSONResponse:
|
|
149
|
+
return JSONResponse({"ok": True})
|
|
150
|
+
|
|
151
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
152
|
+
r = client.get("/ping")
|
|
153
|
+
assert r.status_code == 200 # ログの失敗がリクエスト処理を壊さない
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_context_extractor_merges_with_extra_context() -> None:
|
|
157
|
+
"""context_extractor と extra_context が両方指定されても正しくマージされる。"""
|
|
158
|
+
captured: list[dict] = []
|
|
159
|
+
app = FastAPI()
|
|
160
|
+
app.add_middleware(
|
|
161
|
+
RequestLoggingMiddleware,
|
|
162
|
+
extra_context={"service": "my-api"},
|
|
163
|
+
context_extractor=lambda req: {"user_id": "user-1"},
|
|
164
|
+
)
|
|
165
|
+
app.add_middleware(RequestIdMiddleware)
|
|
166
|
+
|
|
167
|
+
@app.get("/ping")
|
|
168
|
+
async def ping() -> JSONResponse:
|
|
169
|
+
captured.append(dict(structlog.contextvars.get_contextvars()))
|
|
170
|
+
return JSONResponse({"ok": True})
|
|
171
|
+
|
|
172
|
+
client = TestClient(app)
|
|
173
|
+
client.get("/ping")
|
|
174
|
+
assert captured[0]["service"] == "my-api"
|
|
175
|
+
assert captured[0]["user_id"] == "user-1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nene2_python-1.8.24 → nene2_python-1.8.26}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|