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