nene2-python 1.8.34__tar.gz → 1.8.35__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 (385) hide show
  1. {nene2_python-1.8.34 → nene2_python-1.8.35}/CLAUDE.md +69 -1
  2. {nene2_python-1.8.34 → nene2_python-1.8.35}/PKG-INFO +1 -1
  3. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-110.md +66 -0
  4. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-111.md +53 -0
  5. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-112.md +65 -0
  6. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-113.md +79 -0
  7. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-114.md +69 -0
  8. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-115.md +91 -0
  9. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-116.md +99 -0
  10. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-117.md +96 -0
  11. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-118.md +79 -0
  12. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-119.md +67 -0
  13. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-120.md +95 -0
  14. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-121.md +81 -0
  15. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-122.md +87 -0
  16. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-123.md +74 -0
  17. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-124.md +72 -0
  18. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-125.md +82 -0
  19. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-126.md +111 -0
  20. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-127.md +104 -0
  21. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-128.md +85 -0
  22. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-129.md +104 -0
  23. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-130.md +91 -0
  24. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-131.md +88 -0
  25. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-132.md +98 -0
  26. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-133.md +87 -0
  27. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-134.md +96 -0
  28. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-135.md +87 -0
  29. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-136.md +100 -0
  30. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-137.md +99 -0
  31. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-138.md +97 -0
  32. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-139.md +87 -0
  33. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-140.md +90 -0
  34. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-141.md +139 -0
  35. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-142.md +100 -0
  36. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-143.md +113 -0
  37. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-144.md +127 -0
  38. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-145.md +103 -0
  39. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-146.md +103 -0
  40. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-147.md +104 -0
  41. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-148.md +109 -0
  42. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-149.md +123 -0
  43. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-150.md +139 -0
  44. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-151.md +118 -0
  45. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-152.md +114 -0
  46. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-153.md +122 -0
  47. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-154.md +113 -0
  48. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-155.md +133 -0
  49. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-156.md +146 -0
  50. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-157.md +150 -0
  51. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-158.md +117 -0
  52. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-159.md +130 -0
  53. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-160.md +128 -0
  54. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-161.md +155 -0
  55. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-162.md +140 -0
  56. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-163.md +160 -0
  57. nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-164.md +261 -0
  58. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/domain-events.md +31 -0
  59. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/lifespan-and-app-state.md +6 -2
  60. nene2_python-1.8.35/docs/how-to/soft-delete.md +92 -0
  61. nene2_python-1.8.35/docs/how-to/structured-logging.md +81 -0
  62. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/validation.md +24 -0
  63. nene2_python-1.8.35/docs/templates/field-trial-report.md +449 -0
  64. {nene2_python-1.8.34 → nene2_python-1.8.35}/pyproject.toml +1 -1
  65. {nene2_python-1.8.34 → nene2_python-1.8.35}/.env.example +0 -0
  66. {nene2_python-1.8.34 → nene2_python-1.8.35}/.github/workflows/ci.yml +0 -0
  67. {nene2_python-1.8.34 → nene2_python-1.8.35}/.github/workflows/docs.yml +0 -0
  68. {nene2_python-1.8.34 → nene2_python-1.8.35}/.github/workflows/publish.yml +0 -0
  69. {nene2_python-1.8.34 → nene2_python-1.8.35}/.gitignore +0 -0
  70. {nene2_python-1.8.34 → nene2_python-1.8.35}/.vitepress/config.mts +0 -0
  71. {nene2_python-1.8.34 → nene2_python-1.8.35}/.vitepress/theme/custom.css +0 -0
  72. {nene2_python-1.8.34 → nene2_python-1.8.35}/.vitepress/theme/index.ts +0 -0
  73. {nene2_python-1.8.34 → nene2_python-1.8.35}/AGENTS.md +0 -0
  74. {nene2_python-1.8.34 → nene2_python-1.8.35}/CHANGELOG.md +0 -0
  75. {nene2_python-1.8.34 → nene2_python-1.8.35}/Dockerfile +0 -0
  76. {nene2_python-1.8.34 → nene2_python-1.8.35}/LICENSE +0 -0
  77. {nene2_python-1.8.34 → nene2_python-1.8.35}/README.md +0 -0
  78. {nene2_python-1.8.34 → nene2_python-1.8.35}/alembic/README +0 -0
  79. {nene2_python-1.8.34 → nene2_python-1.8.35}/alembic/env.py +0 -0
  80. {nene2_python-1.8.34 → nene2_python-1.8.35}/alembic/script.py.mako +0 -0
  81. {nene2_python-1.8.34 → nene2_python-1.8.35}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  82. {nene2_python-1.8.34 → nene2_python-1.8.35}/alembic.ini +0 -0
  83. {nene2_python-1.8.34 → nene2_python-1.8.35}/compose.yaml +0 -0
  84. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0001-toolchain.md +0 -0
  85. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0002-clean-architecture.md +0 -0
  86. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0003-security-first.md +0 -0
  87. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0004-ai-first-design.md +0 -0
  88. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0005-logging.md +0 -0
  89. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0006-rate-limiting.md +0 -0
  90. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0009-mcp-design.md +0 -0
  91. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0010-async-use-case.md +0 -0
  92. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  93. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/de/index.md +0 -0
  94. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/de/tutorials/getting-started.md +0 -0
  95. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/explanation/architecture.md +0 -0
  96. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/explanation/design-philosophy.md +0 -0
  97. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  98. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  99. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-100.md +0 -0
  100. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-101.md +0 -0
  101. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-102.md +0 -0
  102. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-103.md +0 -0
  103. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-104.md +0 -0
  104. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-105.md +0 -0
  105. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-106.md +0 -0
  106. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-107.md +0 -0
  107. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-108.md +0 -0
  108. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-109.md +0 -0
  109. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  110. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  111. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  112. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  113. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  114. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  115. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  116. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  117. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  118. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  119. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  120. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  121. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  122. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  123. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  124. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  125. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  126. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  127. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  128. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  129. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  130. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  131. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  132. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  133. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  134. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  135. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  136. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  137. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  138. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  139. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  140. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  141. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  142. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  143. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  144. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  145. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  146. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  147. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  148. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  149. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  150. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  151. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  152. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  153. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  154. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  155. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  156. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  157. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  158. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  159. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  160. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  161. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  162. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  163. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  164. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  165. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  166. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  167. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  168. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  169. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  170. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  171. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  172. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  173. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  174. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  175. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  176. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  177. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  178. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  179. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  180. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  181. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  182. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  183. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  184. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  185. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  186. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  187. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  188. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  189. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  190. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  191. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  192. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  193. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  194. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  195. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  196. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  197. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  198. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  199. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  200. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  201. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  202. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  203. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-97.md +0 -0
  204. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-98.md +0 -0
  205. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-99.md +0 -0
  206. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/fr/index.md +0 -0
  207. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/fr/tutorials/getting-started.md +0 -0
  208. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/add-new-domain.md +0 -0
  209. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/api-versioning.md +0 -0
  210. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/async-use-case.md +0 -0
  211. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/background-tasks.md +0 -0
  212. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/configure-auth.md +0 -0
  213. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/cors.md +0 -0
  214. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/custom-auth-middleware.md +0 -0
  215. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/dependency-injection.md +0 -0
  216. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/file-upload.md +0 -0
  217. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/middleware-stack.md +0 -0
  218. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/new-project.md +0 -0
  219. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/problem-details.md +0 -0
  220. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/response-patterns.md +0 -0
  221. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/run-tests.md +0 -0
  222. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/sqlalchemy-repository.md +0 -0
  223. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/streaming.md +0 -0
  224. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/how-to/webhook.md +0 -0
  225. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/howto/mcp-setup.md +0 -0
  226. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/index.md +0 -0
  227. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/explanation/architecture.md +0 -0
  228. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/explanation/design-philosophy.md +0 -0
  229. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/how-to/add-new-domain.md +0 -0
  230. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/how-to/configure-auth.md +0 -0
  231. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/how-to/new-project.md +0 -0
  232. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/how-to/run-tests.md +0 -0
  233. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  234. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/howto/mcp-setup.md +0 -0
  235. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/index.md +0 -0
  236. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/reference/api.md +0 -0
  237. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/reference/configuration.md +0 -0
  238. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/reference/framework-modules.md +0 -0
  239. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/tutorials/first-domain.md +0 -0
  240. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/ja/tutorials/getting-started.md +0 -0
  241. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/pt-br/index.md +0 -0
  242. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/pt-br/tutorials/getting-started.md +0 -0
  243. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/reference/api.md +0 -0
  244. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/reference/configuration.md +0 -0
  245. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/reference/framework-modules.md +0 -0
  246. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/roadmap.md +0 -0
  247. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/todo/current.md +0 -0
  248. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/tutorials/first-domain.md +0 -0
  249. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/tutorials/getting-started.md +0 -0
  250. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/zh/index.md +0 -0
  251. {nene2_python-1.8.34 → nene2_python-1.8.35}/docs/zh/tutorials/getting-started.md +0 -0
  252. {nene2_python-1.8.34 → nene2_python-1.8.35}/package-lock.json +0 -0
  253. {nene2_python-1.8.34 → nene2_python-1.8.35}/package.json +0 -0
  254. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/__init__.py +0 -0
  255. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/__main__.py +0 -0
  256. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/app.py +0 -0
  257. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/__init__.py +0 -0
  258. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/entity.py +0 -0
  259. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/exceptions.py +0 -0
  260. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/handler.py +0 -0
  261. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/repository.py +0 -0
  262. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/sqlalchemy_repository.py +0 -0
  263. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/comment/use_case.py +0 -0
  264. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/mcp.py +0 -0
  265. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/__init__.py +0 -0
  266. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/async_use_case.py +0 -0
  267. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/entity.py +0 -0
  268. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/exceptions.py +0 -0
  269. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/handler.py +0 -0
  270. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/repository.py +0 -0
  271. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/sqlalchemy_repository.py +0 -0
  272. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/note/use_case.py +0 -0
  273. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/schema.py +0 -0
  274. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/__init__.py +0 -0
  275. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/entity.py +0 -0
  276. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/exceptions.py +0 -0
  277. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/handler.py +0 -0
  278. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/repository.py +0 -0
  279. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/sqlalchemy_repository.py +0 -0
  280. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/example/tag/use_case.py +0 -0
  281. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/__init__.py +0 -0
  282. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/__init__.py +0 -0
  283. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/api_key.py +0 -0
  284. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/bearer_token.py +0 -0
  285. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/deps.py +0 -0
  286. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/exceptions.py +0 -0
  287. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/interfaces.py +0 -0
  288. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/auth/local_verifier.py +0 -0
  289. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/cache/__init__.py +0 -0
  290. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/cache/ttl.py +0 -0
  291. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/config/__init__.py +0 -0
  292. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/config/settings.py +0 -0
  293. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/__init__.py +0 -0
  294. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/exceptions.py +0 -0
  295. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/health.py +0 -0
  296. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/interfaces.py +0 -0
  297. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/sqlalchemy_executor.py +0 -0
  298. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/database/utils.py +0 -0
  299. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/http/__init__.py +0 -0
  300. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/http/etag.py +0 -0
  301. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/http/health.py +0 -0
  302. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/http/pagination.py +0 -0
  303. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/http/problem_details.py +0 -0
  304. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/log/__init__.py +0 -0
  305. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/log/setup.py +0 -0
  306. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/mcp/__init__.py +0 -0
  307. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/mcp/http_client.py +0 -0
  308. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/mcp/server.py +0 -0
  309. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/__init__.py +0 -0
  310. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/domain_exception.py +0 -0
  311. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/error_handler.py +0 -0
  312. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/request_id.py +0 -0
  313. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/request_logging.py +0 -0
  314. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/request_size_limit.py +0 -0
  315. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/security_headers.py +0 -0
  316. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/setup.py +0 -0
  317. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/middleware/throttle.py +0 -0
  318. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/py.typed +0 -0
  319. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/security/__init__.py +0 -0
  320. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/security/webhook.py +0 -0
  321. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/use_case/__init__.py +0 -0
  322. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/use_case/protocols.py +0 -0
  323. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/validation/__init__.py +0 -0
  324. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/nene2/validation/exceptions.py +0 -0
  325. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/scripts/__init__.py +0 -0
  326. {nene2_python-1.8.34 → nene2_python-1.8.35}/src/scripts/export_openapi.py +0 -0
  327. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/__init__.py +0 -0
  328. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/conftest.py +0 -0
  329. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/__init__.py +0 -0
  330. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/comment/__init__.py +0 -0
  331. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/comment/test_comment_http.py +0 -0
  332. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/comment/test_comment_repository.py +0 -0
  333. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/comment/test_comment_use_case.py +0 -0
  334. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/conftest.py +0 -0
  335. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/note/__init__.py +0 -0
  336. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/note/test_async_note_use_case.py +0 -0
  337. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/note/test_list_notes.py +0 -0
  338. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/note/test_note_repository.py +0 -0
  339. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/tag/__init__.py +0 -0
  340. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/tag/test_tag_repository.py +0 -0
  341. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/tag/test_tags.py +0 -0
  342. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/test_cors.py +0 -0
  343. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/example/test_mcp.py +0 -0
  344. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/__init__.py +0 -0
  345. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/auth/__init__.py +0 -0
  346. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/auth/test_api_key.py +0 -0
  347. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/auth/test_bearer_token.py +0 -0
  348. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/auth/test_make_require_auth.py +0 -0
  349. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/auth/test_token_issuer.py +0 -0
  350. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/cache/__init__.py +0 -0
  351. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/cache/test_ttl.py +0 -0
  352. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/config/__init__.py +0 -0
  353. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/config/test_settings.py +0 -0
  354. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/database/__init__.py +0 -0
  355. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/database/test_transaction.py +0 -0
  356. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/database/test_utils.py +0 -0
  357. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/http/__init__.py +0 -0
  358. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/http/test_etag.py +0 -0
  359. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/http/test_health.py +0 -0
  360. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/http/test_pagination.py +0 -0
  361. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/http/test_problem_details.py +0 -0
  362. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/log/__init__.py +0 -0
  363. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/log/test_setup.py +0 -0
  364. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/mcp/__init__.py +0 -0
  365. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/mcp/test_http_client.py +0 -0
  366. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/mcp/test_server.py +0 -0
  367. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/__init__.py +0 -0
  368. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_error_handler.py +0 -0
  369. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_id.py +0 -0
  370. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_logging.py +0 -0
  371. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  372. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_security_headers.py +0 -0
  373. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  374. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  375. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/middleware/test_throttle.py +0 -0
  376. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/security/__init__.py +0 -0
  377. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/security/test_webhook.py +0 -0
  378. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/use_case/__init__.py +0 -0
  379. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/use_case/test_protocols.py +0 -0
  380. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  381. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/validation/__init__.py +0 -0
  382. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/nene2/validation/test_exceptions.py +0 -0
  383. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/scripts/__init__.py +0 -0
  384. {nene2_python-1.8.34 → nene2_python-1.8.35}/tests/scripts/test_export_openapi.py +0 -0
  385. {nene2_python-1.8.34 → nene2_python-1.8.35}/uv.lock +0 -0
@@ -355,7 +355,75 @@ docs/
355
355
 
356
356
  ---
357
357
 
358
- ## 12. PHP版 NENE2 との対応表
358
+ ## 12. フィールドトライアル(FT)方法論
359
+
360
+ ### 目的
361
+
362
+ Python 標準ライブラリ・サードパーティライブラリを nene2-python 上で実装し、
363
+ フレームワーク API の安定性を実装者目線で検証する。
364
+ 「実際に詰まったポイント」だけを観察ベースで Issue 化し、
365
+ ドキュメントと設計を同時に成長させるサイクルを回す。
366
+
367
+ ### フロー(1 FT あたり)
368
+
369
+ ```
370
+ 1. テーマ選定(docs/todo/current.md から未検証パターンを選ぶ)
371
+ 2. 独立サンドボックスを作成
372
+ 場所: /home/xi/docker/nene2-python-FT/ftNNN-テーマ名/
373
+ ゼロから uv init → nene2-python を依存として追加
374
+ 3. 実装 + 全チェック通過
375
+ uv run pytest && uv run mypy src/ && uv run ruff check src/ tests/
376
+ 4. 摩擦点を記録(F-1, F-2, ...)
377
+ 5. FT レポート作成 → docs/field-trials/2026-05-field-trial-NNN.md
378
+ テンプレート: docs/templates/field-trial-report.md
379
+ 6. DX Review(6ペルソナ)を実施(後述)
380
+ 7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
381
+ 8. Follow-up Issues を GitHub Issue に変換
382
+ 9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
383
+ ```
384
+
385
+ ### DX Review — 6ペルソナ
386
+
387
+ 各 FT レポートの末尾に、以下 6 ペルソナ目線での評価を必ず記載する。
388
+ 「コードが動く」だけでなく「初心者から経験者まで安全に使えるか」を客観的に検証する。
389
+
390
+ | ペルソナ | 属性 | 主な評価観点 |
391
+ |---|---|---|
392
+ | **1. 初心者** | Python 歴1年・独学中・女性・バックエンド志望 | ドキュメント理解・事故リスク・規約の使いやすさ |
393
+ | **2. ロースキル経験者** | Python 歴3-4年・スクリプト系・男性・SES | コピペ可能性・拡張時の罠・セキュリティ的な事故リスク |
394
+ | **3. フロントエンド寄り** | React/TS 歴4年・バックエンド転向中・ノンバイナリ | エラーレスポンスの質・Python 固有概念の学習コスト |
395
+ | **4. バックエンド経験者** | Django/FastAPI 歴5-6年・男性・リードエンジニア | 他フレームワークとの差異・nene2 の薄さへの評価 |
396
+ | **5. シニアエンジニア** | 設計・コードレビュー担当・女性・10-12年 | コードレビューチェックポイント・チームでの安全なパターン |
397
+ | **6. 設計者** | nene2-python 設計ポリシー目線 | CLAUDE.md ポリシー整合性・初心者でも安全な API 達成度 |
398
+
399
+ 各ペルソナの記述フォーマット:
400
+ - 状況説明 1 文(ペルソナが置かれているコンテキスト)
401
+ - 太字サブヘッディング 2〜4個(ドキュメント理解 / 事故リスク / 規約の使いやすさ など)
402
+ - 事故リスクは「高 / 中 / 低」で定性評価
403
+
404
+ ### セキュリティ診断(3の倍数 FT)
405
+
406
+ **FT番号 % 3 == 0 のとき**(FT165, FT168, ...)、通常のFT完了後に追加で実施する。
407
+
408
+ **診断レベル**: Django・FastAPI・SQLAlchemy 本体でも CVE が報告されてきた攻撃ベクターを対象とする。
409
+
410
+ 対象カテゴリ(詳細は `docs/templates/field-trial-report.md` のセキュリティ診断セクション参照):
411
+
412
+ 1. **OWASP API Security Top 10 (2023)** — BOLA/IDOR・認証破損・Mass Assignment・リソース消費・SSRF・設定ミス
413
+ 2. **インジェクション攻撃** — SQL・コマンド・パストラバーサル・SSTI・HTTP ヘッダーインジェクション
414
+ 3. **認証・認可** — パスワードハッシュ・タイミング攻撃・JWT alg:none・セッション固定
415
+ 4. **入力バリデーション** — 上限なし文字列・数値オーバーフロー・Null バイト・Unicode RTL
416
+ 5. **情報漏洩** — スタックトレース公開・ログへの機密データ出力・pip-audit CVE スキャン
417
+ 6. **Python/FastAPI 固有** — ReDoS・pickle/yaml インジェクション・非同期レースコンディション・Pydantic 型強制・SQLAlchemy raw query バイパス
418
+
419
+ **合否判定**:
420
+ - **合格**: 全カテゴリ問題なし
421
+ - **条件付き合格**: MEDIUM 以下の指摘のみ、次 FT までに修正
422
+ - **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正
423
+
424
+ ---
425
+
426
+ ## 13. PHP版 NENE2 との対応表
359
427
 
360
428
  | PHP | Python |
361
429
  |---|---|
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.34
3
+ Version: 1.8.35
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,66 @@
1
+ # Field Trial 110: ソフトデリートパターン(論理削除)
2
+
3
+ ## テーマ
4
+
5
+ `deleted_at: datetime | None` フィールドを使った論理削除パターンを検証する。
6
+ - `DELETE /resources/{id}` が物理削除ではなく `deleted_at` をセットする
7
+ - `GET /resources` / `GET /resources/{id}` が削除済みアイテムを除外する
8
+ - DELETE は冪等(既に削除済みでも 204)
9
+ - `deleted_at` を公開レスポンスに含めない(管理エンドポイントのみ)
10
+
11
+ ## 実施内容
12
+
13
+ `/home/xi/docker/nene2-python-FT/ft110-soft-delete/` に以下を実装:
14
+
15
+ - `Article` dataclass — `deleted_at: datetime | None = None`、`is_deleted` プロパティ
16
+ - `dataclasses.replace()` で frozen dataclass を更新(`deleted_at` をセット)
17
+ - `ArticleResponse` に `deleted_at` フィールドを含めない設計
18
+ - 管理用 `/articles/{id}/deleted` エンドポイントで `deleted_at` を確認
19
+ - 9 テスト通過
20
+
21
+ ## テスト結果
22
+
23
+ 全 9 テスト一発通過。摩擦ゼロ。
24
+
25
+ ## Friction Points
26
+
27
+ なし。
28
+
29
+ ## 観察
30
+
31
+ ### O1: frozen dataclass の更新は dataclasses.replace() で簡潔に書ける
32
+
33
+ ```python
34
+ from dataclasses import replace
35
+
36
+ _articles[article_id] = replace(article, deleted_at=datetime.now(UTC))
37
+ ```
38
+
39
+ `frozen=True` な dataclass でも `replace()` で新しいインスタンスを作成できる。
40
+ イミュータブル設計を壊さずに更新できる。
41
+
42
+ ### O2: is_deleted プロパティでビジネスロジックをドメインに閉じ込める
43
+
44
+ ```python
45
+ @property
46
+ def is_deleted(self) -> bool:
47
+ return self.deleted_at is not None
48
+ ```
49
+
50
+ クエリフィルタ(`a.is_deleted`)でリポジトリ層が `deleted_at is not None` を意識しなくてよい。
51
+
52
+ ### O3: DELETE は 204 + 冪等が REST ベストプラクティス
53
+
54
+ 既に削除済みのリソースへの DELETE も 204 を返すことで冪等性を保つ。
55
+ 存在しないリソースへの DELETE も同様。
56
+
57
+ ### O4: deleted_at をレスポンスモデルから除外するのは Pydantic の通常設計で簡単
58
+
59
+ `ArticleResponse` に `deleted_at` フィールドを定義しなければ、自動的に除外される。
60
+ `exclude=` や `model_config` の設定は不要。
61
+
62
+ ## まとめ
63
+
64
+ FT110 は摩擦ゼロ。ソフトデリートは nene2 + Python dataclass で自然に実装できる。
65
+ `dataclasses.replace()` / `is_deleted` プロパティ / Pydantic レスポンスフィルタリングの
66
+ 各パターンを how-to に記録する。
@@ -0,0 +1,53 @@
1
+ # Field Trial 111: カーソルベースページネーション
2
+
3
+ ## テーマ
4
+
5
+ ID ベースのカーソル(Base64 エンコード)で大規模データを安定的にページングするパターンを検証する。
6
+ 既存の `PaginationQueryParser`(オフセット型)との比較として実装する。
7
+
8
+ ## 実施内容
9
+
10
+ `/home/xi/docker/nene2-python-FT/ft111-cursor-pagination/` に以下を実装:
11
+
12
+ - `after` クエリパラメータ + `limit` でカーソルページネーション
13
+ - `next_cursor` / `has_next` を含む `CursorPage` レスポンスモデル
14
+ - Base64 URL-safe エンコードでカーソルを不透明にする
15
+ - 100件のサンプルデータで重複なし検証
16
+ - 7 テスト通過(修正後)
17
+
18
+ ## テスト結果
19
+
20
+ 全 7 テスト通過(1件修正後)。
21
+
22
+ ## Friction Points
23
+
24
+ ### FP1: 無効なカーソルで `binascii.Error` が捕捉されず 500 になる
25
+
26
+ **状況**: `?after=invalidcursor!` のような無効な Base64 文字列を渡すと、
27
+ `base64.urlsafe_b64decode()` が `binascii.Error` を raise する。
28
+ これを捕捉しないと `ErrorHandlerMiddleware` が 500 として返す。
29
+
30
+ ```python
31
+ # ❌ binascii.Error が uncaught → 500
32
+ after_id = _decode_cursor(after)
33
+
34
+ # ✅ try-except でカーソルデコードを保護する
35
+ try:
36
+ after_id = _decode_cursor(after)
37
+ except Exception:
38
+ return JSONResponse({"detail": "Invalid cursor"}, status_code=400)
39
+ ```
40
+
41
+ **影響**: 中。入力バリデーションとして当然捕捉すべきだが、
42
+ Pydantic の `Query()` では Base64 形式の検証はできない(文字列型のみ)。
43
+ カーソルデコードは必ず try-except で保護する必要がある。
44
+
45
+ **代替案**: `ValidationException` を raise して 422 にするパターンもあるが、
46
+ 不正なカーソルは「バリデーションエラー」ではなく「不正リクエスト」なので 400 が適切。
47
+
48
+ ## まとめ
49
+
50
+ FP1 を how-to に追記。カーソルのデコードは必ず try-except で保護し 400 を返す。
51
+ オフセットページネーションとの使い分け:
52
+ - オフセット型: 件数が少ない、管理画面など「ページ番号」で飛びたい場合
53
+ - カーソル型: 件数が多い、リアルタイムデータ、無限スクロールなど
@@ -0,0 +1,65 @@
1
+ # Field Trial 112: バルクアップデートパターン(PATCH /items/bulk)
2
+
3
+ ## テーマ
4
+
5
+ FT107(バルク作成・削除)の応用として、PATCH による一括更新操作を検証する。
6
+ - 部分更新(name のみ、price のみ、両方)
7
+ - 207 Multi-Status で部分成功/失敗
8
+ - `dataclasses.replace()` でイミュータブルエンティティを更新
9
+
10
+ ## 実施内容
11
+
12
+ `/home/xi/docker/nene2-python-FT/ft112-bulk-update/` に以下を実装:
13
+
14
+ - `PATCH /items/bulk` — 一括更新(部分成功対応)
15
+ - `ItemUpdateRequest` — `name: str | None`, `price: int | None`(両方省略可能、一方だけ指定可能)
16
+ - `@field_validator` でカスタムバリデーション(空白名チェック)
17
+ - `dataclasses.replace()` でフィールドを選択的に更新
18
+ - 8 テスト通過
19
+
20
+ ## テスト結果
21
+
22
+ 全 8 テスト一発通過。摩擦ゼロ。
23
+
24
+ ## Friction Points
25
+
26
+ なし。
27
+
28
+ ## 観察
29
+
30
+ ### O1: PATCH の部分更新は `name: str | None = None` + `dataclasses.replace()` で実現
31
+
32
+ ```python
33
+ class ItemUpdateRequest(BaseModel):
34
+ item_id: int
35
+ name: str | None = None # 省略可
36
+ price: int | None = None # 省略可
37
+
38
+ new_name = update.name if update.name is not None else item.name
39
+ new_price = update.price if update.price is not None else item.price
40
+ updated = replace(item, name=new_name, price=new_price)
41
+ ```
42
+
43
+ `None` をデフォルトにすることで「指定されたフィールドのみ更新」を表現できる。
44
+
45
+ ### O2: `@field_validator` でフィールドレベルのカスタムバリデーションが簡単
46
+
47
+ ```python
48
+ @field_validator("name")
49
+ @classmethod
50
+ def name_must_not_be_empty(cls, v: str | None) -> str | None:
51
+ if v is not None and v.strip() == "":
52
+ raise ValueError("name must not be empty")
53
+ return v
54
+ ```
55
+
56
+ `None` の場合はスキップし、空白文字のみの文字列を拒否する設計が自然に書ける。
57
+
58
+ ### O3: バルク更新とバルク作成・削除のパターンは統一できる
59
+
60
+ FT107(バルク作成・削除)と同じ 207 Multi-Status パターンで一括更新を実装できた。
61
+ `succeeded` / `failed` / `total` の構造を一貫させることでクライアント側の処理が統一できる。
62
+
63
+ ## まとめ
64
+
65
+ FT112 は摩擦ゼロ確認。PATCH バルク更新は既存の 207 パターン + `dataclasses.replace()` で実現できる。
@@ -0,0 +1,79 @@
1
+ # Field Trial 113: Pydantic 識別共用体(Discriminated Union)
2
+
3
+ ## テーマ
4
+
5
+ `Literal` 型 + `Field(discriminator="type")` による Pydantic v2 の識別共用体パターンを検証する。
6
+ 異なる形状を持つペイロードを 1 つの Union 型で受け取り、OpenAPI スキーマに oneOf として反映させる。
7
+
8
+ ## 実施内容
9
+
10
+ `/home/xi/docker/nene2-python-FT/ft113-discriminated-union/` に以下を実装:
11
+
12
+ - `TextEvent`, `ImageEvent`, `VideoEvent` — それぞれ `type: Literal["text" | "image" | "video"]` を持つ Pydantic モデル
13
+ - `Event = Annotated[Union[TextEvent, ImageEvent, VideoEvent], Field(discriminator="type")]`
14
+ - `POST /events` — 識別共用体でリクエストを受け取り、型を自動判別
15
+ - `GET /events` — クエリパラメータ `event_type` でフィルタリング
16
+ - 10 テスト通過
17
+
18
+ ## テスト結果
19
+
20
+ 全 10 テスト一発通過。摩擦ゼロ。
21
+
22
+ ## Friction Points
23
+
24
+ なし。
25
+
26
+ ## 観察
27
+
28
+ ### O1: `Field(discriminator="type")` で識別共用体を定義できる
29
+
30
+ ```python
31
+ from typing import Annotated, Literal, Union
32
+ from pydantic import BaseModel, Field
33
+
34
+ class TextEvent(BaseModel):
35
+ type: Literal["text"]
36
+ content: str
37
+
38
+ class ImageEvent(BaseModel):
39
+ type: Literal["image"]
40
+ url: str
41
+
42
+ Event = Annotated[
43
+ Union[TextEvent, ImageEvent],
44
+ Field(discriminator="type"),
45
+ ]
46
+
47
+ class EventRequest(BaseModel):
48
+ event: Event
49
+ ```
50
+
51
+ `{"type": "text", "content": "Hello"}` → `TextEvent` に自動マッピングされる。
52
+ 未知の `type` 値(`"audio"` など)は 422 になる。
53
+
54
+ ### O2: OpenAPI スキーマに `oneOf` + `discriminator` が反映される
55
+
56
+ FastAPI の `/docs` で確認すると、リクエストボディのスキーマが以下のように生成される:
57
+
58
+ ```json
59
+ {
60
+ "oneOf": [
61
+ { "$ref": "#/components/schemas/TextEvent" },
62
+ { "$ref": "#/components/schemas/ImageEvent" },
63
+ { "$ref": "#/components/schemas/VideoEvent" }
64
+ ],
65
+ "discriminator": { "propertyName": "type" }
66
+ }
67
+ ```
68
+
69
+ 各サブモデルのスキーマも個別に定義され、OpenAPI クライアントが型を正確に認識できる。
70
+
71
+ ### O3: 必須フィールドの欠如・型ミスマッチは 422 になる
72
+
73
+ 識別子 `type` が合致してもサブモデルのバリデーションは完全に行われる。
74
+ `{"type": "text"}` (`content` 欠如)や `{"type": "video", "duration_seconds": -1}` は 422 になる。
75
+
76
+ ## まとめ
77
+
78
+ FT113 は摩擦ゼロ確認。Pydantic v2 の識別共用体は `Field(discriminator="type")` + `Literal` で
79
+ 簡潔に実装でき、OpenAPI スキーマにも正確に反映される。
@@ -0,0 +1,69 @@
1
+ # Field Trial 114: プラグインレジストリパターン
2
+
3
+ ## テーマ
4
+
5
+ `@runtime_checkable Protocol` + 辞書ベースのレジストリで、型安全なプラグイン登録・ディスパッチを実現するパターンを検証する。
6
+ 通知ハンドラー(email / slack / webhook)を例に、実行時の `isinstance` チェックと OpenAPI ルーティングを組み合わせる。
7
+
8
+ ## 実施内容
9
+
10
+ `/home/xi/docker/nene2-python-FT/ft114-plugin-registry/` に以下を実装:
11
+
12
+ - `NotificationHandlerProtocol` — `@runtime_checkable Protocol`(`name: str` + `handle()` メソッド)
13
+ - `_registry: dict[str, NotificationHandlerProtocol]` — モジュールレベルのレジストリ
14
+ - `register_handler()` / `get_handler()` / `list_handlers()` — レジストリ操作関数
15
+ - `EmailHandler`, `SlackHandler`, `WebhookHandler` — 組み込みハンドラー(Protocol を満たすが継承しない)
16
+ - `POST /notify` — ハンドラー名でディスパッチ
17
+ - `GET /handlers` — 登録済みハンドラー一覧
18
+ - 9 テスト通過
19
+
20
+ ## テスト結果
21
+
22
+ 全 9 テスト一発通過。摩擦ゼロ。
23
+
24
+ ## Friction Points
25
+
26
+ なし。
27
+
28
+ ## 観察
29
+
30
+ ### O1: `@runtime_checkable Protocol` で isinstance チェックが使える
31
+
32
+ ```python
33
+ from typing import Protocol, runtime_checkable
34
+
35
+ @runtime_checkable
36
+ class NotificationHandlerProtocol(Protocol):
37
+ name: str
38
+ def handle(self, payload: dict[str, object]) -> dict[str, object]: ...
39
+
40
+ assert isinstance(EmailHandler(), NotificationHandlerProtocol) # True
41
+ ```
42
+
43
+ 継承なしに Protocol を満たす任意のクラスを `isinstance` で検証できる。
44
+ レジストリに登録する前の型チェックに使えるが、属性の実行時チェックはメソッド定義の存在のみ確認する(引数型は確認されない)。
45
+
46
+ ### O2: モジュールレベル辞書レジストリは test fixture でリセット可能
47
+
48
+ ```python
49
+ _registry: dict[str, NotificationHandlerProtocol] = {}
50
+
51
+ @pytest.fixture(autouse=True)
52
+ def _reset_registry() -> None:
53
+ _registry.clear()
54
+ register_handler(EmailHandler())
55
+ ...
56
+ ```
57
+
58
+ モジュールグローバルの辞書を直接 `clear()` → 再登録することでテスト間の独立性を確保できる。
59
+ `importlib.reload()` は不要。
60
+
61
+ ### O3: 構造的サブタイピングでサードパーティハンドラーが差し込める
62
+
63
+ Protocol を使うため、外部パッケージのクラスでも `name` と `handle()` があれば登録できる。
64
+ ABC 継承を強制しないため、既存コードへの侵食がない。
65
+
66
+ ## まとめ
67
+
68
+ FT114 は摩擦ゼロ確認。`@runtime_checkable Protocol` + 辞書レジストリは、
69
+ 拡張性の高いプラグインシステムを最小コードで実現できる。
@@ -0,0 +1,91 @@
1
+ # Field Trial 115: structlog 構造化ログ + リクエストコンテキスト伝播
2
+
3
+ ## テーマ
4
+
5
+ `RequestLoggingMiddleware` + `RequestIdMiddleware` が structlog contextvars に自動バインドする `request_id` を、
6
+ エンドポイント内のログ呼び出しに透過的に伝播させるパターンを検証する。
7
+ また pytest の `caplog` フィクスチャで structlog 出力をキャプチャできるかを確認する。
8
+
9
+ ## 実施内容
10
+
11
+ `/home/xi/docker/nene2-python-FT/ft115-structured-logging/` に以下を実装:
12
+
13
+ - `RequestLoggingMiddleware` + `RequestIdMiddleware` を組み合わせたアプリ
14
+ - `structlog.get_logger(__name__)` でエンドポイント内からログを発行
15
+ - `get_request_id()` でリクエスト ID をレスポンスボディに含める
16
+ - `configure_for_testing()` を呼んで pytest caplog と統合
17
+ - 11 テスト通過(修正後)
18
+
19
+ ## テスト結果
20
+
21
+ 2 件修正後、全 11 テスト通過。
22
+
23
+ ## Friction Points
24
+
25
+ ### FP1: `X-Request-Id` は UUID v4 形式のみ転送される — 非 UUID 値は黙って置換される
26
+
27
+ **状況**: テストで `X-Request-Id: test-req-001` を送信したところ、レスポンスに別の UUID が返ってきた。
28
+ `RequestIdMiddleware` は UUID v4 形式のみ受け付け、それ以外は新規 UUID を生成する設計になっている。
29
+
30
+ ```python
31
+ # ❌ 非 UUID 形式は無効化される
32
+ client.post("/orders", headers={"X-Request-Id": "test-req-001"})
33
+ # → レスポンスの X-Request-Id は新規生成 UUID になる
34
+
35
+ # ✅ UUID v4 形式なら転送される
36
+ valid_uuid = "550e8400-e29b-41d4-a716-446655440000"
37
+ client.post("/orders", headers={"X-Request-Id": valid_uuid})
38
+ # → レスポンスの X-Request-Id は "550e8400-e29b-41d4-a716-446655440000"
39
+ ```
40
+
41
+ **影響**: 小。設計として正しい動作(ログインジェクション対策)だが、
42
+ テストで任意文字列を X-Request-Id に指定しようとすると予期せず失敗する。
43
+
44
+ **代替案**: UUID v4 形式のリクエスト ID を使うか、`uuid.uuid4()` で生成してからテスト内で指定する。
45
+
46
+ ## 観察
47
+
48
+ ### O1: `RequestLoggingMiddleware` が structlog contextvars に request_id を自動バインド
49
+
50
+ ```python
51
+ structlog.contextvars.bind_contextvars(
52
+ request_id=request_id_var.get(),
53
+ method=request.method,
54
+ path=request.url.path,
55
+ )
56
+ ```
57
+
58
+ エンドポイント内で `logger.info("order.creating", item_id=body.item_id)` を呼ぶと、
59
+ `request_id`, `method`, `path` が自動付与される。明示的な引数渡しは不要。
60
+
61
+ ### O2: `configure_for_testing()` で structlog を pytest caplog に接続できる
62
+
63
+ ```python
64
+ # conftest.py または test ファイルの先頭
65
+ from nene2.log import configure_for_testing
66
+ configure_for_testing()
67
+
68
+ def test_logs(caplog: pytest.LogCaptureFixture) -> None:
69
+ with caplog.at_level(logging.INFO):
70
+ client.post("/orders", json={"item_id": 2, "quantity": 5})
71
+ assert any("order.creating" in record.message for record in caplog.records)
72
+ ```
73
+
74
+ `configure_for_testing()` は structlog を stdlib logging ブリッジ経由で出力するように設定し直す。
75
+ これにより pytest の caplog フィクスチャが structlog の出力を記録できる。
76
+
77
+ ### O3: `WARNING` レベルのログも caplog で確認できる
78
+
79
+ ```python
80
+ with caplog.at_level(logging.WARNING):
81
+ client.get("/orders/404")
82
+ assert any("order.not_found" in record.message for record in caplog.records)
83
+ ```
84
+
85
+ `logger.warning(...)` は caplog の `WARNING` レベルフィルターで確認できる。
86
+
87
+ ## まとめ
88
+
89
+ FP1(UUID v4 バリデーション)を how-to に追記予定。
90
+ structlog + contextvars の伝播はフレームワークが完全に自動化しており、
91
+ エンドポイント側の実装は `structlog.get_logger(__name__)` の呼び出しのみで完結する。
@@ -0,0 +1,99 @@
1
+ # Field Trial 116: @contextmanager リソース管理 + FastAPI lifespan
2
+
3
+ ## テーマ
4
+
5
+ `@contextmanager` の `try/finally` でリソースのクリーンアップを保証しながら、
6
+ FastAPI の `lifespan` コンテキストマネージャと統合するパターンを検証する。
7
+ `FakeConnectionPool` を例に、接続の取得・返却・クローズが確実に行われることを確認する。
8
+
9
+ ## 実施内容
10
+
11
+ `/home/xi/docker/nene2-python-FT/ft116-context-manager/` に以下を実装:
12
+
13
+ - `FakeConnectionPool` — 接続取得・返却・クローズをシミュレートする dataclass
14
+ - `managed_connection()` — `@contextmanager` + `try/finally` で接続のリークを防ぐ
15
+ - `lifespan()` — `@asynccontextmanager` で起動時にプール初期化、終了時にクローズ
16
+ - 8 テスト通過(修正後)
17
+
18
+ ## テスト結果
19
+
20
+ 3 件修正後、全 8 テスト通過。
21
+
22
+ ## Friction Points
23
+
24
+ ### FP1: `TestClient(app)` をコンテキストマネージャとして使わないと lifespan が起動しない
25
+
26
+ **状況**: `TestClient(app)` を直接インスタンス化してフィクスチャに渡すと、
27
+ lifespan の `startup` フェーズが実行されない。
28
+ `_pool` がグローバルで `None` のままなので 503 が返り続けた。
29
+
30
+ ```python
31
+ # ❌ lifespan が実行されない
32
+ @pytest.fixture
33
+ def client() -> TestClient:
34
+ return TestClient(app) # startup イベントが起きない → _pool = None のまま
35
+
36
+ # ✅ コンテキストマネージャとして使う
37
+ @pytest.fixture
38
+ def client() -> Generator[TestClient, None, None]:
39
+ with TestClient(app) as c:
40
+ yield c # with ブロック開始で startup、終了で shutdown が実行される
41
+ ```
42
+
43
+ **影響**: 中。lifespan を使うアプリでは必ず `with TestClient(app) as c:` パターンを使う必要がある。
44
+ FastAPI の公式ドキュメントに記載されているが、pytest フィクスチャでは見落としやすい。
45
+
46
+ ## 観察
47
+
48
+ ### O1: `@contextmanager` + `try/finally` で例外時も必ずリソースが返却される
49
+
50
+ ```python
51
+ @contextmanager
52
+ def managed_connection(pool: FakeConnectionPool) -> Generator[str, None, None]:
53
+ conn = pool.acquire()
54
+ try:
55
+ yield conn
56
+ finally:
57
+ pool.release(conn) # 例外が発生しても必ず実行される
58
+ ```
59
+
60
+ `try/finally` の `finally` ブロックは `yield` が例外で終了した場合も実行される。
61
+ 接続リークを防ぐ確実な方法。
62
+
63
+ ### O2: `@asynccontextmanager` で FastAPI lifespan を実装できる
64
+
65
+ ```python
66
+ from contextlib import asynccontextmanager
67
+
68
+ @asynccontextmanager
69
+ async def lifespan(application: FastAPI) -> AsyncGenerator[None, None]:
70
+ pool = FakeConnectionPool(name="main-pool")
71
+ try:
72
+ yield
73
+ finally:
74
+ pool.close()
75
+
76
+ app = FastAPI(lifespan=lifespan)
77
+ ```
78
+
79
+ `yield` 前が startup、`yield` 後(finally)が shutdown に対応する。
80
+ `app.on_event("startup")` の非推奨化に伴い、現在はこのパターンが標準。
81
+
82
+ ### O3: ネストした `managed_connection` は独立して動作する
83
+
84
+ ```python
85
+ with managed_connection(pool) as conn1:
86
+ with managed_connection(pool) as conn2:
87
+ assert pool.active_connections == 2
88
+ assert pool.active_connections == 1
89
+ assert pool.active_connections == 0
90
+ ```
91
+
92
+ 内側の `with` ブロックが終了すると `conn2` が返却され、
93
+ 外側が終了すると `conn1` が返却される。LIFO 順で確実に動作する。
94
+
95
+ ## まとめ
96
+
97
+ FP1(TestClient をコンテキストマネージャとして使う)を how-to/lifespan-and-app-state.md に追記予定。
98
+ `@contextmanager` + lifespan の組み合わせは nene2 のデータベース接続管理パターンと一致しており、
99
+ 一般的なリソース管理の基本として重要な確認が取れた。