nene2-python 1.8.31__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.31 → nene2_python-1.8.32}/CHANGELOG.md +24 -0
  2. {nene2_python-1.8.31 → nene2_python-1.8.32}/PKG-INFO +1 -1
  3. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-97.md +87 -0
  4. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-98.md +97 -0
  5. nene2_python-1.8.32/docs/field-trials/2026-05-field-trial-99.md +57 -0
  6. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/async-use-case.md +41 -0
  7. nene2_python-1.8.32/docs/how-to/background-tasks.md +101 -0
  8. nene2_python-1.8.32/docs/how-to/cors.md +87 -0
  9. nene2_python-1.8.32/docs/how-to/domain-events.md +119 -0
  10. nene2_python-1.8.32/docs/how-to/file-upload.md +142 -0
  11. nene2_python-1.8.32/docs/how-to/lifespan-and-app-state.md +109 -0
  12. nene2_python-1.8.32/docs/how-to/response-patterns.md +110 -0
  13. nene2_python-1.8.32/docs/how-to/streaming.md +135 -0
  14. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/validation.md +35 -1
  15. {nene2_python-1.8.31 → nene2_python-1.8.32}/pyproject.toml +1 -1
  16. nene2_python-1.8.32/src/nene2/security/__init__.py +5 -0
  17. nene2_python-1.8.32/src/nene2/security/webhook.py +29 -0
  18. nene2_python-1.8.32/tests/nene2/security/test_webhook.py +62 -0
  19. nene2_python-1.8.32/tests/scripts/__init__.py +0 -0
  20. {nene2_python-1.8.31 → nene2_python-1.8.32}/uv.lock +1 -1
  21. {nene2_python-1.8.31 → nene2_python-1.8.32}/.env.example +0 -0
  22. {nene2_python-1.8.31 → nene2_python-1.8.32}/.github/workflows/ci.yml +0 -0
  23. {nene2_python-1.8.31 → nene2_python-1.8.32}/.github/workflows/docs.yml +0 -0
  24. {nene2_python-1.8.31 → nene2_python-1.8.32}/.github/workflows/publish.yml +0 -0
  25. {nene2_python-1.8.31 → nene2_python-1.8.32}/.gitignore +0 -0
  26. {nene2_python-1.8.31 → nene2_python-1.8.32}/.vitepress/config.mts +0 -0
  27. {nene2_python-1.8.31 → nene2_python-1.8.32}/.vitepress/theme/custom.css +0 -0
  28. {nene2_python-1.8.31 → nene2_python-1.8.32}/.vitepress/theme/index.ts +0 -0
  29. {nene2_python-1.8.31 → nene2_python-1.8.32}/AGENTS.md +0 -0
  30. {nene2_python-1.8.31 → nene2_python-1.8.32}/CLAUDE.md +0 -0
  31. {nene2_python-1.8.31 → nene2_python-1.8.32}/Dockerfile +0 -0
  32. {nene2_python-1.8.31 → nene2_python-1.8.32}/LICENSE +0 -0
  33. {nene2_python-1.8.31 → nene2_python-1.8.32}/README.md +0 -0
  34. {nene2_python-1.8.31 → nene2_python-1.8.32}/alembic/README +0 -0
  35. {nene2_python-1.8.31 → nene2_python-1.8.32}/alembic/env.py +0 -0
  36. {nene2_python-1.8.31 → nene2_python-1.8.32}/alembic/script.py.mako +0 -0
  37. {nene2_python-1.8.31 → nene2_python-1.8.32}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  38. {nene2_python-1.8.31 → nene2_python-1.8.32}/alembic.ini +0 -0
  39. {nene2_python-1.8.31 → nene2_python-1.8.32}/compose.yaml +0 -0
  40. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0001-toolchain.md +0 -0
  41. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0002-clean-architecture.md +0 -0
  42. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0003-security-first.md +0 -0
  43. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0004-ai-first-design.md +0 -0
  44. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0005-logging.md +0 -0
  45. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0006-rate-limiting.md +0 -0
  46. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0009-mcp-design.md +0 -0
  47. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0010-async-use-case.md +0 -0
  48. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  49. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/de/index.md +0 -0
  50. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/de/tutorials/getting-started.md +0 -0
  51. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/explanation/architecture.md +0 -0
  52. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/explanation/design-philosophy.md +0 -0
  53. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  54. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  55. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  56. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  57. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  58. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  59. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  60. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  61. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  62. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  63. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  64. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  65. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  66. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  67. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  68. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  69. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  70. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  71. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  72. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  73. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  74. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  75. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  76. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  77. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  78. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  79. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  80. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  81. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  82. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  83. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  84. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  85. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  86. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  87. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  88. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  89. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  90. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  91. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  92. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  93. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  94. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  95. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  96. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  97. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  98. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  99. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  100. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  101. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  102. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  103. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  104. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  105. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  106. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  107. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  108. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  109. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  110. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  111. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  112. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  113. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  114. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  115. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  116. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  117. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  118. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  119. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  120. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  121. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  122. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  123. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  124. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  125. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  126. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  127. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  128. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  129. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  130. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  131. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  132. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  133. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  134. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  135. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  136. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  137. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  138. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  139. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  140. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  141. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  142. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  143. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  144. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  145. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  146. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  147. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  148. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  149. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/fr/index.md +0 -0
  150. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/fr/tutorials/getting-started.md +0 -0
  151. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/add-new-domain.md +0 -0
  152. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/configure-auth.md +0 -0
  153. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/middleware-stack.md +0 -0
  154. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/new-project.md +0 -0
  155. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/problem-details.md +0 -0
  156. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/run-tests.md +0 -0
  157. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/how-to/sqlalchemy-repository.md +0 -0
  158. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/howto/mcp-setup.md +0 -0
  159. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/index.md +0 -0
  160. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/explanation/architecture.md +0 -0
  161. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/explanation/design-philosophy.md +0 -0
  162. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/how-to/add-new-domain.md +0 -0
  163. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/how-to/configure-auth.md +0 -0
  164. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/how-to/new-project.md +0 -0
  165. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/how-to/run-tests.md +0 -0
  166. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  167. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/howto/mcp-setup.md +0 -0
  168. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/index.md +0 -0
  169. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/reference/api.md +0 -0
  170. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/reference/configuration.md +0 -0
  171. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/reference/framework-modules.md +0 -0
  172. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/tutorials/first-domain.md +0 -0
  173. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/ja/tutorials/getting-started.md +0 -0
  174. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/pt-br/index.md +0 -0
  175. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/pt-br/tutorials/getting-started.md +0 -0
  176. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/reference/api.md +0 -0
  177. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/reference/configuration.md +0 -0
  178. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/reference/framework-modules.md +0 -0
  179. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/roadmap.md +0 -0
  180. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/todo/current.md +0 -0
  181. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/tutorials/first-domain.md +0 -0
  182. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/tutorials/getting-started.md +0 -0
  183. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/zh/index.md +0 -0
  184. {nene2_python-1.8.31 → nene2_python-1.8.32}/docs/zh/tutorials/getting-started.md +0 -0
  185. {nene2_python-1.8.31 → nene2_python-1.8.32}/package-lock.json +0 -0
  186. {nene2_python-1.8.31 → nene2_python-1.8.32}/package.json +0 -0
  187. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/__init__.py +0 -0
  188. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/__main__.py +0 -0
  189. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/app.py +0 -0
  190. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/__init__.py +0 -0
  191. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/entity.py +0 -0
  192. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/exceptions.py +0 -0
  193. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/handler.py +0 -0
  194. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/repository.py +0 -0
  195. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/sqlalchemy_repository.py +0 -0
  196. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/comment/use_case.py +0 -0
  197. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/mcp.py +0 -0
  198. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/__init__.py +0 -0
  199. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/async_use_case.py +0 -0
  200. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/entity.py +0 -0
  201. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/exceptions.py +0 -0
  202. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/handler.py +0 -0
  203. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/repository.py +0 -0
  204. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/sqlalchemy_repository.py +0 -0
  205. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/note/use_case.py +0 -0
  206. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/schema.py +0 -0
  207. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/__init__.py +0 -0
  208. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/entity.py +0 -0
  209. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/exceptions.py +0 -0
  210. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/handler.py +0 -0
  211. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/repository.py +0 -0
  212. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/sqlalchemy_repository.py +0 -0
  213. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/example/tag/use_case.py +0 -0
  214. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/__init__.py +0 -0
  215. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/__init__.py +0 -0
  216. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/api_key.py +0 -0
  217. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/bearer_token.py +0 -0
  218. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/deps.py +0 -0
  219. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/exceptions.py +0 -0
  220. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/interfaces.py +0 -0
  221. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/auth/local_verifier.py +0 -0
  222. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/config/__init__.py +0 -0
  223. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/config/settings.py +0 -0
  224. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/__init__.py +0 -0
  225. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/exceptions.py +0 -0
  226. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/health.py +0 -0
  227. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/interfaces.py +0 -0
  228. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/sqlalchemy_executor.py +0 -0
  229. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/database/utils.py +0 -0
  230. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/http/__init__.py +0 -0
  231. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/http/etag.py +0 -0
  232. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/http/health.py +0 -0
  233. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/http/pagination.py +0 -0
  234. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/http/problem_details.py +0 -0
  235. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/log/__init__.py +0 -0
  236. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/log/setup.py +0 -0
  237. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/mcp/__init__.py +0 -0
  238. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/mcp/http_client.py +0 -0
  239. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/mcp/server.py +0 -0
  240. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/__init__.py +0 -0
  241. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/domain_exception.py +0 -0
  242. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/error_handler.py +0 -0
  243. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/request_id.py +0 -0
  244. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/request_logging.py +0 -0
  245. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/request_size_limit.py +0 -0
  246. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/security_headers.py +0 -0
  247. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/setup.py +0 -0
  248. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/middleware/throttle.py +0 -0
  249. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/py.typed +0 -0
  250. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/use_case/__init__.py +0 -0
  251. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/use_case/protocols.py +0 -0
  252. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/validation/__init__.py +0 -0
  253. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/nene2/validation/exceptions.py +0 -0
  254. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/scripts/__init__.py +0 -0
  255. {nene2_python-1.8.31 → nene2_python-1.8.32}/src/scripts/export_openapi.py +0 -0
  256. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/__init__.py +0 -0
  257. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/__init__.py +0 -0
  258. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/comment/__init__.py +0 -0
  259. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/comment/test_comment_http.py +0 -0
  260. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/comment/test_comment_repository.py +0 -0
  261. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/comment/test_comment_use_case.py +0 -0
  262. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/conftest.py +0 -0
  263. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/note/__init__.py +0 -0
  264. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/note/test_async_note_use_case.py +0 -0
  265. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/note/test_list_notes.py +0 -0
  266. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/note/test_note_repository.py +0 -0
  267. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/tag/__init__.py +0 -0
  268. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/tag/test_tag_repository.py +0 -0
  269. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/tag/test_tags.py +0 -0
  270. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/test_cors.py +0 -0
  271. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/example/test_mcp.py +0 -0
  272. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/__init__.py +0 -0
  273. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/auth/__init__.py +0 -0
  274. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/auth/test_api_key.py +0 -0
  275. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/auth/test_bearer_token.py +0 -0
  276. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/auth/test_make_require_auth.py +0 -0
  277. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/auth/test_token_issuer.py +0 -0
  278. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/config/__init__.py +0 -0
  279. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/config/test_settings.py +0 -0
  280. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/database/__init__.py +0 -0
  281. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/database/test_transaction.py +0 -0
  282. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/database/test_utils.py +0 -0
  283. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/http/__init__.py +0 -0
  284. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/http/test_etag.py +0 -0
  285. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/http/test_health.py +0 -0
  286. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/http/test_pagination.py +0 -0
  287. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/http/test_problem_details.py +0 -0
  288. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/log/__init__.py +0 -0
  289. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/log/test_setup.py +0 -0
  290. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/mcp/__init__.py +0 -0
  291. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/mcp/test_http_client.py +0 -0
  292. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/mcp/test_server.py +0 -0
  293. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/__init__.py +0 -0
  294. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_error_handler.py +0 -0
  295. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_id.py +0 -0
  296. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_logging.py +0 -0
  297. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  298. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_security_headers.py +0 -0
  299. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  300. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  301. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/middleware/test_throttle.py +0 -0
  302. {nene2_python-1.8.31/tests/nene2/use_case → nene2_python-1.8.32/tests/nene2/security}/__init__.py +0 -0
  303. {nene2_python-1.8.31/tests/nene2/validation → nene2_python-1.8.32/tests/nene2/use_case}/__init__.py +0 -0
  304. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/use_case/test_protocols.py +0 -0
  305. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  306. {nene2_python-1.8.31/tests/scripts → nene2_python-1.8.32/tests/nene2/validation}/__init__.py +0 -0
  307. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/nene2/validation/test_exceptions.py +0 -0
  308. {nene2_python-1.8.31 → nene2_python-1.8.32}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,30 @@ 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
+
8
32
  ## [1.8.30] — 2026-05-20
9
33
 
10
34
  FT87 フィールドトライアル — カスタムレスポンスヘッダーパターン検証と problem_details_response() 改善。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.31
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
@@ -0,0 +1,87 @@
1
+ # Field Trial 97: HTTP キャッシュヘッダー (ETag / Cache-Control)
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: ETag・Cache-Control・304 Not Modified パターンを nene2 で実装
5
+ **バージョン**: v1.8.30
6
+ **結果**: 摩擦あり(コード修正なし)
7
+
8
+ ---
9
+
10
+ ## 目的
11
+
12
+ HTTP キャッシュ機構(`ETag`, `Cache-Control`, `If-None-Match`, `304 Not Modified`)を nene2 ベースの API で実装するパターンを検証する。
13
+
14
+ ---
15
+
16
+ ## 実施内容
17
+
18
+ `/home/xi/docker/nene2-python-FT/ft97-http-caching/` に以下を実装:
19
+
20
+ - `app.py` — ETag 付き GET エンドポイント、`If-None-Match` による 304 返却、Cache-Control ヘッダー
21
+ - 13 テスト(全 PASS)
22
+
23
+ ---
24
+
25
+ ## 確認できた良好な動作
26
+
27
+ ### ETag + 304 のパターン
28
+
29
+ `If-None-Match` ヘッダーと ETag を比較して 304 を返すパターンは問題なく動作する。
30
+
31
+ ```python
32
+ @app.get("/articles/{article_id}")
33
+ def get_article(article_id: int, request: Request) -> Response:
34
+ data = _article_to_dict(article)
35
+ etag = _compute_etag(data)
36
+
37
+ if request.headers.get("if-none-match") == etag:
38
+ return Response(status_code=304, headers={"ETag": etag})
39
+
40
+ return JSONResponse(data, headers={"ETag": etag, "Cache-Control": "max-age=60"})
41
+ ```
42
+
43
+ ### 304 レスポンスにも X-Request-Id が付く
44
+
45
+ `Response(status_code=304)` を返しても、外側の `RequestIdMiddleware` が `X-Request-Id` を付与する。
46
+
47
+ ---
48
+
49
+ ## 摩擦点
50
+
51
+ ### F97-1: nene2 に ETag 生成ユーティリティがない
52
+
53
+ ETag 生成ロジック(MD5 ハッシュ)を各プロジェクトで手動実装する必要がある。各エンドポイントで繰り返し実装することになり DRY でない。
54
+
55
+ ```python
56
+ def _compute_etag(data: object) -> str:
57
+ content = json.dumps(data, sort_keys=True, ensure_ascii=False)
58
+ return f'"{hashlib.md5(content.encode(), usedforsecurity=False).hexdigest()}"'
59
+ ```
60
+
61
+ nene2 が `generate_etag(data)` などのユーティリティ関数を提供すれば再実装不要になる。
62
+
63
+ ### F97-2: `hashlib.md5()` に `usedforsecurity=False` が必要
64
+
65
+ ruff のセキュリティルール `S324`(MD5 使用禁止)により、ETag 生成で `hashlib.md5()` を使うと lint エラーになる。ETag は暗号セキュリティ用途ではないが、明示的に `usedforsecurity=False` を指定する必要がある。
66
+
67
+ ```python
68
+ # ❌ ruff S324 エラー
69
+ hashlib.md5(content.encode()).hexdigest()
70
+
71
+ # ✅ 正しい
72
+ hashlib.md5(content.encode(), usedforsecurity=False).hexdigest()
73
+ ```
74
+
75
+ nene2 のユーティリティ関数でラップすればプロジェクトごとに対処不要になる。
76
+
77
+ ### F97-3: Cache-Control ヘッダーの付与はエンドポイントごとに手動
78
+
79
+ `JSONResponse(headers={"Cache-Control": "max-age=60"})` のように各エンドポイントで手動指定が必要。
80
+ ミドルウェアでのデフォルト付与(例: 全 GET に `Cache-Control: no-cache` を付与する設定)がないため、付け忘れが起きやすい。
81
+
82
+ ---
83
+
84
+ ## 結論
85
+
86
+ HTTP キャッシュヘッダーは nene2 と問題なく実装できるが、ETag 生成の共通化がない。
87
+ `generate_etag()` などのユーティリティ関数を nene2 に追加することで摩擦を解消できる。
@@ -0,0 +1,97 @@
1
+ # Field Trial 98: PATCH / Partial Update パターン
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: Pydantic v2 の `exclude_unset=True` を使った部分更新パターン
5
+ **バージョン**: v1.8.31
6
+ **結果**: 摩擦あり(コード修正なし)
7
+
8
+ ---
9
+
10
+ ## 目的
11
+
12
+ HTTP PATCH で「送信されたフィールドのみ更新する」パターンを Pydantic v2 + nene2 で実装し、`None`(明示的 null)と「未送信」の区別ができることを検証する。
13
+
14
+ ---
15
+
16
+ ## 実施内容
17
+
18
+ `/home/xi/docker/nene2-python-FT/ft98-patch-partial-update/` に以下を実装:
19
+
20
+ - `app.py` — `PUT`(完全更新)と `PATCH`(部分更新)を持つユーザー CRUD
21
+ - 12 テスト(全 PASS)
22
+
23
+ ---
24
+
25
+ ## 確認できた良好な動作
26
+
27
+ ### `exclude_unset=True` で未送信フィールドを除外
28
+
29
+ `PatchUserBody` の全フィールドを `Optional` にし、`model_dump(exclude_unset=True)` で送信されたフィールドだけを取り出す。
30
+
31
+ ```python
32
+ class PatchUserBody(BaseModel):
33
+ name: str | None = Field(default=None, max_length=100)
34
+ email: str | None = Field(default=None, max_length=200)
35
+ bio: str | None = Field(default=None, max_length=500)
36
+
37
+ @app.patch("/users/{user_id}")
38
+ def patch_user(user_id: int, body: PatchUserBody) -> JSONResponse:
39
+ updates = body.model_dump(exclude_unset=True)
40
+ updated = User(
41
+ user_id=user_id,
42
+ name=updates.get("name", current.name),
43
+ email=updates.get("email", current.email),
44
+ bio=updates.get("bio", current.bio),
45
+ )
46
+ ```
47
+
48
+ ### `{}` と `{"bio": null}` を区別できる
49
+
50
+ `exclude_unset=True` により、空ボディ `{}` と `{"bio": null}` は正しく区別される。
51
+
52
+ ```python
53
+ body_without_bio = PatchUserBody(name="Alice")
54
+ assert "bio" not in body_without_bio.model_dump(exclude_unset=True) # 未送信
55
+ # {"bio": null} を送ると exclude_unset でも "bio" が含まれる → None に更新
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 摩擦点
61
+
62
+ ### F98-1: PATCH ボディの全フィールドが Optional なため、バリデーションが緩くなる
63
+
64
+ `PUT` ボディは必須フィールドあり(空だと 422)。`PATCH` ボディは全フィールド Optional なため、空ボディ `{}` も有効に受け入れられる。
65
+
66
+ ```python
67
+ # PUT: name 省略 → 422
68
+ client.put("/users/1", json={"email": "..."}) # 422
69
+
70
+ # PATCH: 全フィールド省略 → 200(空更新)
71
+ client.patch("/users/1", json={}) # 200(何も変わらない)
72
+ ```
73
+
74
+ 意図的な設計だが、API クライアント側が誤って空 PATCH を送っても検知できない。
75
+
76
+ ### F98-2: `model_dump()` と `model_dump(exclude_unset=True)` の違いを意識する必要がある
77
+
78
+ `default=None` で定義したフィールドは、`model_dump()` では常に含まれる(`{"bio": None}`)。`exclude_unset=True` を忘れると「未送信」と「null 送信」の区別ができなくなる。
79
+
80
+ ```python
81
+ # ❌ exclude_unset=True を忘れると未送信フィールドも含まれる
82
+ updates = body.model_dump() # {"name": None, "email": None, "bio": None}
83
+
84
+ # ✅ 送信されたフィールドのみ
85
+ updates = body.model_dump(exclude_unset=True) # {}
86
+ ```
87
+
88
+ ### F98-3: `str | None` フィールドで「クリアしたい」と「変更なし」が混同しやすい
89
+
90
+ `bio: str | None = None` の PATCH ボディでは、`{"bio": null}` が「bio を null にしたい」なのか「変更なし」なのかをクライアント側の意図だけで区別する必要がある。`exclude_unset=True` により `{"bio": null}` は「null に更新」として正しく扱えるが、API ドキュメントでこの挙動を明記する必要がある。
91
+
92
+ ---
93
+
94
+ ## 結論
95
+
96
+ Pydantic v2 の `exclude_unset=True` + 全フィールド `Optional` の PATCH パターンは nene2 と問題なく組み合わせられる。
97
+ 主な摩擦はバリデーションの緩さと `model_dump` の使い分け。コード修正は不要。
@@ -0,0 +1,57 @@
1
+ # Field Trial 99: Webhook HMAC-SHA256 署名検証
2
+
3
+ ## テーマ
4
+
5
+ 外部サービス(GitHub / Stripe 方式)からの Webhook を HMAC-SHA256 署名で検証するパターンを nene2 上で実装する。
6
+
7
+ ## 実施内容
8
+
9
+ `/home/xi/docker/nene2-python-FT/ft99-webhook-hmac/` に以下を実装:
10
+
11
+ - GitHub 方式: `X-Hub-Signature-256: sha256=<hex>` ヘッダーを HMAC-SHA256 で検証
12
+ - Stripe 方式: `Stripe-Signature: t=<timestamp>,v1=<hex>` 形式の署名を検証
13
+ - `hmac.compare_digest()` による timing-safe 比較
14
+ - `await request.body()` + `await request.json()` の二重読み取りパターン
15
+
16
+ ## テスト結果
17
+
18
+ 全 7 テスト通過。
19
+
20
+ ## Friction Points
21
+
22
+ ### FP1: nene2 に Webhook 署名検証ユーティリティがない
23
+
24
+ **状況**: GitHub/Stripe 方式の HMAC 署名検証は頻出パターンだが、nene2 に `verify_webhook_signature()` のようなユーティリティが存在しない。毎回 `hmac` モジュールを直接扱う必要がある。
25
+
26
+ **影響**: `hmac.new()` / `hmac.compare_digest()` を知らない開発者が `==` 比較を使い、timing attack に脆弱な実装をしてしまうリスクがある。
27
+
28
+ **期待する API**:
29
+ ```python
30
+ from nene2.security import verify_hmac_signature
31
+
32
+ # GitHub 方式
33
+ verify_hmac_signature(body, secret, header_value, prefix="sha256=")
34
+
35
+ # Stripe 方式
36
+ verify_stripe_signature(body, secret, header_value)
37
+ ```
38
+
39
+ ### FP2: `await request.body()` → `await request.json()` の二重読み取りがドキュメント化されていない
40
+
41
+ **状況**: Webhook 署名検証では生バイト(`request.body()`)を HMAC に通してから、JSONとしてパース(`request.json()`)する二重読み取りが必要。FastAPI は内部でボディをキャッシュするため動作するが、この挙動はフレームワークに依存した暗黙の知識。
42
+
43
+ **影響**: 「一度 `body()` を読んだら `json()` は使えない」と誤解して `json.loads(body)` を書く開発者が出る。
44
+
45
+ **期待するドキュメント**: how-to に「Webhook ハンドラーでの生ボディ + JSON 二重読み取りパターン」を追加。
46
+
47
+ ### FP3: BearerTokenMiddleware は Webhook 認証に使えない
48
+
49
+ **状況**: nene2 の `BearerTokenMiddleware` は Bearer トークン認証に特化しており、HMAC 署名検証(リクエストボディを使った認証)には対応しない。Webhook エンドポイントでは `exclude_paths` で除外して自前検証するか、専用の Depends 関数を書く必要がある。
50
+
51
+ **影響**: 摩擦は低いが、「nene2 の認証機構でWebhookも守れる」という誤解が生まれやすい。
52
+
53
+ **期待するドキュメント**: how-to に「Webhook 署名検証 vs Bearer Token 認証の使い分け」を明記。
54
+
55
+ ## まとめ
56
+
57
+ Webhook HMAC 検証自体は Python 標準ライブラリで実装できるが、セキュアな実装(timing-safe 比較)のためのユーティリティがないため FP1 として Issue を起票する。FP2・FP3 はドキュメント摩擦。
@@ -119,3 +119,44 @@ inspect.iscoroutinefunction(use_case.execute) # → True/False
119
119
  ```
120
120
 
121
121
  型安全性は `mypy --strict` の静的解析で保証します。詳細は ADR-0010 を参照してください。
122
+
123
+ ---
124
+
125
+ ## 同期 DB 呼び出しのブロッキング問題
126
+
127
+ `async def` ハンドラーで同期の DB 呼び出し(SQLAlchemy sync API 等)を行うと、イベントループをブロックして他のリクエストが詰まる。
128
+
129
+ ```python
130
+ # ❌ async def 内での同期 DB 呼び出しはブロッキング
131
+ @app.get("/notes")
132
+ async def list_notes() -> JSONResponse:
133
+ notes = session.execute(select(Note)).scalars().all() # ブロック!
134
+ return JSONResponse(...)
135
+ ```
136
+
137
+ **解決策1: `run_in_threadpool` でスレッドプールで実行する**
138
+
139
+ ```python
140
+ from nene2.middleware import run_in_threadpool
141
+
142
+ @app.get("/notes")
143
+ async def list_notes() -> JSONResponse:
144
+ notes = await run_in_threadpool(session.execute, select(Note))
145
+ return JSONResponse(...)
146
+ ```
147
+
148
+ **解決策2: `def`(同期)ハンドラーを使う**
149
+
150
+ 同期 DB を使う場合は、ハンドラーを `async def` にしない。FastAPI が自動でスレッドプールで実行する。
151
+
152
+ ```python
153
+ # ✅ def ハンドラー + 同期 DB = 問題なし
154
+ @app.get("/notes")
155
+ def list_notes() -> JSONResponse:
156
+ notes = session.execute(select(Note)).scalars().all()
157
+ return JSONResponse(...)
158
+ ```
159
+
160
+ **解決策3: SQLAlchemy async API に移行する**
161
+
162
+ 長期的には SQLAlchemy の async API(`AsyncSession`)への移行を検討する。
@@ -0,0 +1,101 @@
1
+ # How-to: BackgroundTasks
2
+
3
+ FastAPI の `BackgroundTasks` を使ってレスポンス後に処理を実行するパターンを説明する。
4
+
5
+ ---
6
+
7
+ ## 1. 基本パターン
8
+
9
+ ```python
10
+ from fastapi import BackgroundTasks, FastAPI
11
+ from fastapi.responses import JSONResponse
12
+
13
+ app = FastAPI()
14
+
15
+ def send_notification(message: str) -> None:
16
+ # 時間がかかる処理(メール送信・外部 API 呼び出し等)
17
+ print(f"Sending: {message}")
18
+
19
+ @app.post("/orders", status_code=201)
20
+ def create_order(
21
+ body: CreateOrderBody,
22
+ background_tasks: BackgroundTasks,
23
+ ) -> JSONResponse:
24
+ order = process_order(body)
25
+ background_tasks.add_task(send_notification, f"Order {order.order_id} created")
26
+ return JSONResponse({"order_id": order.order_id}, status_code=201)
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 2. UseCase との分離
32
+
33
+ UseCase を HTTP 非依存に保つため、`BackgroundTasks` は UseCase に渡さない。ハンドラー層でイベントを受け取り、BackgroundTasks に追加する。
34
+
35
+ ```python
36
+ # ✅ UseCase は BackgroundTasks を知らない
37
+ class CreateOrderUseCase:
38
+ def execute(self, body: CreateOrderInput) -> CreateOrderOutput:
39
+ order = Order(...)
40
+ return CreateOrderOutput(order_id=order.order_id, notify_email=body.email)
41
+
42
+ # ハンドラー層で BackgroundTasks を使う
43
+ @app.post("/orders", status_code=201)
44
+ def create_order(
45
+ body: CreateOrderBody,
46
+ background_tasks: BackgroundTasks,
47
+ use_case: CreateOrderUseCase = Depends(get_use_case),
48
+ ) -> JSONResponse:
49
+ result = use_case.execute(CreateOrderInput(email=body.email))
50
+ background_tasks.add_task(send_notification, result.notify_email)
51
+ return JSONResponse({"order_id": result.order_id}, status_code=201)
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 3. TestClient での挙動
57
+
58
+ `TestClient` では `BackgroundTasks` がレスポンス返却 **前** に同期的に実行される。
59
+
60
+ ```python
61
+ executed: list[str] = []
62
+
63
+ def track_task(msg: str) -> None:
64
+ executed.append(msg)
65
+
66
+ # テストでは BackgroundTasks が同期実行される
67
+ r = client.post("/orders", json={"email": "alice@example.com"})
68
+ assert r.status_code == 201
69
+ assert len(executed) == 1 # すでに実行済み
70
+ ```
71
+
72
+ 本番環境では非同期実行(レスポンス後)だが、テストでは同期実行されることに注意。
73
+
74
+ ---
75
+
76
+ ## 4. 失敗しても 500 にならない
77
+
78
+ `BackgroundTasks` 内で例外が発生しても、レスポンスはすでに送信済みのため 500 にはならない。エラーはログに記録される。
79
+
80
+ ```python
81
+ def risky_task() -> None:
82
+ raise RuntimeError("Background task failed")
83
+
84
+ # レスポンスは 201 で返る(バックグラウンドエラーは隠れる)
85
+ background_tasks.add_task(risky_task)
86
+ ```
87
+
88
+ 重要な処理は BackgroundTasks に頼らず、ジョブキュー(Celery・ARQ 等)を使う。
89
+
90
+ ---
91
+
92
+ ## 5. async def との組み合わせ
93
+
94
+ `background_tasks.add_task()` には同期・非同期どちらの関数も渡せる。
95
+
96
+ ```python
97
+ async def async_notification(email: str) -> None:
98
+ await send_email_async(email)
99
+
100
+ background_tasks.add_task(async_notification, user.email)
101
+ ```
@@ -0,0 +1,87 @@
1
+ # How-to: CORS 設定
2
+
3
+ `setup_middlewares()` の `cors_allowed_origins` パラメーターで CORS を有効化する方法を説明する。
4
+
5
+ ---
6
+
7
+ ## 1. 基本: 単一オリジンを許可
8
+
9
+ ```python
10
+ from nene2.middleware import setup_middlewares
11
+
12
+ app = FastAPI()
13
+ setup_middlewares(app, cors_allowed_origins=["https://example.com"])
14
+ ```
15
+
16
+ `cors_allowed_origins` を指定しない(デフォルト `None`)と CORS ミドルウェアは追加されない。
17
+
18
+ ---
19
+
20
+ ## 2. 複数オリジンを許可
21
+
22
+ ```python
23
+ setup_middlewares(app, cors_allowed_origins=[
24
+ "https://app.example.com",
25
+ "https://admin.example.com",
26
+ ])
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 3. 開発環境: localhost を許可
32
+
33
+ ```python
34
+ import os
35
+
36
+ origins = ["https://app.example.com"]
37
+ if os.getenv("APP_ENV") == "local":
38
+ origins += ["http://localhost:3000", "http://localhost:5173"]
39
+
40
+ setup_middlewares(app, cors_allowed_origins=origins)
41
+ ```
42
+
43
+ **`allow_origins=["*"]` は禁止**。CLAUDE.md のセキュリティポリシーにより、ワイルドカードオリジンは開発環境でも使用不可。
44
+
45
+ ---
46
+
47
+ ## 4. credentials(Cookie・Authorization ヘッダー)を許可
48
+
49
+ `setup_middlewares()` は内部で `allow_credentials=True` を設定しない。credentials が必要な場合は `CORSMiddleware` を直接追加する。
50
+
51
+ ```python
52
+ from starlette.middleware.cors import CORSMiddleware
53
+
54
+ app = FastAPI()
55
+ setup_middlewares(app) # 他のミドルウェア(RequestId 等)は通常通り設定
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["https://app.example.com"],
59
+ allow_credentials=True,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+ ```
64
+
65
+ **注意**: `add_middleware` は LIFO のため、`CORSMiddleware` を後から追加すると最外側に配置される。`setup_middlewares()` の後に呼ぶことで、CORS が最も外側で処理される。
66
+
67
+ ---
68
+
69
+ ## 5. CORS とプリフライトリクエスト
70
+
71
+ `OPTIONS` リクエスト(プリフライト)は `CORSMiddleware` が自動で処理する。`@app.options(...)` を定義する必要はない。
72
+
73
+ ---
74
+
75
+ ## 6. テストでの CORS ヘッダー確認
76
+
77
+ ```python
78
+ def test_cors_header() -> None:
79
+ with TestClient(app) as client:
80
+ r = client.get("/items", headers={"Origin": "https://app.example.com"})
81
+ assert r.headers.get("access-control-allow-origin") == "https://app.example.com"
82
+
83
+ def test_cors_not_allowed_for_unknown_origin() -> None:
84
+ with TestClient(app) as client:
85
+ r = client.get("/items", headers={"Origin": "https://evil.com"})
86
+ assert "access-control-allow-origin" not in r.headers
87
+ ```
@@ -0,0 +1,119 @@
1
+ # How-to: ドメインイベントパターン
2
+
3
+ UseCase 実行後のサイドエフェクト(メール送信・ログ・通知)を、ドメインイベントと BackgroundTasks で分離するパターンを説明する。
4
+
5
+ ---
6
+
7
+ ## 1. 基本パターン: BackgroundTasks でイベントを非同期実行
8
+
9
+ FastAPI の `BackgroundTasks` でレスポンス後に処理を実行する。
10
+
11
+ ```python
12
+ from fastapi import BackgroundTasks, FastAPI
13
+ from fastapi.responses import JSONResponse
14
+
15
+ def send_welcome_email(email: str) -> None:
16
+ # メール送信処理(時間がかかる)
17
+ ...
18
+
19
+ @app.post("/users", status_code=201)
20
+ def create_user(body: CreateUserBody, background_tasks: BackgroundTasks) -> JSONResponse:
21
+ user = create_user_use_case(body.name, body.email)
22
+ background_tasks.add_task(send_welcome_email, user.email)
23
+ return JSONResponse({"user_id": user.user_id}, status_code=201)
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 2. EventBus パターン: UseCase からドメインイベントを発行
29
+
30
+ UseCase がイベントを発行し、ハンドラーが購読するパターン。UseCase は HTTP 知識(BackgroundTasks)を持たない。
31
+
32
+ ```python
33
+ from dataclasses import dataclass
34
+ from typing import Any, Callable
35
+
36
+ # イベント定義
37
+ @dataclass(frozen=True, slots=True)
38
+ class UserCreatedEvent:
39
+ user_id: int
40
+ email: str
41
+
42
+ # EventBus
43
+ type EventHandler = Callable[[Any], None]
44
+
45
+ class EventBus:
46
+ def __init__(self) -> None:
47
+ self._handlers: dict[type, list[EventHandler]] = {}
48
+
49
+ def subscribe(self, event_type: type, handler: EventHandler) -> None:
50
+ self._handlers.setdefault(event_type, []).append(handler)
51
+
52
+ def publish(self, event: object) -> None:
53
+ for handler in self._handlers.get(type(event), []):
54
+ handler(event)
55
+
56
+ event_bus = EventBus()
57
+
58
+ # UseCase: HTTP 非依存
59
+ class CreateUserUseCase:
60
+ def __init__(self, event_bus: EventBus) -> None:
61
+ self._event_bus = event_bus
62
+
63
+ def execute(self, name: str, email: str) -> User:
64
+ user = User(...)
65
+ self._event_bus.publish(UserCreatedEvent(user.user_id, user.email))
66
+ return user
67
+
68
+ # ハンドラー登録
69
+ def on_user_created(event: UserCreatedEvent) -> None:
70
+ send_welcome_email(event.email)
71
+
72
+ event_bus.subscribe(UserCreatedEvent, on_user_created)
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 3. BackgroundTasks と EventBus の組み合わせ
78
+
79
+ HTTP ハンドラーで BackgroundTasks を使い、EventBus のハンドラーをバックグラウンドで実行する。
80
+
81
+ ```python
82
+ @app.post("/users", status_code=201)
83
+ def create_user(
84
+ body: CreateUserBody,
85
+ background_tasks: BackgroundTasks,
86
+ use_case: CreateUserUseCase = Depends(get_create_user_use_case),
87
+ ) -> JSONResponse:
88
+ # UseCase はイベントを同期発行(EventBus に積む)
89
+ user = use_case.execute(body.name, body.email)
90
+ # BackgroundTasks でイベント処理をレスポンス後に実行
91
+ for handler_call in collected_events:
92
+ background_tasks.add_task(handler_call)
93
+ return JSONResponse({"user_id": user.user_id}, status_code=201)
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 4. テストでのイベント確認
99
+
100
+ テスト時は BackgroundTasks の実行を待たずに完了するため、TestClient では同期的にすぐ実行される。
101
+
102
+ ```python
103
+ # TestClient では BackgroundTasks がレスポンス返却前に同期実行される
104
+ executed = []
105
+ event_bus.subscribe(UserCreatedEvent, lambda e: executed.append(e))
106
+
107
+ with TestClient(app) as client:
108
+ r = client.post("/users", json={"name": "Alice", "email": "alice@example.com"})
109
+
110
+ assert len(executed) == 1 # BackgroundTasks は完了済み
111
+ assert executed[0].email == "alice@example.com"
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 注意点
117
+
118
+ - `EventBus` はモジュールレベルのグローバル変数になりやすい。テスト間でハンドラーが蓄積する場合は `autouse` fixture でリセットする。
119
+ - UseCase 内でイベントを発行する場合、UseCase の引数に `EventBus` を渡してコンストラクタインジェクションする(グローバル参照を避ける)。