nene2-python 1.8.18__tar.gz → 1.8.20__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 (261) hide show
  1. {nene2_python-1.8.18 → nene2_python-1.8.20}/CHANGELOG.md +25 -0
  2. {nene2_python-1.8.18 → nene2_python-1.8.20}/PKG-INFO +1 -1
  3. nene2_python-1.8.20/docs/field-trials/2026-05-field-trial-68.md +67 -0
  4. nene2_python-1.8.20/docs/field-trials/2026-05-field-trial-69.md +58 -0
  5. nene2_python-1.8.20/docs/field-trials/2026-05-field-trial-70.md +67 -0
  6. nene2_python-1.8.20/docs/field-trials/2026-05-field-trial-71.md +83 -0
  7. nene2_python-1.8.20/docs/field-trials/2026-05-field-trial-72.md +72 -0
  8. {nene2_python-1.8.18 → nene2_python-1.8.20}/pyproject.toml +1 -1
  9. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/http/problem_details.py +13 -0
  10. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/domain_exception.py +7 -2
  11. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/error_handler.py +44 -1
  12. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_error_handler.py +40 -0
  13. {nene2_python-1.8.18 → nene2_python-1.8.20}/uv.lock +1 -1
  14. {nene2_python-1.8.18 → nene2_python-1.8.20}/.env.example +0 -0
  15. {nene2_python-1.8.18 → nene2_python-1.8.20}/.github/workflows/ci.yml +0 -0
  16. {nene2_python-1.8.18 → nene2_python-1.8.20}/.github/workflows/docs.yml +0 -0
  17. {nene2_python-1.8.18 → nene2_python-1.8.20}/.github/workflows/publish.yml +0 -0
  18. {nene2_python-1.8.18 → nene2_python-1.8.20}/.gitignore +0 -0
  19. {nene2_python-1.8.18 → nene2_python-1.8.20}/.vitepress/config.mts +0 -0
  20. {nene2_python-1.8.18 → nene2_python-1.8.20}/.vitepress/theme/custom.css +0 -0
  21. {nene2_python-1.8.18 → nene2_python-1.8.20}/.vitepress/theme/index.ts +0 -0
  22. {nene2_python-1.8.18 → nene2_python-1.8.20}/AGENTS.md +0 -0
  23. {nene2_python-1.8.18 → nene2_python-1.8.20}/CLAUDE.md +0 -0
  24. {nene2_python-1.8.18 → nene2_python-1.8.20}/Dockerfile +0 -0
  25. {nene2_python-1.8.18 → nene2_python-1.8.20}/LICENSE +0 -0
  26. {nene2_python-1.8.18 → nene2_python-1.8.20}/README.md +0 -0
  27. {nene2_python-1.8.18 → nene2_python-1.8.20}/alembic/README +0 -0
  28. {nene2_python-1.8.18 → nene2_python-1.8.20}/alembic/env.py +0 -0
  29. {nene2_python-1.8.18 → nene2_python-1.8.20}/alembic/script.py.mako +0 -0
  30. {nene2_python-1.8.18 → nene2_python-1.8.20}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  31. {nene2_python-1.8.18 → nene2_python-1.8.20}/alembic.ini +0 -0
  32. {nene2_python-1.8.18 → nene2_python-1.8.20}/compose.yaml +0 -0
  33. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0001-toolchain.md +0 -0
  34. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0002-clean-architecture.md +0 -0
  35. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0003-security-first.md +0 -0
  36. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0004-ai-first-design.md +0 -0
  37. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0005-logging.md +0 -0
  38. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0006-rate-limiting.md +0 -0
  39. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0009-mcp-design.md +0 -0
  40. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0010-async-use-case.md +0 -0
  41. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  42. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/de/index.md +0 -0
  43. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/de/tutorials/getting-started.md +0 -0
  44. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/explanation/architecture.md +0 -0
  45. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/explanation/design-philosophy.md +0 -0
  46. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  47. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  48. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  49. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  50. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  51. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  52. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  53. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  54. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  55. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  56. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  57. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  58. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  59. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  60. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  61. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  62. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  63. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  64. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  65. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  66. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  67. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  68. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  69. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  70. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  71. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  72. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  73. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  74. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  75. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  76. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  77. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  78. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  79. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  80. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  81. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  82. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  83. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  84. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  85. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  86. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  87. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  88. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  89. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  90. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  91. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  92. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  93. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  94. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  95. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  96. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  97. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  98. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  99. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  100. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  101. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  102. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  103. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  104. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  105. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  106. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  107. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  108. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  109. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  110. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  111. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  112. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  113. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/fr/index.md +0 -0
  114. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/fr/tutorials/getting-started.md +0 -0
  115. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/add-new-domain.md +0 -0
  116. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/async-use-case.md +0 -0
  117. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/configure-auth.md +0 -0
  118. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/new-project.md +0 -0
  119. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/problem-details.md +0 -0
  120. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/run-tests.md +0 -0
  121. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/sqlalchemy-repository.md +0 -0
  122. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/how-to/validation.md +0 -0
  123. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/howto/mcp-setup.md +0 -0
  124. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/index.md +0 -0
  125. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/explanation/architecture.md +0 -0
  126. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/explanation/design-philosophy.md +0 -0
  127. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/how-to/add-new-domain.md +0 -0
  128. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/how-to/configure-auth.md +0 -0
  129. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/how-to/new-project.md +0 -0
  130. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/how-to/run-tests.md +0 -0
  131. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  132. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/howto/mcp-setup.md +0 -0
  133. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/index.md +0 -0
  134. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/reference/api.md +0 -0
  135. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/reference/configuration.md +0 -0
  136. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/reference/framework-modules.md +0 -0
  137. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/tutorials/first-domain.md +0 -0
  138. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/ja/tutorials/getting-started.md +0 -0
  139. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/pt-br/index.md +0 -0
  140. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/pt-br/tutorials/getting-started.md +0 -0
  141. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/reference/api.md +0 -0
  142. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/reference/configuration.md +0 -0
  143. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/reference/framework-modules.md +0 -0
  144. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/roadmap.md +0 -0
  145. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/todo/current.md +0 -0
  146. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/tutorials/first-domain.md +0 -0
  147. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/tutorials/getting-started.md +0 -0
  148. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/zh/index.md +0 -0
  149. {nene2_python-1.8.18 → nene2_python-1.8.20}/docs/zh/tutorials/getting-started.md +0 -0
  150. {nene2_python-1.8.18 → nene2_python-1.8.20}/package-lock.json +0 -0
  151. {nene2_python-1.8.18 → nene2_python-1.8.20}/package.json +0 -0
  152. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/__init__.py +0 -0
  153. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/__main__.py +0 -0
  154. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/app.py +0 -0
  155. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/__init__.py +0 -0
  156. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/entity.py +0 -0
  157. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/exceptions.py +0 -0
  158. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/handler.py +0 -0
  159. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/repository.py +0 -0
  160. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/sqlalchemy_repository.py +0 -0
  161. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/comment/use_case.py +0 -0
  162. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/mcp.py +0 -0
  163. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/__init__.py +0 -0
  164. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/async_use_case.py +0 -0
  165. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/entity.py +0 -0
  166. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/exceptions.py +0 -0
  167. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/handler.py +0 -0
  168. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/repository.py +0 -0
  169. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/sqlalchemy_repository.py +0 -0
  170. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/note/use_case.py +0 -0
  171. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/schema.py +0 -0
  172. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/__init__.py +0 -0
  173. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/entity.py +0 -0
  174. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/exceptions.py +0 -0
  175. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/handler.py +0 -0
  176. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/repository.py +0 -0
  177. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/sqlalchemy_repository.py +0 -0
  178. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/example/tag/use_case.py +0 -0
  179. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/__init__.py +0 -0
  180. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/__init__.py +0 -0
  181. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/api_key.py +0 -0
  182. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/bearer_token.py +0 -0
  183. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/exceptions.py +0 -0
  184. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/interfaces.py +0 -0
  185. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/auth/local_verifier.py +0 -0
  186. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/config/__init__.py +0 -0
  187. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/config/settings.py +0 -0
  188. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/__init__.py +0 -0
  189. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/exceptions.py +0 -0
  190. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/health.py +0 -0
  191. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/interfaces.py +0 -0
  192. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/sqlalchemy_executor.py +0 -0
  193. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/database/utils.py +0 -0
  194. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/http/__init__.py +0 -0
  195. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/http/health.py +0 -0
  196. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/http/pagination.py +0 -0
  197. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/log/__init__.py +0 -0
  198. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/log/setup.py +0 -0
  199. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/mcp/__init__.py +0 -0
  200. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/mcp/http_client.py +0 -0
  201. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/mcp/server.py +0 -0
  202. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/__init__.py +0 -0
  203. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/request_id.py +0 -0
  204. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/request_logging.py +0 -0
  205. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/request_size_limit.py +0 -0
  206. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/security_headers.py +0 -0
  207. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/middleware/throttle.py +0 -0
  208. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/py.typed +0 -0
  209. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/use_case/__init__.py +0 -0
  210. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/use_case/protocols.py +0 -0
  211. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/validation/__init__.py +0 -0
  212. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/nene2/validation/exceptions.py +0 -0
  213. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/scripts/__init__.py +0 -0
  214. {nene2_python-1.8.18 → nene2_python-1.8.20}/src/scripts/export_openapi.py +0 -0
  215. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/__init__.py +0 -0
  216. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/__init__.py +0 -0
  217. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/comment/__init__.py +0 -0
  218. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/comment/test_comment_http.py +0 -0
  219. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/comment/test_comment_repository.py +0 -0
  220. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/comment/test_comment_use_case.py +0 -0
  221. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/conftest.py +0 -0
  222. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/note/__init__.py +0 -0
  223. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/note/test_async_note_use_case.py +0 -0
  224. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/note/test_list_notes.py +0 -0
  225. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/note/test_note_repository.py +0 -0
  226. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/tag/__init__.py +0 -0
  227. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/tag/test_tag_repository.py +0 -0
  228. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/tag/test_tags.py +0 -0
  229. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/test_cors.py +0 -0
  230. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/example/test_mcp.py +0 -0
  231. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/__init__.py +0 -0
  232. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/auth/__init__.py +0 -0
  233. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/auth/test_api_key.py +0 -0
  234. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/auth/test_bearer_token.py +0 -0
  235. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/auth/test_token_issuer.py +0 -0
  236. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/config/__init__.py +0 -0
  237. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/config/test_settings.py +0 -0
  238. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/database/__init__.py +0 -0
  239. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/database/test_transaction.py +0 -0
  240. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/database/test_utils.py +0 -0
  241. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/http/__init__.py +0 -0
  242. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/http/test_health.py +0 -0
  243. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/http/test_pagination.py +0 -0
  244. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/http/test_problem_details.py +0 -0
  245. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/log/__init__.py +0 -0
  246. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/log/test_setup.py +0 -0
  247. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/mcp/__init__.py +0 -0
  248. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/mcp/test_http_client.py +0 -0
  249. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/__init__.py +0 -0
  250. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_request_id.py +0 -0
  251. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_request_logging.py +0 -0
  252. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  253. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_security_headers.py +0 -0
  254. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  255. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/middleware/test_throttle.py +0 -0
  256. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/use_case/__init__.py +0 -0
  257. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/use_case/test_protocols.py +0 -0
  258. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/validation/__init__.py +0 -0
  259. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/nene2/validation/test_exceptions.py +0 -0
  260. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/scripts/__init__.py +0 -0
  261. {nene2_python-1.8.18 → nene2_python-1.8.20}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,31 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.20] — 2026-05-20
9
+
10
+ FT72 フィールドトライアル — DatabaseIntegrityException + ErrorHandlerMiddleware.install() 改善。
11
+
12
+ ### Added
13
+ - `ErrorHandlerMiddleware.install(app)` クラスメソッドを追加 (#315) (FT72)
14
+ — `add_middleware` と `add_exception_handler(RequestValidationError)` を一度に設定し、
15
+ Pydantic の 422 バリデーションエラーも nene2 Problem Details 形式に自動統一する
16
+ - Field trial reports: `docs/field-trials/2026-05-field-trial-70.md` 〜 `2026-05-field-trial-72.md` (FT70〜FT72)
17
+
18
+ ---
19
+
20
+ ## [1.8.19] — 2026-05-20
21
+
22
+ FT68 フィールドトライアル — SimpleDomainHandler + extra_factory 実運用検証。
23
+
24
+ ### Changed
25
+ - `problem_details_response` の docstring に `extra` がトップレベルにフラットマージされることを明記 (#308) (FT68)
26
+ - `SimpleDomainHandler` の docstring に `extra_factory` のフラットマージ動作を例示 (#308) (FT68)
27
+
28
+ ### Added
29
+ - Field trial report: `docs/field-trials/2026-05-field-trial-68.md`
30
+
31
+ ---
32
+
8
33
  ## [1.8.18] — 2026-05-20
9
34
 
10
35
  FT67 フィールドトライアル — SqlAlchemyTransactionManager 実運用検証。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.18
3
+ Version: 1.8.20
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,67 @@
1
+ # FT68: SimpleDomainHandler + extra_factory 実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: ドメイン例外ハンドラー (`SimpleDomainHandler`) + `extra_factory` の実運用確認
5
+ **バージョン**: v1.8.18 → v1.8.19 (ドキュメント追加)
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft68-domain-handler/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `nene2.middleware.SimpleDomainHandler` を使って複数のドメイン例外クラスをそれぞれの
13
+ HTTP レスポンスにマッピングし、`extra_factory` による動的フィールドを検証した。
14
+
15
+ ---
16
+
17
+ ## 実装内容
18
+
19
+ - `ArticleNotFoundError`, `ArticleAccessDeniedError`, `ArticleTitleConflictError`: 独自例外クラス
20
+ - 各例外に `SimpleDomainHandler` + `detail_factory` + `extra_factory` を設定
21
+ - `ErrorHandlerMiddleware(domain_handlers=[...])` に渡して自動ハンドリング
22
+
23
+ ---
24
+
25
+ ## テスト結果
26
+
27
+ **7/7 passed** (テスト修正後)
28
+
29
+ | テスト | 結果 |
30
+ |---|---|
31
+ | `test_existing_article_returns_200` | PASSED |
32
+ | `test_not_found_returns_404_with_article_id` | PASSED |
33
+ | `test_access_denied_returns_403` | PASSED |
34
+ | `test_title_conflict_returns_409` | PASSED |
35
+ | `test_detail_factory_populates_detail_field` | PASSED |
36
+ | `test_problem_details_format_compliant` | PASSED |
37
+ | `test_successful_create_returns_201` | PASSED |
38
+
39
+ ---
40
+
41
+ ## Friction Points
42
+
43
+ ### FP-1: `extra_factory` のフィールドがトップレベルにフラットマージされることが不明瞭
44
+
45
+ **発生箇所**: テストで `data["extra"]["article_id"]` とアクセスして `KeyError: 'extra'`
46
+
47
+ **症状**:
48
+ ```python
49
+ # 期待していた構造
50
+ assert data["extra"]["article_id"] == 999 # → KeyError: 'extra'
51
+
52
+ # 実際の構造(RFC 9457 extension members = トップレベル)
53
+ assert data["article_id"] == 999 # ← 正しいアクセス方法
54
+ ```
55
+
56
+ **原因**: `extra` という引数名が「ネストされた辞書」を連想させるが、
57
+ 実際は RFC 9457 仕様の extension members としてトップレベルにフラットマージされる。
58
+
59
+ **修正**: `problem_details_response` と `SimpleDomainHandler` の docstring に
60
+ フラットマージである旨とRFC 9457 extension members との関係を明記 (Issue #308, v1.8.19)
61
+
62
+ ---
63
+
64
+ ## 結論
65
+
66
+ `SimpleDomainHandler` + `extra_factory` は実運用で問題なく使用できる。
67
+ extra フィールドのフラットマージ動作が docstring に明記され、今後は混同を防げる。
@@ -0,0 +1,58 @@
1
+ # FT69: PaginationQueryParser + PaginationResponse 実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: ページネーション機能 (`PaginationQueryParser` + `PaginationResponse`) の実運用確認
5
+ **バージョン**: v1.8.19
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft69-pagination/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `nene2.http.PaginationQueryParser` を FastAPI の `Depends()` で使い、
13
+ `PaginationResponse` でレスポンスを組み立てるパターンを検証した。
14
+ dataclass アイテムの自動シリアライズも確認。
15
+
16
+ ---
17
+
18
+ ## 実装内容
19
+
20
+ - `PaginationQueryParser` を `Annotated[..., Depends()]` で受け取り
21
+ - `pagination.limit` / `pagination.offset` でスライス
22
+ - `PaginationResponse(items, limit, offset, total)` で標準レスポンス
23
+ - `total=None` 時に `total` フィールドが省略されることを確認
24
+
25
+ ---
26
+
27
+ ## テスト結果
28
+
29
+ **8/8 passed**
30
+
31
+ | テスト | 結果 |
32
+ |---|---|
33
+ | `test_default_pagination_returns_first_20` | PASSED |
34
+ | `test_custom_limit_and_offset` | PASSED |
35
+ | `test_limit_too_large_returns_422` | PASSED |
36
+ | `test_limit_zero_returns_422` | PASSED |
37
+ | `test_negative_offset_returns_422` | PASSED |
38
+ | `test_no_total_omits_total_field` | PASSED |
39
+ | `test_last_page_returns_remaining_items` | PASSED |
40
+ | `test_dataclass_items_serialized_to_dict` | PASSED |
41
+
42
+ ---
43
+
44
+ ## Friction Points
45
+
46
+ なし。`PaginationQueryParser` + `PaginationResponse` はすべて直感的に動作した。
47
+
48
+ **特筆点**:
49
+ - `limit=0` や `limit>100` は FastAPI の Query バリデーションが自動で 422 を返す
50
+ - `PaginationResponse.to_dict()` が dataclass インスタンスを自動でシリアライズするのが便利
51
+ - `total=None` 時に `total` フィールドが省略されるため、カーソルページネーションにも対応可能
52
+
53
+ ---
54
+
55
+ ## 結論
56
+
57
+ `PaginationQueryParser` + `PaginationResponse` は実運用で問題なく使用できる。
58
+ FastAPI の Depends と自然に連携し、デフォルト値 (limit=20, max=100) も適切。
@@ -0,0 +1,67 @@
1
+ # FT70: 複数ドメイン連携 実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: 複数ドメイン連携(Post + Comment ネストリソース API)の実運用確認
5
+ **バージョン**: v1.8.19
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft70-multi-domain/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ Post ドメインと Comment ドメインを組み合わせたネストリソース API を実装し、
13
+ 複数のフレームワーク機能を同時に使用した際の統合動作を検証した。
14
+
15
+ ---
16
+
17
+ ## 実装内容
18
+
19
+ - `GET /posts` — `PaginationQueryParser` + `PaginationResponse` でページネーション
20
+ - `POST /posts` — `SqlAlchemyTransactionManager.transactional()` でインサート
21
+ - `GET /posts/{post_id}` — 存在しない場合 404 (Problem Details)
22
+ - `GET /posts/{post_id}/comments` — 親リソース存在チェック付きページネーション
23
+ - `POST /posts/{post_id}/comments` — 親リソース存在チェック付きインサート
24
+ - `RequestIdMiddleware` + `ErrorHandlerMiddleware` をスタック
25
+ - `StaticPool` を使ったインメモリ SQLite(接続共有)
26
+ - `dataclass(frozen=True, slots=True)` を Post / Comment の値オブジェクトに使用
27
+
28
+ ---
29
+
30
+ ## テスト結果
31
+
32
+ **10/10 passed**
33
+
34
+ | テスト | 結果 |
35
+ |---|---|
36
+ | `test_list_posts_returns_paginated` | PASSED |
37
+ | `test_get_post_returns_200` | PASSED |
38
+ | `test_get_nonexistent_post_returns_404` | PASSED |
39
+ | `test_create_post_returns_201` | PASSED |
40
+ | `test_list_comments_for_post` | PASSED |
41
+ | `test_list_comments_for_nonexistent_post_returns_404` | PASSED |
42
+ | `test_create_comment_returns_201` | PASSED |
43
+ | `test_create_comment_for_nonexistent_post_returns_404` | PASSED |
44
+ | `test_request_id_header_present` | PASSED |
45
+ | `test_comment_count_increases_after_create` | PASSED |
46
+
47
+ ---
48
+
49
+ ## Friction Points
50
+
51
+ なし。複数ドメインを組み合わせた際もすべてのフレームワーク機能が正常に動作した。
52
+
53
+ **特筆点**:
54
+ - `StaticPool` は FT67 で習得済みのため、インメモリ SQLite の設定は迷わず実施できた
55
+ - `PaginationQueryParser` + `PaginationResponse` のネストリソース(`/posts/{id}/comments`)への適用も自然
56
+ - `SqlAlchemyTransactionManager.transactional()` で Post と Comment の両インサートが問題なく動作
57
+ - `RequestIdMiddleware` は `ErrorHandlerMiddleware` と共存可能(ミドルウェアスタック順序の制約なし)
58
+ - `problem_details_response()` の 404 応答は親リソース (`post-not-found`) に統一可能
59
+
60
+ ---
61
+
62
+ ## 結論
63
+
64
+ nene2-python の主要機能(Pagination, Transaction, RequestId, ErrorHandler, Problem Details)を
65
+ 複数ドメイン連携シナリオで組み合わせても一切の摩擦がなかった。
66
+ FT57〜FT69 で修正・ドキュメント化された機能がすべて期待通り動作し、
67
+ フレームワークの統合品質が高いことを確認できた。
@@ -0,0 +1,83 @@
1
+ # FT71: 完全レイヤードアーキテクチャ実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: UseCaseProtocol + Repository パターンによる完全レイヤードアーキテクチャ検証
5
+ **バージョン**: v1.8.19
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft71-layered-arch/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ NENE2 の核心設計パターン(HTTP Handler → UseCase → Repository の完全分離)を
13
+ Todo ドメインで実装し、`UseCaseProtocol`・`ValidationException`・`InMemoryRepository` 注入が
14
+ 実運用で問題なく動作することを確認した。
15
+
16
+ ---
17
+
18
+ ## 実装内容
19
+
20
+ ### ドメイン層(HTTP・DB 非依存)
21
+ - `Todo(id, title, done)` — `dataclass(frozen=True, slots=True)` 値オブジェクト
22
+ - Input/Output DTO 群 — `CreateTodoInput/Output`, `GetTodoInput/Output`, `ListTodosInput/Output`, `CompleteTodoInput/Output`
23
+ - `TodoRepositoryInterface` — `abc.ABC` 抽象インターフェース
24
+ - UseCase 群: `CreateTodoUseCase`, `GetTodoUseCase`, `ListTodosUseCase`, `CompleteTodoUseCase`
25
+
26
+ ### インフラ層
27
+ - `InMemoryTodoRepository` — テスト用インメモリ実装
28
+ - `SqlAlchemyTodoRepository` — 本番用 SQLAlchemy 実装
29
+
30
+ ### HTTP 層(thin handler)
31
+ - `create_app(repository=None)` — `repository` が None の場合 SQLAlchemy を使用、注入された場合はそのまま使用
32
+ - Handler は parse → use-case → response の 3 ステップのみ
33
+
34
+ ---
35
+
36
+ ## テスト結果
37
+
38
+ **15/15 passed**
39
+
40
+ | テスト | 結果 | 種別 |
41
+ |---|---|---|
42
+ | `test_create_todo_use_case_returns_new_todo` | PASSED | UseCase 単体 |
43
+ | `test_create_todo_use_case_raises_validation_error_on_blank_title` | PASSED | UseCase 単体 |
44
+ | `test_get_todo_use_case_returns_existing_todo` | PASSED | UseCase 単体 |
45
+ | `test_list_todos_use_case_returns_all_items` | PASSED | UseCase 単体 |
46
+ | `test_complete_todo_use_case_marks_done` | PASSED | UseCase 単体 |
47
+ | `test_list_todos_returns_paginated` | PASSED | HTTP 統合 |
48
+ | `test_get_todo_returns_200` | PASSED | HTTP 統合 |
49
+ | `test_get_nonexistent_todo_returns_404` | PASSED | HTTP 統合 |
50
+ | `test_create_todo_returns_201` | PASSED | HTTP 統合 |
51
+ | `test_create_todo_with_blank_title_returns_422` | PASSED | HTTP 統合 |
52
+ | `test_complete_todo_marks_done` | PASSED | HTTP 統合 |
53
+ | `test_complete_nonexistent_todo_returns_404` | PASSED | HTTP 統合 |
54
+ | `test_list_todos_pagination_offset` | PASSED | HTTP 統合 |
55
+ | `test_request_id_header_present` | PASSED | HTTP 統合 |
56
+ | `test_in_memory_repo_used_for_unit_tests_without_db` | PASSED | DI 注入 |
57
+
58
+ ---
59
+
60
+ ## Friction Points
61
+
62
+ なし。
63
+
64
+ **特筆点**:
65
+ - `ValidationException` を UseCase 内(ドメイン層)で raise しても、
66
+ `ErrorHandlerMiddleware` が自動で 422 Problem Details に変換する。
67
+ ドメイン層が HTTP を知らなくてよい。
68
+ - `create_app(repository=InMemoryTodoRepository(...))` の DI 注入パターンで
69
+ HTTP テストも DB なしで実行できる。5 つの UseCase 単体テストは完全に DB 非依存。
70
+ - `UseCaseProtocol[I, O]` の静的型チェックは `isinstance()` ではなく型注釈で行う。
71
+ `def _assert_protocols() -> None: _: UseCaseProtocol[...] = SomeUseCase(...)` のパターンで
72
+ mypy が protocol 適合を静的に保証する。
73
+ - `InMemoryTodoRepository` の `mark_done()` 実装で `done=True` に更新するため
74
+ リスト全体を再構築するパターン(frozen dataclass なので in-place 変更不可)が自然に強制される。
75
+
76
+ ---
77
+
78
+ ## 結論
79
+
80
+ `UseCaseProtocol` + `Repository Interface` + `InMemoryRepository` の三点セットで
81
+ NENE2 の完全レイヤードアーキテクチャが機能する。
82
+ UseCase 単体テスト(DB なし)と HTTP 統合テスト(SQLAlchemy 経由)を
83
+ 同一 `create_app()` ファクトリで切り替えられる DI パターンが特に有用。
@@ -0,0 +1,72 @@
1
+ # FT72: DatabaseIntegrityException + write() 戻り値パターン実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: ユニーク制約違反 (409) と UPDATE/DELETE の rowcount=0 (404) パターン検証
5
+ **バージョン**: v1.8.19
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft72-db-integrity/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ ユーザー登録 API でユニーク制約違反 → `DatabaseIntegrityException` → 409 のマッピングと、
13
+ `write()` 戻り値 0 を使った UPDATE/DELETE の「対象なし → 404」パターンを実運用で検証した。
14
+ あわせて `request_validation_error_handler` による Pydantic 422 → nene2 形式変換も確認した。
15
+
16
+ ---
17
+
18
+ ## 実装内容
19
+
20
+ - `users` テーブル(`username UNIQUE`, `email UNIQUE`)
21
+ - `POST /users` — INSERT、重複時 `DatabaseIntegrityException` → 409
22
+ - `GET /users/{id}` — 存在しない場合 404
23
+ - `PUT /users/{id}/email` — UPDATE、`write()` 戻り値 0 → 404
24
+ - `DELETE /users/{id}` — DELETE、`write()` 戻り値 0 → 404、成功時 204
25
+ - `SimpleDomainHandler(DatabaseIntegrityException, "user-conflict", "Conflict", 409)` で自動変換
26
+ - `app.add_exception_handler(RequestValidationError, request_validation_error_handler)` で Pydantic 422 形式統一
27
+
28
+ ---
29
+
30
+ ## テスト結果
31
+
32
+ **11/11 passed**
33
+
34
+ | テスト | 結果 |
35
+ |---|---|
36
+ | `test_register_user_returns_201` | PASSED |
37
+ | `test_register_duplicate_username_returns_409` | PASSED |
38
+ | `test_register_duplicate_email_returns_409` | PASSED |
39
+ | `test_get_user_returns_200` | PASSED |
40
+ | `test_get_nonexistent_user_returns_404` | PASSED |
41
+ | `test_update_email_returns_200` | PASSED |
42
+ | `test_update_email_for_nonexistent_user_returns_404` | PASSED |
43
+ | `test_update_to_duplicate_email_returns_409` | PASSED |
44
+ | `test_delete_user_returns_204` | PASSED |
45
+ | `test_delete_nonexistent_user_returns_404` | PASSED |
46
+ | `test_register_invalid_body_returns_422_nene2_format` | PASSED |
47
+
48
+ ---
49
+
50
+ ## Friction Points
51
+
52
+ なし。
53
+
54
+ **特筆点**:
55
+ - `SimpleDomainHandler(DatabaseIntegrityException, ...)` で SQLAlchemy の IntegrityError を
56
+ 透過的に 409 に変換できる。ハンドラー側のコードに `try/except` が不要。
57
+ - `write()` の戻り値セマンティクス(UPDATE/DELETE は rowcount、INSERT は lastrowid)により、
58
+ `if affected == 0: return 404` パターンが自然に書ける。
59
+ - `JSONResponse(None, status_code=204)` が 204 No Content として正しく動作する。
60
+ - `request_validation_error_handler` を `add_exception_handler(RequestValidationError, ...)` で
61
+ 登録することで、Pydantic の 422 バリデーションエラーも nene2 の `validation-failed`
62
+ Problem Details 形式に統一できる。`ErrorHandlerMiddleware` だけではこのケースをカバーできないため、
63
+ 完全な 422 形式統一には両方の登録が必要。
64
+
65
+ ---
66
+
67
+ ## 結論
68
+
69
+ `DatabaseIntegrityException` は `SimpleDomainHandler` で宣言的に 409 にマッピングでき、
70
+ `write()` の 0 戻り値で UPDATE/DELETE の「対象なし → 404」も慣用的に書ける。
71
+ Pydantic バリデーション 422 の形式統一には `request_validation_error_handler` の追加登録が必要な点は
72
+ 覚えておく価値がある。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.18"
3
+ version = "1.8.20"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -60,6 +60,19 @@ def problem_details_response(
60
60
  ) -> JSONResponse:
61
61
  """Build an RFC 9457 Problem Details JSON response.
62
62
 
63
+ Args:
64
+ problem_type: Short identifier appended to ``base_url`` for the ``type`` URI.
65
+ title: Human-readable summary of the error.
66
+ status: HTTP status code.
67
+ detail: Optional human-readable explanation.
68
+ extra: Additional fields merged **at the top level** of the response body
69
+ (RFC 9457 extension members). They are NOT nested under an
70
+ ``"extra"`` key. For example, ``extra={"item_id": 42}`` produces
71
+ ``{"type": "...", "status": 404, "item_id": 42}``.
72
+ Raises ``ValueError`` if any key shadows a reserved field
73
+ (``type``, ``title``, ``status``, ``detail``).
74
+ base_url: Override the base URL for this call only.
75
+
63
76
  ``base_url`` resolution order:
64
77
  1. Explicit ``base_url`` argument
65
78
  2. Value set by :func:`configure_problem_details`
@@ -35,8 +35,11 @@ class SimpleDomainHandler:
35
35
  ]
36
36
  app.add_middleware(ErrorHandlerMiddleware, domain_handlers=handlers)
37
37
 
38
- When you need a dynamic ``detail`` or ``extra`` fields derived from the exception,
39
- pass an ``extra_factory`` callable::
38
+ When you need a dynamic ``detail`` or extra fields derived from the exception,
39
+ pass an ``extra_factory`` callable. The dict returned by ``extra_factory`` is merged
40
+ **at the top level** of the Problem Details response (RFC 9457 extension members) —
41
+ the keys appear directly alongside ``type``, ``title``, etc., NOT nested under
42
+ an ``"extra"`` key::
40
43
 
41
44
  SimpleDomainHandler(
42
45
  PostNotFoundError,
@@ -46,6 +49,8 @@ class SimpleDomainHandler:
46
49
  detail_factory=lambda exc: str(exc),
47
50
  extra_factory=lambda exc: {"post_id": exc.post_id},
48
51
  )
52
+ # Response: {"type": "...", "status": 404, ..., "post_id": 123}
53
+ # NOT: {"type": "...", ..., "extra": {"post_id": 123}}
49
54
  """
50
55
 
51
56
  def __init__(
@@ -11,6 +11,7 @@ from typing import Any
11
11
  from fastapi import Request
12
12
  from fastapi.exceptions import RequestValidationError
13
13
  from fastapi.responses import JSONResponse
14
+ from starlette.applications import Starlette
14
15
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
15
16
  from starlette.responses import Response
16
17
 
@@ -32,7 +33,20 @@ logger = logging.getLogger(__name__)
32
33
 
33
34
 
34
35
  class ErrorHandlerMiddleware(BaseHTTPMiddleware):
35
- """Catch-all error handler that maps exceptions to Problem Details responses."""
36
+ """Catch-all error handler that maps exceptions to Problem Details responses.
37
+
38
+ **Recommended usage** — use :meth:`install` instead of ``add_middleware`` directly.
39
+ ``install`` also registers ``request_validation_error_handler`` so that
40
+ FastAPI's Pydantic validation errors (422) are formatted as nene2 Problem Details::
41
+
42
+ ErrorHandlerMiddleware.install(app)
43
+ # Equivalent to:
44
+ # app.add_middleware(ErrorHandlerMiddleware)
45
+ # app.add_exception_handler(RequestValidationError, request_validation_error_handler)
46
+
47
+ Using ``add_middleware`` directly works, but FastAPI's ``RequestValidationError``
48
+ will be returned in Pydantic's default format rather than nene2 Problem Details.
49
+ """
36
50
 
37
51
  def __init__(
38
52
  self,
@@ -45,6 +59,35 @@ class ErrorHandlerMiddleware(BaseHTTPMiddleware):
45
59
  self.debug = debug
46
60
  self._domain_handlers: list[DomainExceptionHandlerProtocol] = domain_handlers or []
47
61
 
62
+ @classmethod
63
+ def install(
64
+ cls,
65
+ app: object,
66
+ *,
67
+ debug: bool = False,
68
+ domain_handlers: list[DomainExceptionHandlerProtocol] | None = None,
69
+ ) -> None:
70
+ """Add this middleware and register the nene2 validation error handler.
71
+
72
+ Registers both ``ErrorHandlerMiddleware`` and ``request_validation_error_handler``
73
+ so that all 422 responses (Pydantic body validation and ``ValidationException``)
74
+ are formatted as nene2 Problem Details::
75
+
76
+ from nene2.middleware import ErrorHandlerMiddleware
77
+
78
+ app = FastAPI()
79
+ ErrorHandlerMiddleware.install(app)
80
+
81
+ Equivalent to::
82
+
83
+ app.add_middleware(ErrorHandlerMiddleware, debug=debug, domain_handlers=domain_handlers)
84
+ app.add_exception_handler(RequestValidationError, request_validation_error_handler)
85
+ """
86
+ if not isinstance(app, Starlette):
87
+ raise TypeError(f"app must be a Starlette/FastAPI instance, got {type(app)!r}")
88
+ app.add_middleware(cls, debug=debug, domain_handlers=domain_handlers)
89
+ app.add_exception_handler(RequestValidationError, request_validation_error_handler)
90
+
48
91
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
49
92
  try:
50
93
  return await call_next(request)
@@ -1,5 +1,6 @@
1
1
  """Tests for ErrorHandlerMiddleware."""
2
2
 
3
+ import pytest
3
4
  from fastapi import FastAPI
4
5
  from fastapi.exceptions import RequestValidationError
5
6
  from fastapi.responses import JSONResponse
@@ -127,3 +128,42 @@ def test_pydantic_validation_error_field_names_are_extracted() -> None:
127
128
  assert r.status_code == 422
128
129
  fields = [e["field"] for e in r.json()["errors"]]
129
130
  assert "rating" in fields
131
+
132
+
133
+ def _make_app_via_install() -> FastAPI:
134
+ class _Body(BaseModel):
135
+ score: int = Field(ge=0, le=100)
136
+
137
+ app = FastAPI()
138
+ ErrorHandlerMiddleware.install(app)
139
+
140
+ @app.post("/scores")
141
+ async def create_score(body: _Body) -> JSONResponse:
142
+ return JSONResponse({"score": body.score})
143
+
144
+ @app.get("/boom")
145
+ async def boom() -> JSONResponse:
146
+ raise RuntimeError("error via install")
147
+
148
+ return app
149
+
150
+
151
+ def test_install_registers_both_middleware_and_validation_handler() -> None:
152
+ client = TestClient(_make_app_via_install(), raise_server_exceptions=False)
153
+ r = client.post("/scores", json={"score": 999})
154
+ assert r.status_code == 422
155
+ body = r.json()
156
+ assert body["type"].endswith("validation-failed")
157
+ assert "errors" in body
158
+
159
+
160
+ def test_install_middleware_still_catches_runtime_errors() -> None:
161
+ client = TestClient(_make_app_via_install(), raise_server_exceptions=False)
162
+ r = client.get("/boom")
163
+ assert r.status_code == 500
164
+ assert r.json()["type"].endswith("internal-server-error")
165
+
166
+
167
+ def test_install_raises_type_error_for_non_starlette_app() -> None:
168
+ with pytest.raises(TypeError, match="Starlette/FastAPI"):
169
+ ErrorHandlerMiddleware.install(object())
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.8.18"
928
+ version = "1.8.20"
929
929
  source = { editable = "." }
930
930
  dependencies = [
931
931
  { name = "alembic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes