nene2-python 1.8.30__tar.gz → 1.8.32__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 (308) hide show
  1. {nene2_python-1.8.30 → nene2_python-1.8.32}/CHANGELOG.md +35 -0
  2. {nene2_python-1.8.30 → nene2_python-1.8.32}/PKG-INFO +2 -1
  3. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-88.md +163 -0
  4. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-89.md +184 -0
  5. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-90.md +147 -0
  6. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-91.md +177 -0
  7. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-92.md +97 -0
  8. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-93.md +112 -0
  9. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-94.md +91 -0
  10. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-95.md +121 -0
  11. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-96.md +97 -0
  12. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-97.md +87 -0
  13. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-98.md +97 -0
  14. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-99.md +57 -0
  15. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/async-use-case.md +41 -0
  16. nene2_python-1.8.32/docs/how-to/background-tasks.md +101 -0
  17. nene2_python-1.8.32/docs/how-to/cors.md +87 -0
  18. nene2_python-1.8.32/docs/how-to/domain-events.md +119 -0
  19. nene2_python-1.8.32/docs/how-to/file-upload.md +142 -0
  20. nene2_python-1.8.32/docs/how-to/lifespan-and-app-state.md +109 -0
  21. nene2_python-1.8.32/docs/how-to/response-patterns.md +110 -0
  22. nene2_python-1.8.32/docs/how-to/streaming.md +135 -0
  23. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/validation.md +35 -1
  24. {nene2_python-1.8.30 → nene2_python-1.8.32}/pyproject.toml +2 -1
  25. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/http/__init__.py +2 -0
  26. nene2_python-1.8.32/src/nene2/http/etag.py +23 -0
  27. nene2_python-1.8.32/src/nene2/security/__init__.py +5 -0
  28. nene2_python-1.8.32/src/nene2/security/webhook.py +29 -0
  29. nene2_python-1.8.32/tests/nene2/http/test_etag.py +51 -0
  30. nene2_python-1.8.32/tests/nene2/security/test_webhook.py +62 -0
  31. nene2_python-1.8.32/tests/scripts/__init__.py +0 -0
  32. {nene2_python-1.8.30 → nene2_python-1.8.32}/uv.lock +3 -1
  33. {nene2_python-1.8.30 → nene2_python-1.8.32}/.env.example +0 -0
  34. {nene2_python-1.8.30 → nene2_python-1.8.32}/.github/workflows/ci.yml +0 -0
  35. {nene2_python-1.8.30 → nene2_python-1.8.32}/.github/workflows/docs.yml +0 -0
  36. {nene2_python-1.8.30 → nene2_python-1.8.32}/.github/workflows/publish.yml +0 -0
  37. {nene2_python-1.8.30 → nene2_python-1.8.32}/.gitignore +0 -0
  38. {nene2_python-1.8.30 → nene2_python-1.8.32}/.vitepress/config.mts +0 -0
  39. {nene2_python-1.8.30 → nene2_python-1.8.32}/.vitepress/theme/custom.css +0 -0
  40. {nene2_python-1.8.30 → nene2_python-1.8.32}/.vitepress/theme/index.ts +0 -0
  41. {nene2_python-1.8.30 → nene2_python-1.8.32}/AGENTS.md +0 -0
  42. {nene2_python-1.8.30 → nene2_python-1.8.32}/CLAUDE.md +0 -0
  43. {nene2_python-1.8.30 → nene2_python-1.8.32}/Dockerfile +0 -0
  44. {nene2_python-1.8.30 → nene2_python-1.8.32}/LICENSE +0 -0
  45. {nene2_python-1.8.30 → nene2_python-1.8.32}/README.md +0 -0
  46. {nene2_python-1.8.30 → nene2_python-1.8.32}/alembic/README +0 -0
  47. {nene2_python-1.8.30 → nene2_python-1.8.32}/alembic/env.py +0 -0
  48. {nene2_python-1.8.30 → nene2_python-1.8.32}/alembic/script.py.mako +0 -0
  49. {nene2_python-1.8.30 → nene2_python-1.8.32}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  50. {nene2_python-1.8.30 → nene2_python-1.8.32}/alembic.ini +0 -0
  51. {nene2_python-1.8.30 → nene2_python-1.8.32}/compose.yaml +0 -0
  52. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0001-toolchain.md +0 -0
  53. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0002-clean-architecture.md +0 -0
  54. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0003-security-first.md +0 -0
  55. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0004-ai-first-design.md +0 -0
  56. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0005-logging.md +0 -0
  57. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0006-rate-limiting.md +0 -0
  58. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0009-mcp-design.md +0 -0
  59. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0010-async-use-case.md +0 -0
  60. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  61. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/de/index.md +0 -0
  62. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/de/tutorials/getting-started.md +0 -0
  63. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/explanation/architecture.md +0 -0
  64. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/explanation/design-philosophy.md +0 -0
  65. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  66. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  67. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  68. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  69. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  70. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  71. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  72. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  73. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  74. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  75. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  76. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  77. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  78. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  79. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  80. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  81. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  82. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  83. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  84. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  85. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  86. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  87. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  88. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  89. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  90. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  91. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  92. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  93. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  94. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  95. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  96. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  97. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  98. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  99. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  100. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  101. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  102. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  103. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  104. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  105. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  106. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  107. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  108. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  109. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  110. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  111. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  112. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  113. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  114. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  115. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  116. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  117. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  118. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  119. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  120. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  121. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  122. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  123. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  124. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  125. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  126. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  127. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  128. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  129. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  130. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  131. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  132. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  133. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  134. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  135. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  136. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  137. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  138. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  139. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  140. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  141. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  142. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  143. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  144. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  145. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  146. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  147. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  148. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  149. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  150. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  151. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  152. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/fr/index.md +0 -0
  153. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/fr/tutorials/getting-started.md +0 -0
  154. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/add-new-domain.md +0 -0
  155. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/configure-auth.md +0 -0
  156. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/middleware-stack.md +0 -0
  157. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/new-project.md +0 -0
  158. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/problem-details.md +0 -0
  159. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/run-tests.md +0 -0
  160. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/how-to/sqlalchemy-repository.md +0 -0
  161. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/howto/mcp-setup.md +0 -0
  162. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/index.md +0 -0
  163. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/explanation/architecture.md +0 -0
  164. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/explanation/design-philosophy.md +0 -0
  165. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/how-to/add-new-domain.md +0 -0
  166. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/how-to/configure-auth.md +0 -0
  167. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/how-to/new-project.md +0 -0
  168. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/how-to/run-tests.md +0 -0
  169. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  170. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/howto/mcp-setup.md +0 -0
  171. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/index.md +0 -0
  172. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/reference/api.md +0 -0
  173. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/reference/configuration.md +0 -0
  174. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/reference/framework-modules.md +0 -0
  175. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/tutorials/first-domain.md +0 -0
  176. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/ja/tutorials/getting-started.md +0 -0
  177. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/pt-br/index.md +0 -0
  178. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/pt-br/tutorials/getting-started.md +0 -0
  179. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/reference/api.md +0 -0
  180. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/reference/configuration.md +0 -0
  181. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/reference/framework-modules.md +0 -0
  182. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/roadmap.md +0 -0
  183. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/todo/current.md +0 -0
  184. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/tutorials/first-domain.md +0 -0
  185. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/tutorials/getting-started.md +0 -0
  186. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/zh/index.md +0 -0
  187. {nene2_python-1.8.30 → nene2_python-1.8.32}/docs/zh/tutorials/getting-started.md +0 -0
  188. {nene2_python-1.8.30 → nene2_python-1.8.32}/package-lock.json +0 -0
  189. {nene2_python-1.8.30 → nene2_python-1.8.32}/package.json +0 -0
  190. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/__init__.py +0 -0
  191. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/__main__.py +0 -0
  192. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/app.py +0 -0
  193. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/__init__.py +0 -0
  194. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/entity.py +0 -0
  195. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/exceptions.py +0 -0
  196. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/handler.py +0 -0
  197. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/repository.py +0 -0
  198. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/sqlalchemy_repository.py +0 -0
  199. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/comment/use_case.py +0 -0
  200. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/mcp.py +0 -0
  201. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/__init__.py +0 -0
  202. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/async_use_case.py +0 -0
  203. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/entity.py +0 -0
  204. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/exceptions.py +0 -0
  205. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/handler.py +0 -0
  206. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/repository.py +0 -0
  207. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/sqlalchemy_repository.py +0 -0
  208. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/note/use_case.py +0 -0
  209. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/schema.py +0 -0
  210. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/__init__.py +0 -0
  211. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/entity.py +0 -0
  212. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/exceptions.py +0 -0
  213. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/handler.py +0 -0
  214. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/repository.py +0 -0
  215. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/sqlalchemy_repository.py +0 -0
  216. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/example/tag/use_case.py +0 -0
  217. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/__init__.py +0 -0
  218. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/__init__.py +0 -0
  219. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/api_key.py +0 -0
  220. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/bearer_token.py +0 -0
  221. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/deps.py +0 -0
  222. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/exceptions.py +0 -0
  223. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/interfaces.py +0 -0
  224. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/auth/local_verifier.py +0 -0
  225. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/config/__init__.py +0 -0
  226. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/config/settings.py +0 -0
  227. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/__init__.py +0 -0
  228. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/exceptions.py +0 -0
  229. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/health.py +0 -0
  230. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/interfaces.py +0 -0
  231. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/sqlalchemy_executor.py +0 -0
  232. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/database/utils.py +0 -0
  233. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/http/health.py +0 -0
  234. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/http/pagination.py +0 -0
  235. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/http/problem_details.py +0 -0
  236. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/log/__init__.py +0 -0
  237. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/log/setup.py +0 -0
  238. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/mcp/__init__.py +0 -0
  239. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/mcp/http_client.py +0 -0
  240. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/mcp/server.py +0 -0
  241. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/__init__.py +0 -0
  242. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/domain_exception.py +0 -0
  243. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/error_handler.py +0 -0
  244. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/request_id.py +0 -0
  245. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/request_logging.py +0 -0
  246. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/request_size_limit.py +0 -0
  247. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/security_headers.py +0 -0
  248. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/setup.py +0 -0
  249. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/middleware/throttle.py +0 -0
  250. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/py.typed +0 -0
  251. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/use_case/__init__.py +0 -0
  252. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/use_case/protocols.py +0 -0
  253. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/validation/__init__.py +0 -0
  254. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/nene2/validation/exceptions.py +0 -0
  255. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/scripts/__init__.py +0 -0
  256. {nene2_python-1.8.30 → nene2_python-1.8.32}/src/scripts/export_openapi.py +0 -0
  257. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/__init__.py +0 -0
  258. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/__init__.py +0 -0
  259. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/comment/__init__.py +0 -0
  260. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/comment/test_comment_http.py +0 -0
  261. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/comment/test_comment_repository.py +0 -0
  262. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/comment/test_comment_use_case.py +0 -0
  263. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/conftest.py +0 -0
  264. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/note/__init__.py +0 -0
  265. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/note/test_async_note_use_case.py +0 -0
  266. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/note/test_list_notes.py +0 -0
  267. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/note/test_note_repository.py +0 -0
  268. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/tag/__init__.py +0 -0
  269. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/tag/test_tag_repository.py +0 -0
  270. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/tag/test_tags.py +0 -0
  271. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/test_cors.py +0 -0
  272. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/example/test_mcp.py +0 -0
  273. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/__init__.py +0 -0
  274. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/auth/__init__.py +0 -0
  275. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/auth/test_api_key.py +0 -0
  276. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/auth/test_bearer_token.py +0 -0
  277. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/auth/test_make_require_auth.py +0 -0
  278. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/auth/test_token_issuer.py +0 -0
  279. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/config/__init__.py +0 -0
  280. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/config/test_settings.py +0 -0
  281. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/database/__init__.py +0 -0
  282. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/database/test_transaction.py +0 -0
  283. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/database/test_utils.py +0 -0
  284. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/http/__init__.py +0 -0
  285. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/http/test_health.py +0 -0
  286. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/http/test_pagination.py +0 -0
  287. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/http/test_problem_details.py +0 -0
  288. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/log/__init__.py +0 -0
  289. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/log/test_setup.py +0 -0
  290. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/mcp/__init__.py +0 -0
  291. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/mcp/test_http_client.py +0 -0
  292. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/mcp/test_server.py +0 -0
  293. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/__init__.py +0 -0
  294. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_error_handler.py +0 -0
  295. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_id.py +0 -0
  296. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_logging.py +0 -0
  297. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  298. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_security_headers.py +0 -0
  299. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  300. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  301. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/middleware/test_throttle.py +0 -0
  302. {nene2_python-1.8.30/tests/nene2/use_case → nene2_python-1.8.32/tests/nene2/security}/__init__.py +0 -0
  303. {nene2_python-1.8.30/tests/nene2/validation → nene2_python-1.8.32/tests/nene2/use_case}/__init__.py +0 -0
  304. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/use_case/test_protocols.py +0 -0
  305. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  306. {nene2_python-1.8.30/tests/scripts → nene2_python-1.8.32/tests/nene2/validation}/__init__.py +0 -0
  307. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/nene2/validation/test_exceptions.py +0 -0
  308. {nene2_python-1.8.30 → nene2_python-1.8.32}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.32] — 2026-05-20
9
+
10
+ FT99 フィールドトライアル — Webhook HMAC-SHA256 署名検証パターン検証と nene2.security モジュール追加。
11
+
12
+ ### Added
13
+ - `nene2.security` モジュールを新設 (#404) (FT99)
14
+ - `verify_hmac_signature(body, secret, signature, *, prefix="")` — GitHub/Stripe 方式の Webhook HMAC-SHA256 署名を timing-safe に検証
15
+ - `hmac.compare_digest()` による timing attack 対策済み
16
+ - Field trial report: `docs/field-trials/2026-05-field-trial-99.md` (FT99)
17
+
18
+ ---
19
+
20
+ ## [1.8.31] — 2026-05-20
21
+
22
+ FT97 フィールドトライアル — HTTP キャッシュヘッダーパターン検証と generate_etag() 追加。
23
+
24
+ ### Added
25
+ - `nene2.http.generate_etag(data)` — ETag 生成ユーティリティ関数を追加 (#397) (FT97)
26
+ — dict / list / str / bytes から RFC 9110 形式の ETag 文字列を生成
27
+ — HTTP キャッシュ (`If-None-Match` / `304 Not Modified`) パターンで利用
28
+ - Field trial reports: `docs/field-trials/2026-05-field-trial-92.md` 〜 `2026-05-field-trial-97.md` (FT92〜FT97)
29
+
30
+ ---
31
+
32
+ ## [1.8.30] — 2026-05-20
33
+
34
+ FT87 フィールドトライアル — カスタムレスポンスヘッダーパターン検証と problem_details_response() 改善。
35
+
36
+ ### Added
37
+ - `problem_details_response()` に `headers: dict[str, str] | None = None` パラメーターを追加 (#369) (FT87)
38
+ — エラーレスポンスに `Retry-After`(429)、`WWW-Authenticate`(401)などのカスタムヘッダーを付与可能に
39
+ - Field trial reports: `docs/field-trials/2026-05-field-trial-85.md`, `2026-05-field-trial-86.md`, `2026-05-field-trial-87.md` (FT85, FT86, FT87)
40
+
41
+ ---
42
+
8
43
  ## [1.8.29] — 2026-05-20
9
44
 
10
45
  FT84 フィールドトライアル — 認証 Depends ユーティリティ検証と make_require_auth() 追加。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.30
3
+ Version: 1.8.32
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
@@ -28,6 +28,7 @@ Requires-Dist: httpx>=0.27
28
28
  Requires-Dist: mcp>=1.0
29
29
  Requires-Dist: pydantic-settings>=2.6
30
30
  Requires-Dist: pydantic>=2.9
31
+ Requires-Dist: pyjwt>=2.12.0
31
32
  Requires-Dist: python-multipart>=0.0.12
32
33
  Requires-Dist: pyyaml>=6.0
33
34
  Requires-Dist: sqlalchemy>=2.0.49
@@ -0,0 +1,163 @@
1
+ # FT88: ドメインイベント — 同期イベントバスパターン検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: FastAPI/nene2 でのドメインイベント実装方法と摩擦点
5
+ **バージョン**: v1.8.30
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft88-domain-events/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ ドメインイベント(`OrderPlacedEvent`, `OrderCancelledEvent`)を発行し、
13
+ 通知・監査ログなどのサイドエフェクトを分離するパターンを検証。
14
+ シンプルな同期 `EventBus` を自前実装し、nene2 のアーキテクチャと組み合わせた。
15
+ nene2 に EventBus の仕組みがないため、ガイダンスの欠如が摩擦ポイントとなる。
16
+
17
+ ---
18
+
19
+ ## 実装パターン
20
+
21
+ ### EventBus(自前実装)
22
+
23
+ ```python
24
+ type EventHandler[T] = Callable[[T], None]
25
+
26
+ class EventBus:
27
+ def __init__(self) -> None:
28
+ self._handlers: dict[type, list[EventHandler[Any]]] = defaultdict(list)
29
+ self.published: list[Any] = [] # テスト用
30
+
31
+ def subscribe[T](self, event_type: type[T], handler: EventHandler[T]) -> None:
32
+ self._handlers[event_type].append(handler)
33
+
34
+ def publish(self, event: Any) -> None:
35
+ self.published.append(event)
36
+ for handler in self._handlers[type(event)]:
37
+ handler(event)
38
+ ```
39
+
40
+ ### UseCase でのイベント発行
41
+
42
+ ```python
43
+ @app.post("/orders", response_model=OrderResponse, status_code=201)
44
+ def place_order(body: PlaceOrderBody) -> JSONResponse:
45
+ order = Order(...)
46
+ _orders[order.order_id] = order
47
+
48
+ # ← イベント発行
49
+ event_bus.publish(OrderPlacedEvent(
50
+ order_id=order.order_id,
51
+ customer_id=order.customer_id,
52
+ total=order.total,
53
+ ))
54
+ return JSONResponse({...}, status_code=201)
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 発見した問題
60
+
61
+ ### 問題1: EventBus が nene2 に含まれていない
62
+
63
+ nene2 はドメインイベントのアーキテクチャパターンを提供していない。
64
+ ユーザーが自前で `EventBus` を実装する必要があり、設計が人によって異なる。
65
+ 「どのレイヤーでイベントを発行するか」「イベントをどう注入するか」のガイダンスがない。
66
+
67
+ ### 問題2: 同期イベントバスでは例外がHTTPレスポンスに影響する
68
+
69
+ ```python
70
+ # イベントハンドラーで例外が発生すると...
71
+ def _on_order_placed(event: OrderPlacedEvent) -> None:
72
+ send_email(event) # ← これが失敗すると
73
+
74
+ # → ErrorHandlerMiddleware が 500 を返す
75
+ # → 注文は作成されたのに 500 でクライアントに返る
76
+ ```
77
+
78
+ 「注文は DB に保存されたが通知送信に失敗」の場合、
79
+ 同期バスでは HTTP レスポンスが 500 になってしまう。
80
+ `BackgroundTasks` と組み合わせるか、非同期バスを使う必要があるが、
81
+ nene2 ドキュメントにそのパターンが示されていない。
82
+
83
+ ### 問題3: TestClient.delete() が json= をサポートしない
84
+
85
+ ```python
86
+ # ❌ TypeError: TestClient.delete() got an unexpected keyword argument 'json'
87
+ client.delete("/orders/1", json={"reason": "..."})
88
+
89
+ # ✅ 回避策
90
+ client.request("DELETE", "/orders/1", json={"reason": "..."})
91
+ ```
92
+
93
+ DELETE + リクエストボディのパターンを httpx の TestClient がサポートしない。
94
+ この摩擦は nene2 に起因しないが、DELETE + ボディが必要な API 設計時にハマる。
95
+
96
+ ---
97
+
98
+ ## テスト結果(全14件パス)
99
+
100
+ ```
101
+ test_place_order_returns_201 PASSED
102
+ test_place_order_publishes_event PASSED
103
+ test_place_order_triggers_notification PASSED
104
+ test_place_order_records_audit_log PASSED
105
+ test_cancel_order_returns_204 PASSED
106
+ test_cancel_order_publishes_event PASSED
107
+ test_cancel_order_404 PASSED
108
+ test_cancel_order_does_not_publish_event_on_404 PASSED
109
+ test_event_bus_subscribe_and_publish PASSED
110
+ test_event_bus_multiple_handlers PASSED
111
+ test_event_bus_unrelated_handler_not_called PASSED
112
+ test_event_bus_records_published_events PASSED
113
+ test_friction_event_bus_not_part_of_nene2 PASSED
114
+ test_friction_event_handler_exception_propagates PASSED
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 摩擦ポイント一覧
120
+
121
+ | ID | 内容 | 深刻度 |
122
+ |---|---|---|
123
+ | F88-1 | EventBus が nene2 に含まれていない、ガイダンスもない | 中 |
124
+ | F88-2 | 同期イベントバスではハンドラー例外が HTTP 500 になる、BackgroundTasks との組み合わせガイドがない | 高 |
125
+ | F88-3 | TestClient.delete() が json= をサポートしない(httpx の制限) | 低 |
126
+
127
+ ---
128
+
129
+ ## 使用感(主観評価)
130
+
131
+ ### 直感性 ★★★☆☆
132
+
133
+ EventBus 自体の実装は簡単。Python 3.12 のジェネリクス構文 (`type EventHandler[T]`) で
134
+ 型安全にハンドラーを登録できる。問題はアーキテクチャの指針がないこと。
135
+
136
+ ### 実害の深刻さ ★★★★☆
137
+
138
+ F88-2 の「サイドエフェクト失敗で HTTP 500」は実際の運用で問題になる。
139
+ メール送信・Slack 通知などの外部連携は失敗しても注文作成は成功扱いにしたい。
140
+ `BackgroundTasks` との組み合わせパターンが必要。
141
+
142
+ ### 修正のしやすさ ★★★★☆
143
+
144
+ - F88-1: ドキュメント(アーキテクチャガイド)を追加するだけ
145
+ - F88-2: `BackgroundTasks` とのパターン例を how-to に追加
146
+ - F88-3: コードコメント or ドキュメントで `client.request()` を使うよう明記
147
+
148
+ ### 総合コメント
149
+
150
+ nene2 は「薄い HTTP 層」原則で UseCase とドメインを分離しているため、
151
+ EventBus を UseCase に注入するのは自然に実装できる。
152
+ ただし、「どこで EventBus を初期化して DI するか」が明確でなく、
153
+ グローバル変数 vs lifespan + app.state vs Depends() の選択に迷う。
154
+
155
+ ---
156
+
157
+ ## 推奨アクション
158
+
159
+ 1. **docs**: how-to ガイドに「ドメインイベントパターン」を追加
160
+ - シンプルな同期 EventBus の実装例
161
+ - BackgroundTasks と組み合わせた非同期サイドエフェクトパターン
162
+ - EventBus の DI 方法(lifespan + app.state または module-level singleton)
163
+ 2. **docs**: DELETE + リクエストボディのテスト方法 (`client.request("DELETE", ...)`) を明記
@@ -0,0 +1,184 @@
1
+ # FT89: カスタムバリデーション — Pydantic バリデーターと nene2 ValidationException の統合
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: カスタム Pydantic バリデーター・クロスフィールド検証・nene2 ValidationException の統合
5
+ **バージョン**: v1.8.30
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft89-custom-validation/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ Pydantic v2 の `@field_validator` / `@model_validator` を使ったカスタムバリデーションと
13
+ nene2 の `ValidationException` の統合を検証。
14
+ `@model_validator` で `raise ValueError(...)` すると
15
+ nene2 の Problem Details で `field: "request"` になる摩擦が発見された。
16
+
17
+ ---
18
+
19
+ ## 検証したパターン
20
+
21
+ ### 1. クロスフィールドバリデーション(`@model_validator`)
22
+
23
+ ```python
24
+ class DateRangeBody(BaseModel):
25
+ start_date: date
26
+ end_date: date
27
+
28
+ @model_validator(mode="after")
29
+ def end_date_after_start_date(self) -> "DateRangeBody":
30
+ if self.end_date <= self.start_date:
31
+ raise ValueError("end_date must be after start_date")
32
+ return self
33
+ ```
34
+
35
+ ✅ 動作するが、エラーレスポンスの `field` が `"request"` になる(後述)。
36
+
37
+ ### 2. フィールドレベルバリデーション(`@field_validator`)
38
+
39
+ ```python
40
+ class PasswordBody(BaseModel):
41
+ @field_validator("new_password")
42
+ @classmethod
43
+ def new_password_not_same_as_current(cls, value: str, info: FieldValidationInfo) -> str:
44
+ if info.data.get("current_password") == value:
45
+ raise ValueError("new_password must differ from current_password")
46
+ return value
47
+ ```
48
+
49
+ ✅ `field: "new_password"` が正しく設定される。
50
+
51
+ ### 3. UseCase 層からの ValidationException
52
+
53
+ ```python
54
+ def validate_business_rule(label: str) -> None:
55
+ errors: list[ValidationError] = []
56
+ if label.lower() in RESERVED_LABELS:
57
+ errors.append(ValidationError(
58
+ field="label",
59
+ message=f"'{label}' is reserved.",
60
+ code="reserved-label",
61
+ ))
62
+ if errors:
63
+ raise ValidationException(errors)
64
+ ```
65
+
66
+ ✅ 完全にコントロール可能。field / message / code を自由に設定できる。
67
+
68
+ ---
69
+
70
+ ## 発見した問題
71
+
72
+ ### 問題1: `@model_validator` のエラーが `field: "request"` になる
73
+
74
+ ```python
75
+ # @model_validator で raise ValueError
76
+ # → Pydantic の loc: ("body",) or ()
77
+ # → nene2 の request_validation_error_handler で "body" を除くと空
78
+ # → field = "request" になる
79
+
80
+ # 実際のレスポンス:
81
+ {
82
+ "type": "https://nene2.dev/problems/validation-failed",
83
+ "errors": [
84
+ {
85
+ "field": "request", # ← "end_date" や "end_date_after_start_date" ではない
86
+ "message": "Value error, end_date must be after start_date",
87
+ "code": "value_error"
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ `@field_validator("end_date")` のエラーは `field: "end_date"` になるが、
94
+ `@model_validator` のエラーは `field: "request"` になる。
95
+ クライアント(TypeScript など)がフィールド単位でエラーを表示する場合、
96
+ どのフィールドにエラーが対応するかが不明になる。
97
+
98
+ ### 回避策: UseCase 層で ValidationException を raise
99
+
100
+ ```python
101
+ @app.post("/date-ranges")
102
+ def create_date_range(body: DateRangeBody) -> JSONResponse:
103
+ # Pydantic の @model_validator をやめて UseCase 層で検証
104
+ errors: list[ValidationError] = []
105
+ if body.end_date <= body.start_date:
106
+ errors.append(ValidationError("end_date", "end_date must be after start_date", "invalid"))
107
+ if errors:
108
+ raise ValidationException(errors)
109
+ ...
110
+ ```
111
+
112
+ `ValidationException` を使えば field 名を明示できる。
113
+ ただし Pydantic の `@model_validator` との二重定義になる。
114
+
115
+ ---
116
+
117
+ ## テスト結果(全16件パス)
118
+
119
+ ```
120
+ test_date_range_valid_returns_201 PASSED
121
+ test_date_range_end_before_start_returns_422 PASSED
122
+ test_date_range_same_dates_returns_422 PASSED
123
+ test_date_range_reserved_label_returns_422 PASSED
124
+ test_password_change_valid_returns_204 PASSED
125
+ test_password_change_same_as_current_returns_422 PASSED
126
+ test_password_change_mismatch_returns_422 PASSED
127
+ test_password_too_short_returns_422 PASSED
128
+ test_event_valid_returns_201 PASSED
129
+ test_event_empty_tag_returns_422 PASSED
130
+ test_event_duplicate_tags_returns_422 PASSED
131
+ test_event_too_many_tags_returns_422 PASSED
132
+ test_explicit_error_missing_title_returns_422 PASSED
133
+ test_validation_error_response_is_problem_details_format PASSED
134
+ test_friction_pydantic_error_field_path_in_problem_details PASSED
135
+ test_friction_multiple_validation_errors_all_returned PASSED
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 摩擦ポイント一覧
141
+
142
+ | ID | 内容 | 深刻度 |
143
+ |---|---|---|
144
+ | F89-1 | `@model_validator` のエラーが `field: "request"` になり具体的なフィールド名が失われる | 中 |
145
+ | F89-2 | クロスフィールドバリデーションを Pydantic で書くか UseCase で書くかの指針がない | 低 |
146
+
147
+ ---
148
+
149
+ ## 使用感(主観評価)
150
+
151
+ ### 直感性 ★★★★☆
152
+
153
+ Pydantic v2 の `@field_validator` は直感的で nene2 と自然に統合できる。
154
+ `@model_validator` も動くが、エラーの `field` 名が `"request"` になる不一致が惜しい。
155
+
156
+ ### 実害の深刻さ ★★★☆☆
157
+
158
+ `field: "request"` 問題は、クライアントサイドがフィールド単位でバリデーション
159
+ エラーを表示しない場合(メッセージを表示するだけ)は問題にならない。
160
+ しかし TypeScript/React でフォームのフィールドにエラーを表示する場合は不便。
161
+
162
+ ### 修正のしやすさ ★★★★☆
163
+
164
+ F89-1 はドキュメントだけで対応可能(`@model_validator` より `ValidationException` を推奨と明記)。
165
+ または `request_validation_error_handler` で `model_validator` の loc パターンを
166
+ 検出して field 名を抽出するよう改善することも検討できる。
167
+
168
+ ### 総合コメント
169
+
170
+ nene2 の `ValidationException` + `ValidationError` パターンは優れている。
171
+ フィールド名・メッセージ・コードが明示的で、クライアントに構造化エラーを返せる。
172
+ Pydantic の `@field_validator` との組み合わせも問題なし。
173
+ `@model_validator` の field パス問題は既知の FastAPI/Pydantic の挙動で、
174
+ ドキュメントで「クロスフィールドバリデーションは UseCase で行う」と案内するのが現実的。
175
+
176
+ ---
177
+
178
+ ## 推奨アクション
179
+
180
+ 1. **docs**: how-to ガイドに「カスタムバリデーションパターン」を追加
181
+ - `@field_validator` は nene2 Problem Details と自然に統合される
182
+ - クロスフィールドバリデーションは `ValidationException` で行うと `field` 名が正確
183
+ - `@model_validator` のエラーは `field: "request"` になることを明記
184
+ 2. **docs**: Pydantic バリデーションと UseCase バリデーションの使い分けを説明
@@ -0,0 +1,147 @@
1
+ # FT90: ファイルアップロード — multipart/form-data バリデーションパターン検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: FastAPI UploadFile + nene2 でのファイルアップロードバリデーション
5
+ **バージョン**: v1.8.30
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft90-file-upload/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `UploadFile` を使ったファイルアップロード、コンテントタイプ・サイズバリデーション、
13
+ 複数ファイル一括アップロード、`RequestSizeLimitMiddleware` との共存を検証。
14
+ `setup_middlewares()` のパラメーター名の非自明さと、
15
+ FastAPI がコンテントタイプを自動検証しないことが摩擦として発見された。
16
+
17
+ ---
18
+
19
+ ## 実装パターン
20
+
21
+ ### 単一ファイルアップロード + バリデーション
22
+
23
+ ```python
24
+ _ALLOWED_IMAGE_TYPES = frozenset({"image/jpeg", "image/png", "image/webp", "image/gif"})
25
+ _MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
26
+
27
+ @app.post("/files", response_model=FileResponse, status_code=201)
28
+ async def upload_file(
29
+ file: UploadFile = File(description="ファイル"),
30
+ description: str = Form(default="", max_length=500),
31
+ ) -> JSONResponse:
32
+ if file.content_type not in _ALLOWED_IMAGE_TYPES:
33
+ return problem_details_response(
34
+ "invalid-content-type", "Invalid Content Type", 415,
35
+ headers={"Accept": ", ".join(sorted(_ALLOWED_IMAGE_TYPES))},
36
+ )
37
+ content = await file.read()
38
+ if len(content) > _MAX_FILE_SIZE:
39
+ return problem_details_response("file-too-large", "File Too Large", 413, ...)
40
+ ...
41
+ ```
42
+
43
+ ### nene2 の RequestSizeLimitMiddleware との共存
44
+
45
+ ```python
46
+ # ✅ 正しいパラメーター名: max_request_bytes
47
+ setup_middlewares(app, max_request_bytes=10 * 1024 * 1024)
48
+
49
+ # ❌ 間違い(TypeError になる)
50
+ setup_middlewares(app, max_request_size=10 * 1024 * 1024)
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 発見した問題
56
+
57
+ ### 問題1: `setup_middlewares()` のパラメーター名が非自明
58
+
59
+ `max_request_bytes` という名前は正確だが、
60
+ `max_request_size` や `max_body_size` を試みるユーザーが多い。
61
+ `TypeError: setup_middlewares() got an unexpected keyword argument 'max_request_size'. Did you mean 'max_request_bytes'?`
62
+ というエラーが出るため気付けるが、IDE 補完がないと分かりにくい。
63
+
64
+ ### 問題2: FastAPI は UploadFile のコンテントタイプを自動検証しない
65
+
66
+ ```python
67
+ # コンテントタイプを偽って送信しても FastAPI は受け付ける
68
+ client.post("/files", files={"file": ("evil.exe", content, "image/jpeg")})
69
+ # → content_type="image/jpeg" として処理される(EXE バイナリでも)
70
+ ```
71
+
72
+ 本番環境では `file.content_type` ヘッダーだけでなく、
73
+ ファイルの魔法バイト(magic bytes)を検証する必要があるが、
74
+ nene2 ドキュメントにそのパターンが示されていない。
75
+
76
+ ### 問題3: `async def` ハンドラーが必要
77
+
78
+ `UploadFile.read()` は `await` が必要なため、ハンドラーを `async def` で定義する必要がある。
79
+ nene2 の run_in_threadpool パターン(FT76)との組み合わせで注意が必要。
80
+
81
+ ---
82
+
83
+ ## テスト結果(全13件パス)
84
+
85
+ ```
86
+ test_upload_valid_jpeg_returns_201 PASSED
87
+ test_upload_png_returns_201 PASSED
88
+ test_upload_with_description_form_field PASSED
89
+ test_upload_invalid_content_type_returns_415 PASSED
90
+ test_upload_too_large_returns_413 PASSED
91
+ test_upload_same_content_returns_same_id PASSED
92
+ test_get_uploaded_file_info PASSED
93
+ test_get_nonexistent_file_returns_404 PASSED
94
+ test_list_files_after_upload PASSED
95
+ test_batch_upload_multiple_files PASSED
96
+ test_batch_upload_invalid_type_returns_415 PASSED
97
+ test_friction_upload_file_missing_returns_422 PASSED
98
+ test_friction_content_type_not_validated_by_fastapi PASSED
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 摩擦ポイント一覧
104
+
105
+ | ID | 内容 | 深刻度 |
106
+ |---|---|---|
107
+ | F90-1 | `setup_middlewares()` の `max_request_bytes` パラメーター名が非自明(`max_request_size` と間違えやすい) | 低 |
108
+ | F90-2 | FastAPI は UploadFile のコンテントタイプを自動検証しない(魔法バイト検証のパターン未文書) | 中 |
109
+ | F90-3 | `UploadFile.read()` は `async def` が必要(sync ハンドラーとの混在に注意) | 低 |
110
+
111
+ ---
112
+
113
+ ## 使用感(主観評価)
114
+
115
+ ### 直感性 ★★★★☆
116
+
117
+ `UploadFile` + `File()` のパターンは直感的。
118
+ Form フィールドと同時に送信する場合も `data={"field": "value"}` で簡単。
119
+ `problem_details_response(headers=...)` で 415 の `Accept` ヘッダーも付けられた(FT87 の成果)。
120
+
121
+ ### 実害の深刻さ ★★★☆☆
122
+
123
+ F90-2 はセキュリティ観点で中程度。
124
+ コンテントタイプを偽ったファイルアップロードは現実の攻撃手法。
125
+ nene2 ドキュメントに魔法バイト検証の例があれば防げる。
126
+
127
+ ### 修正のしやすさ ★★★★★
128
+
129
+ F90-1: `max_request_bytes` のドキュメントに `max_request_size` との違いを明記するだけ。
130
+ F90-2: ドキュメントに魔法バイト検証の例を追加。
131
+ F90-3: ドキュメントに `async def` が必要な旨を明記。
132
+
133
+ ### 総合コメント
134
+
135
+ nene2 の `RequestSizeLimitMiddleware` との共存は問題なし。
136
+ `problem_details_response()` を使ったエラー応答(415, 413)もきれいに動く。
137
+ FT87 で追加した `headers` パラメーターが早速役立った(415 に `Accept` ヘッダーを付与)。
138
+
139
+ ---
140
+
141
+ ## 推奨アクション
142
+
143
+ 1. **docs**: how-to に「ファイルアップロード」ガイドを追加
144
+ - `UploadFile` + コンテントタイプ・サイズバリデーションのパターン
145
+ - 魔法バイト(magic bytes)検証の例
146
+ - `RequestSizeLimitMiddleware` との組み合わせ(`max_request_bytes` を明示)
147
+ 2. **docs**: `max_request_bytes` パラメーターに `max_request_size` との混同を防ぐ注記