nene2-python 1.4.0__tar.gz → 1.6.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.6.0}/.github/workflows/ci.yml +6 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.github/workflows/publish.yml +10 -1
- nene2_python-1.6.0/CHANGELOG.md +122 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/PKG-INFO +1 -1
- nene2_python-1.6.0/docs/adr/0011-mcp-as-core-dependency.md +63 -0
- nene2_python-1.6.0/docs/field-trials/2026-05-field-trial-12.md +58 -0
- nene2_python-1.6.0/docs/field-trials/2026-05-field-trial-13.md +54 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/pyproject.toml +1 -1
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_size_limit.py +19 -1
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/throttle.py +15 -1
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/validation/exceptions.py +20 -2
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_size_limit.py +21 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_throttle.py +32 -0
- nene2_python-1.6.0/tests/nene2/validation/test_exceptions.py +46 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/uv.lock +1 -1
- nene2_python-1.4.0/CHANGELOG.md +0 -54
- nene2_python-1.4.0/tests/nene2/validation/test_exceptions.py +0 -22
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.env.example +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.gitignore +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/config.mts +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/AGENTS.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/CLAUDE.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/Dockerfile +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/LICENSE +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/README.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/README +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/env.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/script.py.mako +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic.ini +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/compose.yaml +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/de/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/fr/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/pt-br/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/api.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/configuration.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/roadmap.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/todo/current.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/zh/index.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/package-lock.json +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/package.json +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/__main__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/app.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/mcp.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/schema.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/entity.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/health.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/health.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/py.typed +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/scripts/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/conftest.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/test_cors.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.4.0 → nene2_python-1.6.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 }}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to nene2-python are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [1.6.0] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT13 (ValidationException実運用) field trial — validation DX improvements.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `ValidationException.single(field, message, code)` — convenience classmethod for single-error raises
|
|
14
|
+
- `ValidationError.__post_init__` now names the specific empty field in the error message
|
|
15
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-13.md`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.5.0] — 2026-05-20
|
|
20
|
+
|
|
21
|
+
FT12 (ThrottleMiddleware + RequestSizeLimitMiddleware) field trial — middleware exclude_paths consistency.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `ThrottleMiddleware` — `exclude_paths` parameter to bypass rate limiting for `/health`, `/docs`, etc.
|
|
25
|
+
- `RequestSizeLimitMiddleware` — same `exclude_paths` parameter for consistency with other middleware
|
|
26
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-12.md`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [1.4.0] — 2026-05-20
|
|
31
|
+
|
|
32
|
+
FT11 (BearerTokenMiddleware + HttpxMcpClient) field trial — auth usability improvements.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- `BearerTokenMiddleware` — `exclude_paths` parameter to bypass auth for `/docs`, `/openapi.json`, `/health`, etc.
|
|
36
|
+
- `ApiKeyAuthMiddleware` — same `exclude_paths` parameter
|
|
37
|
+
- `LocalTokenVerifier.from_env(env_var, *, separator=",")` — create a verifier from a comma-delimited environment variable, with whitespace trimming and custom separator support
|
|
38
|
+
- `docs/how-to/configure-auth.md` — three new sections: `from_env` usage, `exclude_paths` usage, MCP server fail-fast token check pattern
|
|
39
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-11.md`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## [1.3.0] — 2026-05-20
|
|
44
|
+
|
|
45
|
+
FT10 (MySQL adapter) field trial — pagination and serialization improvements.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- `PaginationQueryParser.__init__` — makes the class usable as a FastAPI `Depends()` parameter directly: `Annotated[PaginationQueryParser, Depends()]`
|
|
49
|
+
- `PaginationResponse.to_dict()` — auto-serializes `dataclass(frozen=True, slots=True)` items via `dataclasses.asdict()` (previously raised `TypeError` for slotted dataclasses)
|
|
50
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-10.md`
|
|
51
|
+
- How-to guide: MySQL adapter setup (`docs/how-to/use-mysql.md`)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## [1.2.0] — 2026-05-19
|
|
56
|
+
|
|
57
|
+
FT9 (MCP server standalone) field trial — MCP server and HTTP client improvements.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- `LocalMcpServer` — `port` and `host` constructor parameters (previously hardcoded)
|
|
61
|
+
- `McpHttpError` — raised by `HttpxMcpClient.raise_for_error()` on 4xx/5xx responses, maps to MCP `isError: true`
|
|
62
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-9.md`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## [1.1.0] — 2026-05-19
|
|
67
|
+
|
|
68
|
+
FT8 (nested resources + datetime) field trial — database datetime handling.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
- `nene2.database.utils.parse_db_datetime(value)` — normalises SQLite string timestamps and MySQL naive `datetime` objects to UTC-aware `datetime`; handles both adapters transparently
|
|
72
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-8.md`
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## [1.0.0] — 2026-05-19
|
|
77
|
+
|
|
78
|
+
First stable release. Feature parity with PHP NENE2 v1.4.0.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
**Core framework (`nene2`)**
|
|
83
|
+
- `nene2.use_case` — `UseCaseProtocol[I, O]` and `AsyncUseCaseProtocol[I, O]` (Python 3.12 generics + `@runtime_checkable`)
|
|
84
|
+
- `nene2.auth` — `TokenIssuerProtocol`, `TokenVerificationException`, `BearerTokenMiddleware`, `ApiKeyAuthMiddleware`, `LocalTokenVerifier`
|
|
85
|
+
- `nene2.database` — `DatabaseQueryExecutorInterface`, `DatabaseTransactionManagerInterface` with `transactional(callback)` pattern, `SqlAlchemyQueryExecutor`, `SqlAlchemyTransactionManager`, `_BoundQueryExecutor`
|
|
86
|
+
- `nene2.mcp` — `LocalMcpServer` (FastMCP wrapper), `McpHttpClientProtocol`, `McpHttpResponse`, `HttpxMcpClient`
|
|
87
|
+
- `nene2.middleware` — `ErrorHandlerMiddleware`, `SecurityHeadersMiddleware`, `RequestIdMiddleware`, `RequestLoggingMiddleware`, `RequestSizeLimitMiddleware`, `ThrottleMiddleware`
|
|
88
|
+
- `nene2.http` — `PaginationQueryParser`, `PaginationResponse`, `problem_details_response()` (RFC 9457)
|
|
89
|
+
- `nene2.log` — structlog setup (JSON for production, ConsoleRenderer for local)
|
|
90
|
+
- `nene2.config` — `AppSettings` with Pydantic Settings (SQLite / MySQL / PostgreSQL)
|
|
91
|
+
- `nene2.validation` — `ValidationException`, `ValidationError`
|
|
92
|
+
|
|
93
|
+
**Example application (`example`)**
|
|
94
|
+
- Note, Tag, Comment domains — full CRUD (entity / repository / use_case / handler / SQLAlchemy repository)
|
|
95
|
+
- `AsyncListNotesUseCase`, `AsyncGetNoteUseCase` — demonstrates `AsyncUseCaseProtocol` with `asyncio.gather`
|
|
96
|
+
- `create_mcp_server()` — 15 MCP tools (Note × 5, Tag × 5, Comment × 5)
|
|
97
|
+
- `/health` endpoint with DB health check
|
|
98
|
+
- `export-openapi` script — exports static `docs/openapi.yaml`
|
|
99
|
+
|
|
100
|
+
**Documentation (Diátaxis)**
|
|
101
|
+
- Tutorial: Getting started, Implement a new domain
|
|
102
|
+
- How-to: Add new domain, Configure auth, MCP setup, Run tests
|
|
103
|
+
- Explanation: Architecture overview, Design philosophy & PHP correspondence
|
|
104
|
+
- Reference: Configuration, Framework modules, REST API
|
|
105
|
+
- ADR-0001 through ADR-0010
|
|
106
|
+
|
|
107
|
+
**Infra**
|
|
108
|
+
- GitHub Actions CI (pytest + mypy + ruff + pip-audit)
|
|
109
|
+
- GitHub Pages (VitePress) with Python-themed dark design
|
|
110
|
+
- VitePress docs site with Python Yellow/Blue branding
|
|
111
|
+
|
|
112
|
+
### Architecture highlights
|
|
113
|
+
- Clean Architecture: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy
|
|
114
|
+
- `mypy --strict` on all source files
|
|
115
|
+
- `ruff` lint + format (S, ANN, UP, B, SIM, PL, and more)
|
|
116
|
+
- 165 tests, 92% coverage
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## [0.1.0] — 2026-05-19
|
|
121
|
+
|
|
122
|
+
Initial implementation commit.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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` の有無が揃っていないことが主な摩擦。
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Field Trial 13 — ValidationException 実運用
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**App:** User Registration API(メール・パスワード・年齢の複合バリデーション)
|
|
5
|
+
**Directory:** `/home/xi/docker/nene2-python-FT/ft13-validation/`
|
|
6
|
+
**nene2-python version:** v1.5.0
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
`ValidationException` と `ValidationError` を実際のアプリで使い、
|
|
11
|
+
複数フィールドのバリデーション・複数エラーの集約・レスポンス形式を検証した。
|
|
12
|
+
|
|
13
|
+
## 動作確認結果
|
|
14
|
+
|
|
15
|
+
- 複数フィールドのバリデーションエラーが一度に返ること ✓(fail-fast ではなく fail-all)
|
|
16
|
+
- `ErrorHandlerMiddleware` が `ValidationException` を 422 に変換すること ✓
|
|
17
|
+
- レスポンスが RFC 9457 形式で `errors` 配列を含むこと ✓
|
|
18
|
+
- `ValidationError` の `field`, `message`, `code` が全フィールドで返ること ✓
|
|
19
|
+
|
|
20
|
+
## 摩擦点
|
|
21
|
+
|
|
22
|
+
### FT13-F1 (LOW): 単一エラーでもリストラップが必要
|
|
23
|
+
|
|
24
|
+
単一フィールドのエラーでも `list` でラップする必要があり、やや冗長。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# 現状(毎回リストが必要)
|
|
28
|
+
raise ValidationException([ValidationError("email", "invalid", "invalid_email")])
|
|
29
|
+
|
|
30
|
+
# こうしたい
|
|
31
|
+
raise ValidationException.single("email", "invalid", "invalid_email")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
特に UseCase 層で早期リターンするケース(1つのエラーが致命的で後続チェック不要な場合)に
|
|
35
|
+
`ValidationException([...])` は読みにくい。
|
|
36
|
+
|
|
37
|
+
### FT13-F2 (LOW): ValidationError の空文字エラーメッセージがどのフィールドか不明
|
|
38
|
+
|
|
39
|
+
`ValidationError("", "message", "code")` を作ると `ValueError: field, message, and code must be non-empty strings` が出るが、どのフィールドが空かわからない。開発時のデバッグに時間がかかる。
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# 現状
|
|
43
|
+
ValidationError("", "msg", "code")
|
|
44
|
+
# → ValueError: field, message, and code must be non-empty strings
|
|
45
|
+
|
|
46
|
+
# 改善案
|
|
47
|
+
# → ValueError: ValidationError.field must not be empty
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## まとめ
|
|
51
|
+
|
|
52
|
+
`ValidationException` の基本動作は問題なし。摩擦は軽微で、実務で使うのに大きな障壁はない。
|
|
53
|
+
FT13-F1 の `ValidationException.single()` はよくある単一エラーケースの DX 改善として有用。
|
|
54
|
+
FT13-F2 はデバッグ時の QoL 向上。どちらも LOW 優先度。
|
|
@@ -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:
|
|
@@ -16,8 +16,9 @@ class ValidationError:
|
|
|
16
16
|
code: str
|
|
17
17
|
|
|
18
18
|
def __post_init__(self) -> None:
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
for attr in ("field", "message", "code"):
|
|
20
|
+
if not getattr(self, attr):
|
|
21
|
+
raise ValueError(f"ValidationError.{attr} must not be empty")
|
|
21
22
|
|
|
22
23
|
def to_dict(self) -> dict[str, str]:
|
|
23
24
|
return {"field": self.field, "message": self.message, "code": self.code}
|
|
@@ -27,8 +28,25 @@ class ValidationException(Exception):
|
|
|
27
28
|
"""Raised when one or more validation rules fail.
|
|
28
29
|
|
|
29
30
|
ErrorHandlerMiddleware maps this to a 422 validation-failed Problem Details response.
|
|
31
|
+
|
|
32
|
+
For a single error, use the convenience method::
|
|
33
|
+
|
|
34
|
+
raise ValidationException.single("email", "invalid", "invalid_email")
|
|
35
|
+
|
|
36
|
+
For multiple errors accumulated during validation::
|
|
37
|
+
|
|
38
|
+
errors = []
|
|
39
|
+
if not valid_email:
|
|
40
|
+
errors.append(ValidationError("email", "invalid", "invalid_email"))
|
|
41
|
+
if errors:
|
|
42
|
+
raise ValidationException(errors)
|
|
30
43
|
"""
|
|
31
44
|
|
|
32
45
|
def __init__(self, errors: list[ValidationError]) -> None:
|
|
33
46
|
super().__init__("Validation failed")
|
|
34
47
|
self.errors = errors
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def single(cls, field: str, message: str, code: str) -> "ValidationException":
|
|
51
|
+
"""Convenience constructor for a single validation error."""
|
|
52
|
+
return cls([ValidationError(field, message, code)])
|
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tests for ValidationError and ValidationException."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from nene2.validation.exceptions import ValidationError, ValidationException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_validation_error_to_dict() -> None:
|
|
9
|
+
error = ValidationError(field="title", message="required", code="required")
|
|
10
|
+
assert error.to_dict() == {"field": "title", "message": "required", "code": "required"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_validation_error_rejects_empty_field() -> None:
|
|
14
|
+
with pytest.raises(ValueError):
|
|
15
|
+
ValidationError(field="", message="msg", code="code")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_validation_exception_stores_errors() -> None:
|
|
19
|
+
errors = [ValidationError("f", "m", "c")]
|
|
20
|
+
exc = ValidationException(errors)
|
|
21
|
+
assert exc.errors == errors
|
|
22
|
+
assert str(exc) == "Validation failed"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_validation_error_empty_field_message_names_the_field() -> None:
|
|
26
|
+
with pytest.raises(ValueError, match="ValidationError.field must not be empty"):
|
|
27
|
+
ValidationError(field="", message="msg", code="code")
|
|
28
|
+
|
|
29
|
+
with pytest.raises(ValueError, match="ValidationError.message must not be empty"):
|
|
30
|
+
ValidationError(field="f", message="", code="code")
|
|
31
|
+
|
|
32
|
+
with pytest.raises(ValueError, match="ValidationError.code must not be empty"):
|
|
33
|
+
ValidationError(field="f", message="msg", code="")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_validation_exception_single() -> None:
|
|
37
|
+
exc = ValidationException.single("email", "invalid", "invalid_email")
|
|
38
|
+
assert len(exc.errors) == 1
|
|
39
|
+
assert exc.errors[0].field == "email"
|
|
40
|
+
assert exc.errors[0].message == "invalid"
|
|
41
|
+
assert exc.errors[0].code == "invalid_email"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_validation_exception_single_is_validation_exception() -> None:
|
|
45
|
+
exc = ValidationException.single("f", "m", "c")
|
|
46
|
+
assert isinstance(exc, ValidationException)
|
nene2_python-1.4.0/CHANGELOG.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to nene2-python are documented here.
|
|
4
|
-
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## [1.0.0] — 2026-05-19
|
|
9
|
-
|
|
10
|
-
First stable release. Feature parity with PHP NENE2 v1.4.0.
|
|
11
|
-
|
|
12
|
-
### Added
|
|
13
|
-
|
|
14
|
-
**Core framework (`nene2`)**
|
|
15
|
-
- `nene2.use_case` — `UseCaseProtocol[I, O]` and `AsyncUseCaseProtocol[I, O]` (Python 3.12 generics + `@runtime_checkable`)
|
|
16
|
-
- `nene2.auth` — `TokenIssuerProtocol`, `TokenVerificationException`, `BearerTokenMiddleware`, `ApiKeyAuthMiddleware`, `LocalTokenVerifier`
|
|
17
|
-
- `nene2.database` — `DatabaseQueryExecutorInterface`, `DatabaseTransactionManagerInterface` with `transactional(callback)` pattern, `SqlAlchemyQueryExecutor`, `SqlAlchemyTransactionManager`, `_BoundQueryExecutor`
|
|
18
|
-
- `nene2.mcp` — `LocalMcpServer` (FastMCP wrapper), `McpHttpClientProtocol`, `McpHttpResponse`, `HttpxMcpClient`
|
|
19
|
-
- `nene2.middleware` — `ErrorHandlerMiddleware`, `SecurityHeadersMiddleware`, `RequestIdMiddleware`, `RequestLoggingMiddleware`, `RequestSizeLimitMiddleware`, `ThrottleMiddleware`
|
|
20
|
-
- `nene2.http` — `PaginationQueryParser`, `PaginationResponse`, `problem_details_response()` (RFC 9457)
|
|
21
|
-
- `nene2.log` — structlog setup (JSON for production, ConsoleRenderer for local)
|
|
22
|
-
- `nene2.config` — `AppSettings` with Pydantic Settings (SQLite / MySQL / PostgreSQL)
|
|
23
|
-
- `nene2.validation` — `ValidationException`, `ValidationError`
|
|
24
|
-
|
|
25
|
-
**Example application (`example`)**
|
|
26
|
-
- Note, Tag, Comment domains — full CRUD (entity / repository / use_case / handler / SQLAlchemy repository)
|
|
27
|
-
- `AsyncListNotesUseCase`, `AsyncGetNoteUseCase` — demonstrates `AsyncUseCaseProtocol` with `asyncio.gather`
|
|
28
|
-
- `create_mcp_server()` — 15 MCP tools (Note × 5, Tag × 5, Comment × 5)
|
|
29
|
-
- `/health` endpoint with DB health check
|
|
30
|
-
- `export-openapi` script — exports static `docs/openapi.yaml`
|
|
31
|
-
|
|
32
|
-
**Documentation (Diátaxis)**
|
|
33
|
-
- Tutorial: Getting started, Implement a new domain
|
|
34
|
-
- How-to: Add new domain, Configure auth, MCP setup, Run tests
|
|
35
|
-
- Explanation: Architecture overview, Design philosophy & PHP correspondence
|
|
36
|
-
- Reference: Configuration, Framework modules, REST API
|
|
37
|
-
- ADR-0001 through ADR-0010
|
|
38
|
-
|
|
39
|
-
**Infra**
|
|
40
|
-
- GitHub Actions CI (pytest + mypy + ruff + pip-audit)
|
|
41
|
-
- GitHub Pages (VitePress) with Python-themed dark design
|
|
42
|
-
- VitePress docs site with Python Yellow/Blue branding
|
|
43
|
-
|
|
44
|
-
### Architecture highlights
|
|
45
|
-
- Clean Architecture: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy
|
|
46
|
-
- `mypy --strict` on all source files
|
|
47
|
-
- `ruff` lint + format (S, ANN, UP, B, SIM, PL, and more)
|
|
48
|
-
- 165 tests, 92% coverage
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## [0.1.0] — 2026-05-19
|
|
53
|
-
|
|
54
|
-
Initial implementation commit.
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"""Tests for ValidationError and ValidationException."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from nene2.validation.exceptions import ValidationError, ValidationException
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_validation_error_to_dict() -> None:
|
|
9
|
-
error = ValidationError(field="title", message="required", code="required")
|
|
10
|
-
assert error.to_dict() == {"field": "title", "message": "required", "code": "required"}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_validation_error_rejects_empty_field() -> None:
|
|
14
|
-
with pytest.raises(ValueError):
|
|
15
|
-
ValidationError(field="", message="msg", code="code")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_validation_exception_stores_errors() -> None:
|
|
19
|
-
errors = [ValidationError("f", "m", "c")]
|
|
20
|
-
exc = ValidationException(errors)
|
|
21
|
-
assert exc.errors == errors
|
|
22
|
-
assert str(exc) == "Validation failed"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|