nene2-python 1.4.0__tar.gz → 1.5.0__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.4.0 → nene2_python-1.5.0}/.github/workflows/ci.yml +6 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.github/workflows/publish.yml +10 -1
- {nene2_python-1.4.0 → nene2_python-1.5.0}/CHANGELOG.md +57 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/PKG-INFO +1 -1
- nene2_python-1.5.0/docs/adr/0011-mcp-as-core-dependency.md +63 -0
- nene2_python-1.5.0/docs/field-trials/2026-05-field-trial-12.md +58 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/pyproject.toml +1 -1
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/request_size_limit.py +19 -1
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/throttle.py +15 -1
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_request_size_limit.py +21 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_throttle.py +32 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/uv.lock +1 -1
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.env.example +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.gitignore +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.vitepress/config.mts +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/AGENTS.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/CLAUDE.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/Dockerfile +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/LICENSE +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/README.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/alembic/README +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/alembic/env.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/alembic/script.py.mako +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/alembic.ini +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/compose.yaml +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/de/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/fr/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/pt-br/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/reference/api.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/reference/configuration.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/roadmap.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/todo/current.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/zh/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/package-lock.json +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/package.json +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/__main__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/app.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/mcp.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/note/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/schema.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/health.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/http/health.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/py.typed +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/scripts/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/conftest.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/test_cors.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.5.0}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -31,6 +31,12 @@ jobs:
|
|
|
31
31
|
- name: pytest (with coverage)
|
|
32
32
|
run: uv run pytest
|
|
33
33
|
|
|
34
|
+
- name: coverage gate — domain/use_case layers (90%)
|
|
35
|
+
run: |
|
|
36
|
+
uv run coverage report \
|
|
37
|
+
--include="src/example/*/use_case.py,src/example/*/async_use_case.py,src/example/*/entity.py" \
|
|
38
|
+
--fail-under=90
|
|
39
|
+
|
|
34
40
|
- name: mypy
|
|
35
41
|
run: uv run mypy src/
|
|
36
42
|
|
|
@@ -89,8 +89,17 @@ jobs:
|
|
|
89
89
|
name: dist
|
|
90
90
|
path: dist/
|
|
91
91
|
|
|
92
|
+
- name: Extract release notes from CHANGELOG
|
|
93
|
+
id: changelog
|
|
94
|
+
run: |
|
|
95
|
+
VERSION="${GITHUB_REF_NAME#v}"
|
|
96
|
+
NOTES=$(awk "/^## \[$VERSION\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md | sed '/^[[:space:]]*$/d;/^---$/d' | sed '1{/^[[:space:]]*$/d}')
|
|
97
|
+
echo "notes<<EOF" >> "$GITHUB_OUTPUT"
|
|
98
|
+
echo "$NOTES" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "EOF" >> "$GITHUB_OUTPUT"
|
|
100
|
+
|
|
92
101
|
- name: Create GitHub Release
|
|
93
102
|
uses: softprops/action-gh-release@v2
|
|
94
103
|
with:
|
|
95
104
|
files: dist/*
|
|
96
|
-
|
|
105
|
+
body: ${{ steps.changelog.outputs.notes }}
|
|
@@ -5,6 +5,63 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.5.0] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT12 (ThrottleMiddleware + RequestSizeLimitMiddleware) field trial — middleware exclude_paths consistency.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `ThrottleMiddleware` — `exclude_paths` parameter to bypass rate limiting for `/health`, `/docs`, etc.
|
|
14
|
+
- `RequestSizeLimitMiddleware` — same `exclude_paths` parameter for consistency with other middleware
|
|
15
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-12.md`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.4.0] — 2026-05-20
|
|
20
|
+
|
|
21
|
+
FT11 (BearerTokenMiddleware + HttpxMcpClient) field trial — auth usability improvements.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `BearerTokenMiddleware` — `exclude_paths` parameter to bypass auth for `/docs`, `/openapi.json`, `/health`, etc.
|
|
25
|
+
- `ApiKeyAuthMiddleware` — same `exclude_paths` parameter
|
|
26
|
+
- `LocalTokenVerifier.from_env(env_var, *, separator=",")` — create a verifier from a comma-delimited environment variable, with whitespace trimming and custom separator support
|
|
27
|
+
- `docs/how-to/configure-auth.md` — three new sections: `from_env` usage, `exclude_paths` usage, MCP server fail-fast token check pattern
|
|
28
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-11.md`
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## [1.3.0] — 2026-05-20
|
|
33
|
+
|
|
34
|
+
FT10 (MySQL adapter) field trial — pagination and serialization improvements.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `PaginationQueryParser.__init__` — makes the class usable as a FastAPI `Depends()` parameter directly: `Annotated[PaginationQueryParser, Depends()]`
|
|
38
|
+
- `PaginationResponse.to_dict()` — auto-serializes `dataclass(frozen=True, slots=True)` items via `dataclasses.asdict()` (previously raised `TypeError` for slotted dataclasses)
|
|
39
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-10.md`
|
|
40
|
+
- How-to guide: MySQL adapter setup (`docs/how-to/use-mysql.md`)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [1.2.0] — 2026-05-19
|
|
45
|
+
|
|
46
|
+
FT9 (MCP server standalone) field trial — MCP server and HTTP client improvements.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- `LocalMcpServer` — `port` and `host` constructor parameters (previously hardcoded)
|
|
50
|
+
- `McpHttpError` — raised by `HttpxMcpClient.raise_for_error()` on 4xx/5xx responses, maps to MCP `isError: true`
|
|
51
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-9.md`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## [1.1.0] — 2026-05-19
|
|
56
|
+
|
|
57
|
+
FT8 (nested resources + datetime) field trial — database datetime handling.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- `nene2.database.utils.parse_db_datetime(value)` — normalises SQLite string timestamps and MySQL naive `datetime` objects to UTC-aware `datetime`; handles both adapters transparently
|
|
61
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-8.md`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
8
65
|
## [1.0.0] — 2026-05-19
|
|
9
66
|
|
|
10
67
|
First stable release. Feature parity with PHP NENE2 v1.4.0.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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,63 @@
|
|
|
1
|
+
# ADR-0011: MCP をコア依存として含める
|
|
2
|
+
|
|
3
|
+
## ステータス
|
|
4
|
+
|
|
5
|
+
承認済み (2026-05-20)
|
|
6
|
+
|
|
7
|
+
## コンテキスト
|
|
8
|
+
|
|
9
|
+
外部レビューで「`mcp>=1.0` をコア依存に含めると、MCP を使わない利用者まで FastMCP の依存ツリーを背負う」という指摘を受けた。`nene2[mcp]` optional extras に分離する案が提示された。
|
|
10
|
+
|
|
11
|
+
## 決定
|
|
12
|
+
|
|
13
|
+
`mcp>=1.0` はコア依存として `pyproject.toml` の `dependencies` に置く。optional extras には分離しない。
|
|
14
|
+
|
|
15
|
+
### 理由
|
|
16
|
+
|
|
17
|
+
**1. MCP はフレームワークの設計思想の中核**
|
|
18
|
+
|
|
19
|
+
`CLAUDE.md` の設計哲学に「LLM delivery ready: API・MCP・認証・DB・引き継ぎドキュメントを整合させる」と明記している。MCP は認証・DB と同列のファーストクラス機能であり、"optional な付加機能" ではない。
|
|
20
|
+
|
|
21
|
+
**2. UseCase アーキテクチャとの直結**
|
|
22
|
+
|
|
23
|
+
`nene2.use_case` の `UseCaseProtocol[I, O]` は HTTP と MCP の両方から呼ばれることを前提に設計されている(ADR-0002)。UseCase を MCP ツールとして公開する経路が常に存在することがフレームワークの価値のひとつであり、その経路を extras 依存にすることはアーキテクチャの前提に反する。
|
|
24
|
+
|
|
25
|
+
**3. extras 化のコスト**
|
|
26
|
+
|
|
27
|
+
optional extras にすると `nene2.mcp` モジュール全体の import が条件分岐になる:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
try:
|
|
31
|
+
from fastmcp import FastMCP
|
|
32
|
+
except ImportError:
|
|
33
|
+
raise ImportError("Install nene2[mcp] to use MCP features.")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
フィールドトライアルのたびに MCP を使うため、このパターンを随所に書くことは単なる負債になる。
|
|
37
|
+
|
|
38
|
+
**4. 現在の採用者像**
|
|
39
|
+
|
|
40
|
+
nene2-python の想定ユーザーは「AI エージェント連携を見据えた API バックエンドを構築したい開発者」であり、MCP を使わない利用者は現時点で想定外のユースケースに属する。
|
|
41
|
+
|
|
42
|
+
## 将来の見直し条件
|
|
43
|
+
|
|
44
|
+
以下のいずれかが発生した場合、optional extras 化を検討する:
|
|
45
|
+
|
|
46
|
+
- FastMCP の依存ツリーが著しく肥大化し、インストール時間・容量が問題視される
|
|
47
|
+
- 「認証・DB・ページネーションだけ使いたい、MCP は不要」という実際の利用者フィードバックが複数件寄せられる
|
|
48
|
+
- MCP SDK のライセンスや破壊的変更がコア依存として許容できなくなる
|
|
49
|
+
|
|
50
|
+
その際は `nene2[mcp]` extras として分離し、`nene2.mcp` モジュールのインポートを条件分岐化する。
|
|
51
|
+
|
|
52
|
+
## 代替案
|
|
53
|
+
|
|
54
|
+
| 案 | 却下理由 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `nene2[mcp]` optional extras | 設計思想と不整合・extras 化のコストが現状では割に合わない |
|
|
57
|
+
| MCP モジュールを別パッケージ (`nene2-mcp`) に分離 | フレームワークの一体感が失われる・インストール手順が増える |
|
|
58
|
+
|
|
59
|
+
## 結果
|
|
60
|
+
|
|
61
|
+
- `uv add nene2-python` 一発で MCP 機能を含む完全なスタックが手に入る
|
|
62
|
+
- フィールドトライアルで MCP を毎回インストールする手間がない
|
|
63
|
+
- 将来的に extras 分離が必要になった場合は ADR を更新してこの決定を覆す
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Field Trial 12 — ThrottleMiddleware + RequestSizeLimitMiddleware 実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** Chirp API(短文投稿 API、レート制限 + ペイロード制限付き)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft12-throttle/`
|
|
6
|
+
**nene2-python version:** v1.4.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`ThrottleMiddleware`(固定ウィンドウレート制限)と `RequestSizeLimitMiddleware`(リクエストボディ制限)を実際のアプリに組み込み、動作と摩擦点を確認した。
|
|
11
|
+
|
|
12
|
+
## 動作確認結果
|
|
13
|
+
|
|
14
|
+
- `ThrottleMiddleware(limit=3, window=60)` で3リクエスト後に 429 + `Retry-After` ヘッダーが返ること ✓
|
|
15
|
+
- `RequestSizeLimitMiddleware(max_bytes=100)` でペイロード超過時に 413 が返ること ✓
|
|
16
|
+
- 両ミドルウェアとも Problem Details (RFC 9457) 形式でエラーレスポンスが返ること ✓
|
|
17
|
+
- `AppSettings` に `throttle_limit`, `throttle_window`, `max_body_size` が揃っていること ✓
|
|
18
|
+
|
|
19
|
+
## 摩擦点
|
|
20
|
+
|
|
21
|
+
### FT12-F1 (MEDIUM): ThrottleMiddleware に exclude_paths がない
|
|
22
|
+
|
|
23
|
+
BearerTokenMiddleware・ApiKeyAuthMiddleware は FT11 で `exclude_paths` を追加したが、
|
|
24
|
+
`ThrottleMiddleware` には同パラメータがない。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# やりたいこと: /health はレート制限の対象外にしたい
|
|
28
|
+
app.add_middleware(
|
|
29
|
+
ThrottleMiddleware,
|
|
30
|
+
limit=60,
|
|
31
|
+
window=60,
|
|
32
|
+
exclude_paths=["/health", "/docs"], # ← 存在しない
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
実際には `/health` も 60req/min のレート制限にかかる。ロードバランサーのヘルスチェックが
|
|
37
|
+
高頻度で叩く環境では 429 が返り、インスタンスがダウン扱いになるリスクがある。
|
|
38
|
+
|
|
39
|
+
**再現コード:**
|
|
40
|
+
```python
|
|
41
|
+
app.add_middleware(ThrottleMiddleware, limit=2, window=60)
|
|
42
|
+
# /health を3回叩くと3回目が 429
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### FT12-F2 (MEDIUM): RequestSizeLimitMiddleware に exclude_paths がない
|
|
46
|
+
|
|
47
|
+
同様に `RequestSizeLimitMiddleware` にも `exclude_paths` がない。
|
|
48
|
+
実用上の影響は ThrottleMiddleware より小さいが(GET リクエストはボディなしなので通常問題ない)、
|
|
49
|
+
一貫性の観点から揃えるべき。
|
|
50
|
+
|
|
51
|
+
BearerTokenMiddleware, ApiKeyAuthMiddleware, ThrottleMiddleware がすべて `exclude_paths` を
|
|
52
|
+
持つのに RequestSizeLimitMiddleware だけ持たない状態は混乱を招く。
|
|
53
|
+
|
|
54
|
+
## まとめ
|
|
55
|
+
|
|
56
|
+
基本動作は問題なし。FT11 で auth 系ミドルウェアに `exclude_paths` を追加したが、
|
|
57
|
+
同じ修正が `ThrottleMiddleware` と `RequestSizeLimitMiddleware` にも必要。
|
|
58
|
+
3つのミドルウェアで `exclude_paths` の有無が揃っていないことが主な摩擦。
|
|
@@ -22,13 +22,31 @@ class RequestSizeLimitMiddleware(BaseHTTPMiddleware):
|
|
|
22
22
|
Checks the Content-Length header first for a fast pre-flight reject,
|
|
23
23
|
then reads the actual body to catch chunked-transfer requests that
|
|
24
24
|
omit Content-Length entirely.
|
|
25
|
+
|
|
26
|
+
Use ``exclude_paths`` to bypass the size limit for specific endpoints::
|
|
27
|
+
|
|
28
|
+
app.add_middleware(
|
|
29
|
+
RequestSizeLimitMiddleware,
|
|
30
|
+
max_bytes=1_048_576,
|
|
31
|
+
exclude_paths=["/upload/multipart"],
|
|
32
|
+
)
|
|
25
33
|
"""
|
|
26
34
|
|
|
27
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
app: object,
|
|
38
|
+
*,
|
|
39
|
+
max_bytes: int = _DEFAULT_MAX_BYTES,
|
|
40
|
+
exclude_paths: list[str] | None = None,
|
|
41
|
+
) -> None:
|
|
28
42
|
super().__init__(app) # type: ignore[arg-type]
|
|
29
43
|
self._max_bytes = max_bytes
|
|
44
|
+
self._exclude_paths = set(exclude_paths or [])
|
|
30
45
|
|
|
31
46
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
47
|
+
if request.url.path in self._exclude_paths:
|
|
48
|
+
return await call_next(request)
|
|
49
|
+
|
|
32
50
|
content_length = request.headers.get("Content-Length")
|
|
33
51
|
if content_length is not None:
|
|
34
52
|
try:
|
|
@@ -25,7 +25,17 @@ _DEFAULT_WINDOW = 60 # seconds
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
28
|
-
"""Fixed-window rate limiter keyed by client IP.
|
|
28
|
+
"""Fixed-window rate limiter keyed by client IP.
|
|
29
|
+
|
|
30
|
+
Use ``exclude_paths`` to bypass rate limiting for health checks and API docs::
|
|
31
|
+
|
|
32
|
+
app.add_middleware(
|
|
33
|
+
ThrottleMiddleware,
|
|
34
|
+
limit=60,
|
|
35
|
+
window=60,
|
|
36
|
+
exclude_paths=["/health", "/docs", "/openapi.json"],
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
29
39
|
|
|
30
40
|
def __init__(
|
|
31
41
|
self,
|
|
@@ -33,10 +43,12 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
33
43
|
*,
|
|
34
44
|
limit: int = _DEFAULT_LIMIT,
|
|
35
45
|
window: int = _DEFAULT_WINDOW,
|
|
46
|
+
exclude_paths: list[str] | None = None,
|
|
36
47
|
) -> None:
|
|
37
48
|
super().__init__(app) # type: ignore[arg-type]
|
|
38
49
|
self._limit = limit
|
|
39
50
|
self._window = window
|
|
51
|
+
self._exclude_paths = set(exclude_paths or [])
|
|
40
52
|
self._counts: dict[str, tuple[int, float]] = {}
|
|
41
53
|
self._lock = threading.Lock()
|
|
42
54
|
|
|
@@ -58,6 +70,8 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
|
|
|
58
70
|
return count <= self._limit, remaining
|
|
59
71
|
|
|
60
72
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
73
|
+
if request.url.path in self._exclude_paths:
|
|
74
|
+
return await call_next(request)
|
|
61
75
|
key = self._client_key(request)
|
|
62
76
|
allowed, retry_after = self._is_allowed(key)
|
|
63
77
|
if not allowed:
|
|
@@ -66,3 +66,24 @@ def test_malformed_content_length_is_tolerated() -> None:
|
|
|
66
66
|
headers={"Content-Length": "abc", "Content-Type": "application/octet-stream"},
|
|
67
67
|
)
|
|
68
68
|
assert response.status_code == 200
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_exclude_paths_bypasses_size_limit() -> None:
|
|
72
|
+
app = FastAPI()
|
|
73
|
+
app.add_middleware(
|
|
74
|
+
RequestSizeLimitMiddleware,
|
|
75
|
+
max_bytes=10,
|
|
76
|
+
exclude_paths=["/upload/large"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@app.post("/upload")
|
|
80
|
+
async def upload() -> JSONResponse:
|
|
81
|
+
return JSONResponse({"ok": True})
|
|
82
|
+
|
|
83
|
+
@app.post("/upload/large")
|
|
84
|
+
async def upload_large() -> JSONResponse:
|
|
85
|
+
return JSONResponse({"ok": True})
|
|
86
|
+
|
|
87
|
+
client = TestClient(app)
|
|
88
|
+
assert client.post("/upload", content=b"x" * 100).status_code == 413
|
|
89
|
+
assert client.post("/upload/large", content=b"x" * 100).status_code == 200
|
|
@@ -43,3 +43,35 @@ def test_forwarded_for_header_used_as_key() -> None:
|
|
|
43
43
|
|
|
44
44
|
response2 = client.get("/ping", headers={"X-Forwarded-For": "10.0.0.2"})
|
|
45
45
|
assert response2.status_code == 200
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_exclude_paths_bypasses_throttle() -> None:
|
|
49
|
+
app = FastAPI()
|
|
50
|
+
app.add_middleware(
|
|
51
|
+
ThrottleMiddleware,
|
|
52
|
+
limit=2,
|
|
53
|
+
window=60,
|
|
54
|
+
exclude_paths=["/health"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@app.get("/health")
|
|
58
|
+
async def health() -> JSONResponse:
|
|
59
|
+
return JSONResponse({"status": "ok"})
|
|
60
|
+
|
|
61
|
+
@app.get("/ping")
|
|
62
|
+
async def ping() -> JSONResponse:
|
|
63
|
+
return JSONResponse({"ok": True})
|
|
64
|
+
|
|
65
|
+
client = TestClient(app)
|
|
66
|
+
for _ in range(5):
|
|
67
|
+
assert client.get("/health").status_code == 200
|
|
68
|
+
|
|
69
|
+
client.get("/ping")
|
|
70
|
+
client.get("/ping")
|
|
71
|
+
assert client.get("/ping").status_code == 429
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_exclude_paths_default_is_empty() -> None:
|
|
75
|
+
client = TestClient(_make_app(limit=1))
|
|
76
|
+
client.get("/ping")
|
|
77
|
+
assert client.get("/ping").status_code == 429
|
|
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.4.0 → nene2_python-1.5.0}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|