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.
Files changed (194) hide show
  1. {nene2_python-1.4.0 → nene2_python-1.6.0}/.github/workflows/ci.yml +6 -0
  2. {nene2_python-1.4.0 → nene2_python-1.6.0}/.github/workflows/publish.yml +10 -1
  3. nene2_python-1.6.0/CHANGELOG.md +122 -0
  4. {nene2_python-1.4.0 → nene2_python-1.6.0}/PKG-INFO +1 -1
  5. nene2_python-1.6.0/docs/adr/0011-mcp-as-core-dependency.md +63 -0
  6. nene2_python-1.6.0/docs/field-trials/2026-05-field-trial-12.md +58 -0
  7. nene2_python-1.6.0/docs/field-trials/2026-05-field-trial-13.md +54 -0
  8. {nene2_python-1.4.0 → nene2_python-1.6.0}/pyproject.toml +1 -1
  9. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_size_limit.py +19 -1
  10. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/throttle.py +15 -1
  11. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/validation/exceptions.py +20 -2
  12. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_size_limit.py +21 -0
  13. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_throttle.py +32 -0
  14. nene2_python-1.6.0/tests/nene2/validation/test_exceptions.py +46 -0
  15. {nene2_python-1.4.0 → nene2_python-1.6.0}/uv.lock +1 -1
  16. nene2_python-1.4.0/CHANGELOG.md +0 -54
  17. nene2_python-1.4.0/tests/nene2/validation/test_exceptions.py +0 -22
  18. {nene2_python-1.4.0 → nene2_python-1.6.0}/.env.example +0 -0
  19. {nene2_python-1.4.0 → nene2_python-1.6.0}/.github/workflows/docs.yml +0 -0
  20. {nene2_python-1.4.0 → nene2_python-1.6.0}/.gitignore +0 -0
  21. {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/config.mts +0 -0
  22. {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/theme/custom.css +0 -0
  23. {nene2_python-1.4.0 → nene2_python-1.6.0}/.vitepress/theme/index.ts +0 -0
  24. {nene2_python-1.4.0 → nene2_python-1.6.0}/AGENTS.md +0 -0
  25. {nene2_python-1.4.0 → nene2_python-1.6.0}/CLAUDE.md +0 -0
  26. {nene2_python-1.4.0 → nene2_python-1.6.0}/Dockerfile +0 -0
  27. {nene2_python-1.4.0 → nene2_python-1.6.0}/LICENSE +0 -0
  28. {nene2_python-1.4.0 → nene2_python-1.6.0}/README.md +0 -0
  29. {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/README +0 -0
  30. {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/env.py +0 -0
  31. {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/script.py.mako +0 -0
  32. {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  33. {nene2_python-1.4.0 → nene2_python-1.6.0}/alembic.ini +0 -0
  34. {nene2_python-1.4.0 → nene2_python-1.6.0}/compose.yaml +0 -0
  35. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0001-toolchain.md +0 -0
  36. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0002-clean-architecture.md +0 -0
  37. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0003-security-first.md +0 -0
  38. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0004-ai-first-design.md +0 -0
  39. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0005-logging.md +0 -0
  40. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0006-rate-limiting.md +0 -0
  41. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0009-mcp-design.md +0 -0
  42. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/adr/0010-async-use-case.md +0 -0
  43. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/de/index.md +0 -0
  44. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/de/tutorials/getting-started.md +0 -0
  45. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/explanation/architecture.md +0 -0
  46. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/explanation/design-philosophy.md +0 -0
  47. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  48. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  49. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  50. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  51. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  52. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  53. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  54. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  55. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  56. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  57. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  58. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/fr/index.md +0 -0
  59. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/fr/tutorials/getting-started.md +0 -0
  60. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/add-new-domain.md +0 -0
  61. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/configure-auth.md +0 -0
  62. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/new-project.md +0 -0
  63. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/run-tests.md +0 -0
  64. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/how-to/sqlalchemy-repository.md +0 -0
  65. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/howto/mcp-setup.md +0 -0
  66. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/index.md +0 -0
  67. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/explanation/architecture.md +0 -0
  68. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/explanation/design-philosophy.md +0 -0
  69. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/add-new-domain.md +0 -0
  70. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/configure-auth.md +0 -0
  71. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/new-project.md +0 -0
  72. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/run-tests.md +0 -0
  73. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  74. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/howto/mcp-setup.md +0 -0
  75. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/index.md +0 -0
  76. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/api.md +0 -0
  77. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/configuration.md +0 -0
  78. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/reference/framework-modules.md +0 -0
  79. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/tutorials/first-domain.md +0 -0
  80. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/ja/tutorials/getting-started.md +0 -0
  81. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/pt-br/index.md +0 -0
  82. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/pt-br/tutorials/getting-started.md +0 -0
  83. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/api.md +0 -0
  84. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/configuration.md +0 -0
  85. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/reference/framework-modules.md +0 -0
  86. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/roadmap.md +0 -0
  87. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/todo/current.md +0 -0
  88. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/tutorials/first-domain.md +0 -0
  89. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/tutorials/getting-started.md +0 -0
  90. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/zh/index.md +0 -0
  91. {nene2_python-1.4.0 → nene2_python-1.6.0}/docs/zh/tutorials/getting-started.md +0 -0
  92. {nene2_python-1.4.0 → nene2_python-1.6.0}/package-lock.json +0 -0
  93. {nene2_python-1.4.0 → nene2_python-1.6.0}/package.json +0 -0
  94. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/__init__.py +0 -0
  95. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/__main__.py +0 -0
  96. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/app.py +0 -0
  97. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/__init__.py +0 -0
  98. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/entity.py +0 -0
  99. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/exceptions.py +0 -0
  100. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/handler.py +0 -0
  101. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/repository.py +0 -0
  102. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/sqlalchemy_repository.py +0 -0
  103. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/comment/use_case.py +0 -0
  104. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/mcp.py +0 -0
  105. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/__init__.py +0 -0
  106. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/async_use_case.py +0 -0
  107. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/entity.py +0 -0
  108. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/exceptions.py +0 -0
  109. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/handler.py +0 -0
  110. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/repository.py +0 -0
  111. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/sqlalchemy_repository.py +0 -0
  112. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/note/use_case.py +0 -0
  113. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/schema.py +0 -0
  114. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/__init__.py +0 -0
  115. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/entity.py +0 -0
  116. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/exceptions.py +0 -0
  117. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/handler.py +0 -0
  118. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/repository.py +0 -0
  119. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/sqlalchemy_repository.py +0 -0
  120. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/example/tag/use_case.py +0 -0
  121. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/__init__.py +0 -0
  122. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/__init__.py +0 -0
  123. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/api_key.py +0 -0
  124. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/bearer_token.py +0 -0
  125. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/exceptions.py +0 -0
  126. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/interfaces.py +0 -0
  127. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/auth/local_verifier.py +0 -0
  128. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/config/__init__.py +0 -0
  129. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/config/settings.py +0 -0
  130. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/__init__.py +0 -0
  131. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/exceptions.py +0 -0
  132. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/health.py +0 -0
  133. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/interfaces.py +0 -0
  134. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/sqlalchemy_executor.py +0 -0
  135. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/database/utils.py +0 -0
  136. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/__init__.py +0 -0
  137. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/health.py +0 -0
  138. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/pagination.py +0 -0
  139. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/http/problem_details.py +0 -0
  140. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/log/__init__.py +0 -0
  141. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/log/setup.py +0 -0
  142. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/__init__.py +0 -0
  143. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/http_client.py +0 -0
  144. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/mcp/server.py +0 -0
  145. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/__init__.py +0 -0
  146. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/domain_exception.py +0 -0
  147. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/error_handler.py +0 -0
  148. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_id.py +0 -0
  149. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/request_logging.py +0 -0
  150. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/middleware/security_headers.py +0 -0
  151. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/py.typed +0 -0
  152. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/use_case/__init__.py +0 -0
  153. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/use_case/protocols.py +0 -0
  154. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/nene2/validation/__init__.py +0 -0
  155. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/scripts/__init__.py +0 -0
  156. {nene2_python-1.4.0 → nene2_python-1.6.0}/src/scripts/export_openapi.py +0 -0
  157. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/__init__.py +0 -0
  158. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/__init__.py +0 -0
  159. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/__init__.py +0 -0
  160. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_http.py +0 -0
  161. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_repository.py +0 -0
  162. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/comment/test_comment_use_case.py +0 -0
  163. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/conftest.py +0 -0
  164. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/__init__.py +0 -0
  165. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_async_note_use_case.py +0 -0
  166. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_list_notes.py +0 -0
  167. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/note/test_note_repository.py +0 -0
  168. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/__init__.py +0 -0
  169. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/test_tag_repository.py +0 -0
  170. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/tag/test_tags.py +0 -0
  171. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/test_cors.py +0 -0
  172. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/example/test_mcp.py +0 -0
  173. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/__init__.py +0 -0
  174. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/__init__.py +0 -0
  175. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_api_key.py +0 -0
  176. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_bearer_token.py +0 -0
  177. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/auth/test_token_issuer.py +0 -0
  178. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/__init__.py +0 -0
  179. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/test_transaction.py +0 -0
  180. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/database/test_utils.py +0 -0
  181. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/http/__init__.py +0 -0
  182. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/http/test_pagination.py +0 -0
  183. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/mcp/__init__.py +0 -0
  184. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/mcp/test_http_client.py +0 -0
  185. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/__init__.py +0 -0
  186. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_error_handler.py +0 -0
  187. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_id.py +0 -0
  188. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_request_logging.py +0 -0
  189. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/middleware/test_security_headers.py +0 -0
  190. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/use_case/__init__.py +0 -0
  191. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/use_case/test_protocols.py +0 -0
  192. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/nene2/validation/__init__.py +0 -0
  193. {nene2_python-1.4.0 → nene2_python-1.6.0}/tests/scripts/__init__.py +0 -0
  194. {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
- generate_release_notes: true
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.4.0
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 優先度。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.4.0"
3
+ version = "1.6.0"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -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__(self, app: object, *, max_bytes: int = _DEFAULT_MAX_BYTES) -> None:
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
- if not self.field or not self.message or not self.code:
20
- raise ValueError("field, message, and code must be non-empty strings")
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)
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.4.0"
928
+ version = "1.6.0"
929
929
  source = { editable = "." }
930
930
  dependencies = [
931
931
  { name = "alembic" },
@@ -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