nene2-python 1.8.64__tar.gz → 1.8.66__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 (418) hide show
  1. {nene2_python-1.8.64 → nene2_python-1.8.66}/PKG-INFO +1 -1
  2. nene2_python-1.8.66/docs/field-trials/2026-05-field-trial-193.md +296 -0
  3. nene2_python-1.8.66/docs/field-trials/2026-05-field-trial-194.md +302 -0
  4. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/INDEX.md +5 -3
  5. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/todo/current.md +9 -6
  6. {nene2_python-1.8.64 → nene2_python-1.8.66}/pyproject.toml +1 -1
  7. {nene2_python-1.8.64 → nene2_python-1.8.66}/.env.example +0 -0
  8. {nene2_python-1.8.64 → nene2_python-1.8.66}/.github/workflows/ci.yml +0 -0
  9. {nene2_python-1.8.64 → nene2_python-1.8.66}/.github/workflows/docs.yml +0 -0
  10. {nene2_python-1.8.64 → nene2_python-1.8.66}/.github/workflows/publish.yml +0 -0
  11. {nene2_python-1.8.64 → nene2_python-1.8.66}/.gitignore +0 -0
  12. {nene2_python-1.8.64 → nene2_python-1.8.66}/.vitepress/config.mts +0 -0
  13. {nene2_python-1.8.64 → nene2_python-1.8.66}/.vitepress/theme/custom.css +0 -0
  14. {nene2_python-1.8.64 → nene2_python-1.8.66}/.vitepress/theme/index.ts +0 -0
  15. {nene2_python-1.8.64 → nene2_python-1.8.66}/AGENTS.md +0 -0
  16. {nene2_python-1.8.64 → nene2_python-1.8.66}/CHANGELOG.md +0 -0
  17. {nene2_python-1.8.64 → nene2_python-1.8.66}/CLAUDE.md +0 -0
  18. {nene2_python-1.8.64 → nene2_python-1.8.66}/Dockerfile +0 -0
  19. {nene2_python-1.8.64 → nene2_python-1.8.66}/LICENSE +0 -0
  20. {nene2_python-1.8.64 → nene2_python-1.8.66}/README.md +0 -0
  21. {nene2_python-1.8.64 → nene2_python-1.8.66}/alembic/README +0 -0
  22. {nene2_python-1.8.64 → nene2_python-1.8.66}/alembic/env.py +0 -0
  23. {nene2_python-1.8.64 → nene2_python-1.8.66}/alembic/script.py.mako +0 -0
  24. {nene2_python-1.8.64 → nene2_python-1.8.66}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  25. {nene2_python-1.8.64 → nene2_python-1.8.66}/alembic.ini +0 -0
  26. {nene2_python-1.8.64 → nene2_python-1.8.66}/compose.yaml +0 -0
  27. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0001-toolchain.md +0 -0
  28. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0002-clean-architecture.md +0 -0
  29. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0003-security-first.md +0 -0
  30. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0004-ai-first-design.md +0 -0
  31. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0005-logging.md +0 -0
  32. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0006-rate-limiting.md +0 -0
  33. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0009-mcp-design.md +0 -0
  34. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0010-async-use-case.md +0 -0
  35. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  36. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/de/index.md +0 -0
  37. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/de/tutorials/getting-started.md +0 -0
  38. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/explanation/architecture.md +0 -0
  39. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/explanation/design-philosophy.md +0 -0
  40. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  41. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  42. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-100.md +0 -0
  43. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-101.md +0 -0
  44. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-102.md +0 -0
  45. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-103.md +0 -0
  46. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-104.md +0 -0
  47. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-105.md +0 -0
  48. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-106.md +0 -0
  49. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-107.md +0 -0
  50. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-108.md +0 -0
  51. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-109.md +0 -0
  52. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  53. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-110.md +0 -0
  54. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-111.md +0 -0
  55. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-112.md +0 -0
  56. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-113.md +0 -0
  57. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-114.md +0 -0
  58. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-115.md +0 -0
  59. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-116.md +0 -0
  60. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-117.md +0 -0
  61. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-118.md +0 -0
  62. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-119.md +0 -0
  63. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  64. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-120.md +0 -0
  65. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-121.md +0 -0
  66. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-122.md +0 -0
  67. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-123.md +0 -0
  68. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-124.md +0 -0
  69. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-125.md +0 -0
  70. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-126.md +0 -0
  71. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-127.md +0 -0
  72. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-128.md +0 -0
  73. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-129.md +0 -0
  74. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  75. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-130.md +0 -0
  76. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-131.md +0 -0
  77. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-132.md +0 -0
  78. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-133.md +0 -0
  79. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-134.md +0 -0
  80. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-135.md +0 -0
  81. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-136.md +0 -0
  82. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-137.md +0 -0
  83. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-138.md +0 -0
  84. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-139.md +0 -0
  85. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  86. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-140.md +0 -0
  87. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-141.md +0 -0
  88. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-142.md +0 -0
  89. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-143.md +0 -0
  90. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-144.md +0 -0
  91. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-145.md +0 -0
  92. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-146.md +0 -0
  93. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-147.md +0 -0
  94. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-148.md +0 -0
  95. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-149.md +0 -0
  96. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  97. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-150.md +0 -0
  98. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-151.md +0 -0
  99. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-152.md +0 -0
  100. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-153.md +0 -0
  101. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-154.md +0 -0
  102. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-155.md +0 -0
  103. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-156.md +0 -0
  104. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-157.md +0 -0
  105. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-158.md +0 -0
  106. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-159.md +0 -0
  107. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  108. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-160.md +0 -0
  109. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-161.md +0 -0
  110. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-162.md +0 -0
  111. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-163.md +0 -0
  112. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-164.md +0 -0
  113. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-165.md +0 -0
  114. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-166.md +0 -0
  115. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-167.md +0 -0
  116. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-168.md +0 -0
  117. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-169.md +0 -0
  118. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  119. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-170.md +0 -0
  120. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-171.md +0 -0
  121. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-172.md +0 -0
  122. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-173.md +0 -0
  123. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-174.md +0 -0
  124. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-175.md +0 -0
  125. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-176.md +0 -0
  126. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-177.md +0 -0
  127. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-178.md +0 -0
  128. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-179.md +0 -0
  129. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  130. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-180.md +0 -0
  131. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-181.md +0 -0
  132. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-182.md +0 -0
  133. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-183.md +0 -0
  134. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-184.md +0 -0
  135. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-185.md +0 -0
  136. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-186.md +0 -0
  137. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-187.md +0 -0
  138. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-188.md +0 -0
  139. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-189.md +0 -0
  140. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  141. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-190.md +0 -0
  142. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-191.md +0 -0
  143. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-192.md +0 -0
  144. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  145. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  146. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  147. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  148. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  149. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  150. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  151. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  152. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  153. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  154. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  155. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  156. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  157. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  158. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  159. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  160. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  161. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  162. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  163. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  164. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  165. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  166. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  167. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  168. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  169. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  170. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  171. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  172. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  173. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  174. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  175. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  176. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  177. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  178. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  179. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  180. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  181. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  182. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  183. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  184. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  185. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  186. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  187. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  188. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  189. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  190. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  191. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  192. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  193. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  194. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  195. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  196. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  197. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  198. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  199. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  200. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  201. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  202. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  203. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  204. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  205. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  206. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  207. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  208. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  209. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  210. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  211. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  212. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  213. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  214. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  215. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  216. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  217. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  218. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  219. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  220. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  221. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  222. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  223. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  224. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  225. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  226. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  227. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  228. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  229. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-97.md +0 -0
  230. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-98.md +0 -0
  231. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/field-trials/2026-05-field-trial-99.md +0 -0
  232. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/fr/index.md +0 -0
  233. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/fr/tutorials/getting-started.md +0 -0
  234. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/add-new-domain.md +0 -0
  235. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/api-versioning.md +0 -0
  236. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/async-use-case.md +0 -0
  237. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/background-tasks.md +0 -0
  238. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/configure-auth.md +0 -0
  239. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/cors.md +0 -0
  240. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/custom-auth-middleware.md +0 -0
  241. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/decimal-unicode-input.md +0 -0
  242. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/dependency-injection.md +0 -0
  243. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/domain-events.md +0 -0
  244. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/email-address-parsing.md +0 -0
  245. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/file-upload.md +0 -0
  246. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/lifespan-and-app-state.md +0 -0
  247. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/middleware-stack.md +0 -0
  248. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/new-project.md +0 -0
  249. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/problem-details.md +0 -0
  250. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/response-patterns.md +0 -0
  251. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/run-tests.md +0 -0
  252. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/soft-delete.md +0 -0
  253. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/sqlalchemy-repository.md +0 -0
  254. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/streaming.md +0 -0
  255. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/structured-logging.md +0 -0
  256. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/validation.md +0 -0
  257. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/how-to/webhook.md +0 -0
  258. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/howto/mcp-setup.md +0 -0
  259. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/index.md +0 -0
  260. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/explanation/architecture.md +0 -0
  261. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/explanation/design-philosophy.md +0 -0
  262. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/how-to/add-new-domain.md +0 -0
  263. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/how-to/configure-auth.md +0 -0
  264. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/how-to/new-project.md +0 -0
  265. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/how-to/run-tests.md +0 -0
  266. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  267. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/howto/mcp-setup.md +0 -0
  268. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/index.md +0 -0
  269. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/reference/api.md +0 -0
  270. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/reference/configuration.md +0 -0
  271. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/reference/framework-modules.md +0 -0
  272. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/tutorials/first-domain.md +0 -0
  273. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/ja/tutorials/getting-started.md +0 -0
  274. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/pt-br/index.md +0 -0
  275. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/pt-br/tutorials/getting-started.md +0 -0
  276. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/reference/api.md +0 -0
  277. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/reference/configuration.md +0 -0
  278. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/reference/framework-modules.md +0 -0
  279. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/roadmap.md +0 -0
  280. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/templates/field-trial-report.md +0 -0
  281. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/tutorials/first-domain.md +0 -0
  282. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/tutorials/getting-started.md +0 -0
  283. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/zh/index.md +0 -0
  284. {nene2_python-1.8.64 → nene2_python-1.8.66}/docs/zh/tutorials/getting-started.md +0 -0
  285. {nene2_python-1.8.64 → nene2_python-1.8.66}/package-lock.json +0 -0
  286. {nene2_python-1.8.64 → nene2_python-1.8.66}/package.json +0 -0
  287. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/__init__.py +0 -0
  288. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/__main__.py +0 -0
  289. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/app.py +0 -0
  290. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/__init__.py +0 -0
  291. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/entity.py +0 -0
  292. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/exceptions.py +0 -0
  293. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/handler.py +0 -0
  294. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/repository.py +0 -0
  295. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/sqlalchemy_repository.py +0 -0
  296. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/comment/use_case.py +0 -0
  297. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/mcp.py +0 -0
  298. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/__init__.py +0 -0
  299. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/async_use_case.py +0 -0
  300. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/entity.py +0 -0
  301. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/exceptions.py +0 -0
  302. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/handler.py +0 -0
  303. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/repository.py +0 -0
  304. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/sqlalchemy_repository.py +0 -0
  305. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/note/use_case.py +0 -0
  306. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/schema.py +0 -0
  307. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/__init__.py +0 -0
  308. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/entity.py +0 -0
  309. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/exceptions.py +0 -0
  310. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/handler.py +0 -0
  311. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/repository.py +0 -0
  312. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/sqlalchemy_repository.py +0 -0
  313. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/example/tag/use_case.py +0 -0
  314. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/__init__.py +0 -0
  315. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/__init__.py +0 -0
  316. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/api_key.py +0 -0
  317. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/bearer_token.py +0 -0
  318. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/deps.py +0 -0
  319. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/exceptions.py +0 -0
  320. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/interfaces.py +0 -0
  321. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/auth/local_verifier.py +0 -0
  322. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/cache/__init__.py +0 -0
  323. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/cache/ttl.py +0 -0
  324. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/config/__init__.py +0 -0
  325. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/config/settings.py +0 -0
  326. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/__init__.py +0 -0
  327. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/exceptions.py +0 -0
  328. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/health.py +0 -0
  329. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/interfaces.py +0 -0
  330. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/sqlalchemy_executor.py +0 -0
  331. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/database/utils.py +0 -0
  332. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/http/__init__.py +0 -0
  333. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/http/etag.py +0 -0
  334. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/http/health.py +0 -0
  335. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/http/pagination.py +0 -0
  336. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/http/problem_details.py +0 -0
  337. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/log/__init__.py +0 -0
  338. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/log/setup.py +0 -0
  339. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/mcp/__init__.py +0 -0
  340. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/mcp/http_client.py +0 -0
  341. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/mcp/server.py +0 -0
  342. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/__init__.py +0 -0
  343. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/domain_exception.py +0 -0
  344. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/error_handler.py +0 -0
  345. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/request_id.py +0 -0
  346. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/request_logging.py +0 -0
  347. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/request_size_limit.py +0 -0
  348. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/security_headers.py +0 -0
  349. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/setup.py +0 -0
  350. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/middleware/throttle.py +0 -0
  351. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/py.typed +0 -0
  352. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/security/__init__.py +0 -0
  353. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/security/webhook.py +0 -0
  354. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/use_case/__init__.py +0 -0
  355. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/use_case/protocols.py +0 -0
  356. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/validation/__init__.py +0 -0
  357. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/nene2/validation/exceptions.py +0 -0
  358. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/scripts/__init__.py +0 -0
  359. {nene2_python-1.8.64 → nene2_python-1.8.66}/src/scripts/export_openapi.py +0 -0
  360. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/__init__.py +0 -0
  361. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/conftest.py +0 -0
  362. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/__init__.py +0 -0
  363. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/comment/__init__.py +0 -0
  364. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/comment/test_comment_http.py +0 -0
  365. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/comment/test_comment_repository.py +0 -0
  366. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/comment/test_comment_use_case.py +0 -0
  367. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/conftest.py +0 -0
  368. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/note/__init__.py +0 -0
  369. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/note/test_async_note_use_case.py +0 -0
  370. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/note/test_list_notes.py +0 -0
  371. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/note/test_note_repository.py +0 -0
  372. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/tag/__init__.py +0 -0
  373. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/tag/test_tag_repository.py +0 -0
  374. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/tag/test_tags.py +0 -0
  375. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/test_cors.py +0 -0
  376. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/example/test_mcp.py +0 -0
  377. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/__init__.py +0 -0
  378. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/auth/__init__.py +0 -0
  379. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/auth/test_api_key.py +0 -0
  380. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/auth/test_bearer_token.py +0 -0
  381. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/auth/test_make_require_auth.py +0 -0
  382. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/auth/test_token_issuer.py +0 -0
  383. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/cache/__init__.py +0 -0
  384. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/cache/test_ttl.py +0 -0
  385. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/config/__init__.py +0 -0
  386. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/config/test_settings.py +0 -0
  387. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/database/__init__.py +0 -0
  388. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/database/test_transaction.py +0 -0
  389. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/database/test_utils.py +0 -0
  390. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/http/__init__.py +0 -0
  391. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/http/test_etag.py +0 -0
  392. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/http/test_health.py +0 -0
  393. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/http/test_pagination.py +0 -0
  394. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/http/test_problem_details.py +0 -0
  395. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/log/__init__.py +0 -0
  396. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/log/test_setup.py +0 -0
  397. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/mcp/__init__.py +0 -0
  398. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/mcp/test_http_client.py +0 -0
  399. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/mcp/test_server.py +0 -0
  400. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/__init__.py +0 -0
  401. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_error_handler.py +0 -0
  402. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_request_id.py +0 -0
  403. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_request_logging.py +0 -0
  404. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  405. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_security_headers.py +0 -0
  406. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  407. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  408. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/middleware/test_throttle.py +0 -0
  409. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/security/__init__.py +0 -0
  410. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/security/test_webhook.py +0 -0
  411. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/use_case/__init__.py +0 -0
  412. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/use_case/test_protocols.py +0 -0
  413. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  414. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/validation/__init__.py +0 -0
  415. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/nene2/validation/test_exceptions.py +0 -0
  416. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/scripts/__init__.py +0 -0
  417. {nene2_python-1.8.64 → nene2_python-1.8.66}/tests/scripts/test_export_openapi.py +0 -0
  418. {nene2_python-1.8.64 → nene2_python-1.8.66}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.64
3
+ Version: 1.8.66
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,296 @@
1
+ # FT193: socket モジュール — TCP/UDP socketpair・DNS 解決・ソケットオプション
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: Python `socket` モジュールの基本操作を nene2-python FastAPI アプリとして実装し、低レベルネットワーク API の DX を検証する
5
+ **セキュリティ診断**: なし(193 % 3 = 1)
6
+ **クラッカーペンテスト**: なし(193 % 4 = 1)
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `socket` モジュールは Python の BSD ソケット低レベル API。
13
+ TCP/UDP 通信・DNS 名前解決・ソケットオプション照会などを提供する。
14
+ FT192(asyncio)と並ぶ並行・ネットワーク系の基盤モジュールであり、
15
+ 今回は `socketpair()` を使ったインプロセスエコーで外部ネットワーク依存なしにテストする設計をとった。
16
+
17
+ ---
18
+
19
+ ## 実装したサンプルアプリ
20
+
21
+ **場所**: `/home/xi/docker/nene2-python-FT/ft193-socket/`
22
+
23
+ ### 主要機能
24
+
25
+ | 関数/クラス | 概要 |
26
+ |---|---|
27
+ | `dns_lookup(host, port)` | `socket.getaddrinfo` で DNS 名前解決。`match` 文で型安全にアドレスタプルを展開 |
28
+ | `hostname_info()` | `gethostname` / `getfqdn` / `gethostbyname` でローカルホスト情報を取得 |
29
+ | `tcp_echo_pair(message)` | `socketpair(SOCK_STREAM)` を使ったインプロセス TCP エコー |
30
+ | `udp_echo_pair(message)` | `socketpair(SOCK_DGRAM)` を使ったインプロセス UDP エコー |
31
+ | `socket_options_info()` | 新規 TCP ソケットのデフォルトオプション値(`SO_REUSEADDR` / `SO_SNDBUF` 等)を返す |
32
+ | `socket_capabilities()` | IPv6 対応・デュアルスタック対応・デフォルトタイムアウトを報告 |
33
+
34
+ ### HTTP エンドポイント
35
+
36
+ | メソッド | パス | 概要 |
37
+ |---|---|---|
38
+ | POST | `/socket/dns-lookup` | DNS ルックアップ(`host` / `port` を受け取る) |
39
+ | GET | `/socket/hostname` | ローカルホスト情報 |
40
+ | POST | `/socket/tcp-echo` | TCP エコー(socketpair) |
41
+ | POST | `/socket/udp-echo` | UDP エコー(socketpair) |
42
+ | GET | `/socket/options` | ソケットオプション照会 |
43
+ | GET | `/socket/capabilities` | 実行環境のソケット機能情報 |
44
+
45
+ ---
46
+
47
+ ## テスト結果
48
+
49
+ **27 passed**
50
+
51
+ ```
52
+ 27 passed in 0.35s
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 摩擦ポイント
58
+
59
+ ### F-1: `sendall(b"")` で `recv()` がブロック(深刻度: 中)
60
+
61
+ **事象**: `tcp_echo_pair("")` を呼ぶと `client.sendall(b"")` が no-op になり、
62
+ `server.recv(256)` がデータを待ち続けてテストが永久ブロックした。
63
+
64
+ **原因**: TCP ソケットは `send(b"")` を実際には送出しない。
65
+ `recv()` はソケットが閉じられるか、データが届くまで待ち続ける。
66
+ UDP (`SOCK_DGRAM`) は 0 バイトデータグラムを送信できるため問題にならないが、
67
+ TCP とふるまいが異なる点が開発者の盲点になりやすい。
68
+
69
+ **対応**: 空メッセージを早期リターンで処理する。
70
+
71
+ ```python
72
+ def tcp_echo_pair(message: str) -> EchoResult:
73
+ message = message[:MAX_MESSAGE_LEN]
74
+ # sendall(b"") は TCP ではデータを送出しないため server.recv がブロックする
75
+ if not message:
76
+ return EchoResult(sent="", received="", matched=True, byte_count=0)
77
+ ...
78
+ ```
79
+
80
+ CLAUDE.md への追記事項なし(一般的な Python ソケット挙動)。
81
+
82
+ ### F-2: `socket.getaddrinfo` 戻り値の mypy 型エラー(深刻度: 低)
83
+
84
+ **事象**: `info[4][0]` を `AddressInfo.address: str` に渡すと mypy が
85
+ `Argument has incompatible type "str | int"` と報告した。
86
+
87
+ **原因**: `socket.getaddrinfo` の戻り値アドレスタプルは
88
+ `tuple[str, int] | tuple[str, int, int, int]` より広い union として typeshed が定義しており、
89
+ `[0]` インデックスアクセスで `str | int` になる。
90
+
91
+ **対応**: `match` 文の型パターンで絞り込む。
92
+
93
+ ```python
94
+ match info[4]:
95
+ case (str() as address, int() as addr_port, *_):
96
+ addresses.append(AddressInfo(
97
+ family=info[0].name,
98
+ type=info[1].name,
99
+ address=address,
100
+ port=addr_port,
101
+ ))
102
+ case _:
103
+ pass
104
+ ```
105
+
106
+ `str() as address` / `int() as addr_port` のパターンが mypy の型絞り込みを働かせ、
107
+ `cast()` や `# type: ignore` なしで型安全に書けた。Python 3.10+ `match` の典型的な有効活用。
108
+
109
+ ---
110
+
111
+ ## 観察点
112
+
113
+ ### 観察1: `socketpair` でネットワーク依存なしにソケット動作を検証できる
114
+
115
+ ```python
116
+ client, server = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
117
+ try:
118
+ client.sendall(message.encode())
119
+ data = server.recv(MAX_MESSAGE_LEN)
120
+ server.sendall(data)
121
+ received_bytes = client.recv(MAX_MESSAGE_LEN)
122
+ finally:
123
+ client.close()
124
+ server.close()
125
+ ```
126
+
127
+ `socketpair()` はカーネル内のパイプに近く、ネットワークスタックを経由しない。
128
+ FT 環境でポートを bind/listen せずにソケット通信をテストするのに最適。
129
+ `AF_UNIX + SOCK_STREAM` (TCP 相当) と `AF_UNIX + SOCK_DGRAM` (UDP 相当) の両方が使える。
130
+
131
+ ### 観察2: `socket.getaddrinfo` 戻り値を `match` で分岐するパターン
132
+
133
+ ```python
134
+ match info[4]:
135
+ case (str() as address, int() as addr_port, *_):
136
+ # AF_INET: (host, port)
137
+ # AF_INET6: (host, port, flowinfo, scopeid)
138
+ # どちらも先頭2要素が (str, int) なのでこのパターンで捕捉できる
139
+ ...
140
+ case _:
141
+ pass # 想定外のアドレス族(AF_ALG 等)はスキップ
142
+ ```
143
+
144
+ `*_` で残余要素を無視しているため AF_INET4/INET6 の両方を1パターンで処理できる。
145
+ これは Python 3.10 以降の `match` がある場合の慣用的な書き方。
146
+
147
+ ### 観察3: `socket.AddressFamily.name` / `socket.SocketKind.name` で読みやすい文字列を取得
148
+
149
+ ```python
150
+ # info[0] は AddressFamily IntEnum なので .name でシンボル名を取得
151
+ family=info[0].name # "AF_INET" / "AF_INET6"
152
+ type=info[1].name # "SOCK_STREAM"
153
+ ```
154
+
155
+ `IntEnum` の `.name` プロパティはシリアライズ時に読みやすく、
156
+ 直接 `int` を返すより API レスポンスとして価値が高い。
157
+
158
+ ### 観察4: `socket_capabilities()` で環境差異を明示的に記録
159
+
160
+ ```python
161
+ def socket_capabilities() -> SocketCapabilities:
162
+ has_ipv6 = socket.has_ipv6
163
+ try:
164
+ has_dual = socket.has_dualstack_ipv6()
165
+ except OSError:
166
+ has_dual = False
167
+ return SocketCapabilities(
168
+ has_ipv6=has_ipv6,
169
+ has_dualstack_ipv6=has_dual,
170
+ default_timeout=socket.getdefaulttimeout(),
171
+ hostname=socket.gethostname(),
172
+ )
173
+ ```
174
+
175
+ `has_dualstack_ipv6()` は OS レベルで IPv6 デュアルスタックが使えるかを確認する。
176
+ WSL2 環境では `False` になることがある。`OSError` でガードしてポータブルに書く。
177
+
178
+ ---
179
+
180
+ ## nene2-python フレームワークとの統合
181
+
182
+ - `socket` モジュールは nene2-python のミドルウェアや認証と直接の接点はない。
183
+ DNS 解決やホスト情報を FastAPI エンドポイントで提供する形で統合した。
184
+ - 戻り値は `@dataclass(frozen=True, slots=True)` で定義し、
185
+ FastAPI が Pydantic v2 経由で JSON シリアライズする。
186
+ ネストした dataclass (`DnsResult.addresses: list[AddressInfo]`) も問題なく動作した。
187
+ - HTTP 境界での入力制約(`host: str = Field(..., max_length=253)`, `port: int = Field(80, ge=1, le=65535)`)
188
+ は Pydantic Body で完結し、DNS ルックアップに不正入力が渡らないよう保護した。
189
+ - `getaddrinfo` が失敗する未知ホストに対しては `socket.gaierror` を捕捉して空リストを返す
190
+ 設計にした。例外を 500 にせず意味のあるレスポンスを返す nene2 の「薄い HTTP 層」の原則に合致する。
191
+
192
+ ---
193
+
194
+ ## Developer Experience (DX) Review
195
+
196
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
197
+
198
+ FastAPI の Hello World を書けるようになった段階。
199
+ `socket` が標準ライブラリにあることは知っているが、
200
+ 「なぜ TCP と UDP で動きが違うのか」の理解が浅い。
201
+
202
+ **ドキュメント理解**: `socket.getaddrinfo` の戻り値型が複雑で、Python 公式ドキュメントだけでは
203
+ タプルの構造を読み取りにくい。サンプルコードと「`info[4]` はアドレスタプルで AF_INET は `(host, port)`」
204
+ という図解があると理解が一段上がる。
205
+ **事故リスク**: 中。`sendall(b"")` で `recv()` がブロックするというF-1の罠は
206
+ 初心者には気づきにくい。「空文字の扱い」は必ずコメントかドキュメントに記載が必要。
207
+ **規約の使いやすさ**: `socketpair` → `sendall` → `recv` のシーケンスは直感的で、
208
+ 一度理解すれば机械的に書ける。`finally` でクローズを忘れない規約(または `with` 文)を教えれば問題ない。
209
+
210
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
211
+
212
+ 既存コードを見て `socketpair` を使いこなせるが、深い仕組みはブラックボックスにしがち。
213
+
214
+ **コピペ可能性**: `tcp_echo_pair` のコードをそのままコピーして使える。
215
+ ただし「空メッセージのガード」を知らずに削除するリスクがある(F-1 の再発)。
216
+ **拡張時の罠**: `socketpair` を `connect` に置き換えて外部サービスに向けるとき、
217
+ タイムアウトを設定しないまま本番投入するリスクが高い。
218
+ `sock.settimeout(seconds)` を必ず設定することをコードコメントかテンプレートで強制すべき。
219
+ **セキュリティ的な事故リスク**: 低。`socket` そのものは低レベル API のため、
220
+ 使い方を誤っても金銭的損害には直結しにくい。ただし `getaddrinfo` を SSRF の起点にするパターンには注意が必要。
221
+
222
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
223
+
224
+ REST API の呼び出し側として実装経験があり、
225
+ 「なぜこのエンドポイントが `message: str` だけを受け取るのか」が分かりやすい API を好む。
226
+
227
+ **エラーレスポンスの質**: `host` が 254 文字以上のとき 422 + `detail` の Pydantic エラーが返り、
228
+ クライアントにとって扱いやすい。`gaierror` (DNS 失敗) を 200 + 空 `addresses` で返す設計は
229
+ クライアントがエラーを区別する必要がなく良い DX 判断。
230
+ **Python 固有概念の学習コスト**: `socketpair` / `AF_UNIX` / `SOCK_STREAM` の意味は
231
+ TypeScript 経験者には馴染みが薄い。エンドポイントの OpenAPI description で目的を説明している点は助かる。
232
+ **事故リスク**: 低。このエンドポイント群は読み取り系で副作用がないため、
233
+ フロントエンド寄り開発者が誤用してシステムを壊すリスクは小さい。
234
+
235
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
236
+
237
+ `socket` モジュールは使ったことがあるが、`socketpair` は「そんな API があったのか」という反応。
238
+
239
+ **他フレームワークとの差異**: Django では `socket` をユーザーランドに公開するエンドポイントを
240
+ 作ることはほとんどない。このFTで示したパターン(ヘルスチェックや環境情報エンドポイントへの応用)は
241
+ 実用性が高く「使える設計」と評価できる。
242
+ **nene2-python の薄さへの評価**: `create_app()` ファクトリパターン・`APIRouter` の分離が
243
+ このサイズ(ファイル3つ)でも一貫して適用されており、「小さくても本番と同じ構造」を実証している。
244
+ チームで使うときのテンプレートとして説得力がある。
245
+ **本番投入可能性**: ソケット操作系の本番エンドポイントは認証保護が必須。
246
+ このFTでは `BearerTokenMiddleware` を使っていないが、実運用では認証ミドルウェアを被せること。
247
+
248
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
249
+
250
+ チームメンバーが F-1 の罠を再現しないかを重点確認する。
251
+
252
+ **コードレビューチェックポイント**:
253
+ - [x] 空メッセージの早期リターンがあるか(F-1 対策)
254
+ - [x] `socketpair` の `finally` でクローズ漏れがないか
255
+ - [x] `gaierror` を握りつぶさず意味のある値を返しているか
256
+ - [ ] 実運用では `sock.settimeout()` でハングを防いでいるか(このFTには外部接続なし)
257
+ - [x] ユーザー入力 `host` / `message` に長さ制限があるか(Pydantic Field で保護済み)
258
+
259
+ **チームでの安全な共有パターン**: 空入力ガードとタイムアウト設定をセットにした
260
+ `safe_socket_connect()` ヘルパーをプロジェクト共通ユーティリティとして提供すると事故が減る。
261
+ **ツール追加の必要性**: ruff の `S` ルールが `socket.create_connection` に `timeout=None` を
262
+ flagging しないため、コードレビューで明示的に確認が必要。
263
+
264
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
265
+
266
+ CLAUDE.md の「Security first」「薄い HTTP 層」「AI-readable」との整合を確認する。
267
+
268
+ **ポリシー達成度**: 高
269
+ **「初心者でも安全な API」達成度**: 高
270
+
271
+ - `host` の長さ制限(253 chars = DNS 最大長)と `port` の範囲制限(1〜65535)が Pydantic で宣言的に実装されており、検証を忘れるリスクがない。
272
+ - `gaierror` の捕捉と空リスト返却は、例外を「500 にする」のではなく「意味のあるレスポンスにする」nene2 の方針に沿っている。
273
+ - `match` 文による型安全な分岐は `cast()` / `# type: ignore` を使わず mypy --strict をパスしており、CLAUDE.md「型安全ポリシー」を遵守している。
274
+ - F-1(空メッセージブロック)の修正コメント「sendall(b"") は TCP ではデータを送出しないため server.recv がブロックする」は、「WHY が非自明な場合のみコメント」のポリシーに準拠した正当なコメント。
275
+
276
+ **設計上の負債・ドキュメント不足**: なし
277
+ **Follow-up Issue 候補**: なし
278
+
279
+ ---
280
+
281
+ ## Follow-up Issues
282
+
283
+ なし — 今回発見した摩擦点(F-1, F-2)はサンドボックス内で即時修正済み。
284
+
285
+ ---
286
+
287
+ ## まとめ
288
+
289
+ `socket` モジュールは低レベル API ながら、
290
+ `socketpair()` を使うことで外部ネットワーク依存なしにインプロセスでエコーテストを書ける点が発見だった。
291
+ `getaddrinfo` の戻り値型(F-2)は typeshed の制約で mypy が `str | int` を報告するが、
292
+ `match` 文のパターンマッチングで `cast()` なしに解決できた。
293
+ `sendall(b"")` が TCP ブロックの原因になる点(F-1)は典型的な「空入力の罠」として記録した。
294
+
295
+ 次のFT194 は 194 % 3 = 2 → セキュリティ診断なし、194 % 4 = 2 → クラッカーペンテストなし。
296
+ ネットワーク系の継続として `ssl` モジュール(TLS コンテキスト・自己署名証明書・HTTPS クライアント)が候補。
@@ -0,0 +1,302 @@
1
+ # FT194: ipaddress モジュール — IPv4/IPv6 解析・CIDR 計算・SSRF 防御パターン
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: Python `ipaddress` モジュールを使ったアドレス解析・ネットワーク計算・SSRF 防御パターンの実装と検証
5
+ **セキュリティ診断**: なし(194 % 3 = 2)
6
+ **クラッカーペンテスト**: なし(194 % 4 = 2)
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `ipaddress` モジュールは IPv4/IPv6 アドレス・ネットワークを型安全に扱う標準ライブラリ。
13
+ FT193(socket)で DNS 解決の話が出たが、解決後の IP が安全かどうかを確認するのに `ipaddress` は不可欠。
14
+ FT184(urllib.request SSRF 防御)で使ったパターンの理論的基盤となるモジュールを正面から検証する。
15
+
16
+ 主要ユースケース:
17
+ - DNS 解決後 IP の SSRF 安全性チェック(AWS メタデータ `169.254.169.254` 等のブロック)
18
+ - CIDR ネットワーク計算(ホスト数・アドレス範囲)
19
+ - IP バージョン・分類フラグの判定(private / loopback / link-local / multicast)
20
+ - IP 範囲列挙(監査・ログ用途)
21
+
22
+ ---
23
+
24
+ ## 実装したサンプルアプリ
25
+
26
+ **場所**: `/home/xi/docker/nene2-python-FT/ft194-ipaddress/`
27
+
28
+ ### 主要機能
29
+
30
+ | 関数/クラス | 概要 |
31
+ |---|---|
32
+ | `parse_address(ip_str)` | IPv4/IPv6 アドレスを解析。バージョン・分類フラグ全量を返す |
33
+ | `parse_network(cidr)` | CIDR 表記を解析。`strict=False` でホストビット許容 |
34
+ | `ssrf_safety_check(ip_str)` | DNS 解決後 IP が SSRF に悪用されないか確認。理由コードも返す |
35
+ | `cidr_contains(cidr, ip_str)` | IP が CIDR 範囲内にあるか。IPv4/IPv6 バージョン不一致は安全に `False` |
36
+ | `ip_range(start, end)` | 範囲内 IP を最大 100 件列挙。超過時は `truncated=True` |
37
+
38
+ ### HTTP エンドポイント
39
+
40
+ | メソッド | パス | 概要 |
41
+ |---|---|---|
42
+ | POST | `/ipaddress/parse` | IP アドレス解析(フィールドバリデータで不正 IP を 422 返却) |
43
+ | POST | `/ipaddress/network` | CIDR ネットワーク解析 |
44
+ | POST | `/ipaddress/ssrf-check` | SSRF 安全性チェック |
45
+ | POST | `/ipaddress/contains` | CIDR 包含チェック |
46
+ | POST | `/ipaddress/range` | IP 範囲列挙 |
47
+
48
+ ---
49
+
50
+ ## テスト結果
51
+
52
+ **42 passed**
53
+
54
+ ```
55
+ 42 passed in 0.32s
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 摩擦ポイント
61
+
62
+ ### F-1: `IPv4Network.num_hosts` が typeshed 未定義(深刻度: 低)
63
+
64
+ **事象**: `net.num_hosts` を呼ぶと mypy が
65
+ `Item "IPv4Network" of "IPv4Network | IPv6Network" has no attribute "num_hosts"` を報告した。
66
+
67
+ **原因**: Python 標準ライブラリの `ipaddress` モジュールには `num_hosts` プロパティが存在するが、
68
+ typeshed の `IPv4Network` / `IPv6Network` スタブには宣言されていない(`_BaseNetwork` に定義されているが継承スタブが未整備)。
69
+
70
+ **対応**: `num_hosts` を使わず、`prefixlen` と `max_prefixlen` から手動計算する。
71
+
72
+ ```python
73
+ # typeshed に num_hosts が未定義のため手動計算
74
+ # /31・/127(ポイントツーポイント)と /32・/128(ホストルート)は全アドレスが使用可能
75
+ if net.prefixlen >= net.max_prefixlen - 1:
76
+ num_hosts = net.num_addresses
77
+ else:
78
+ num_hosts = net.num_addresses - 2
79
+ ```
80
+
81
+ ### F-2: Python 3.11+ で `127.0.0.1.is_private` が `True` に変更(深刻度: 低)
82
+
83
+ **事象**: `parse_address("127.0.0.1")` の結果に `is_private=True` が返り、
84
+ `assert result.is_private is False` のテストが失敗した。
85
+
86
+ **原因**: Python 3.11 で `ipaddress` モジュールの `is_private` の定義が拡張された。
87
+ `127.0.0.0/8`(ループバック)が `is_private=True` を返すように変更された(RFC 1918 の厳密な解釈から RFC 5735 準拠の解釈へ)。
88
+
89
+ **対応**: テストから `is_private=False` の仮定を削除し、`is_global=False` のみを確認。
90
+
91
+ ```python
92
+ def test_ipv4_loopback(self) -> None:
93
+ result = parse_address("127.0.0.1")
94
+ assert result.is_loopback is True
95
+ # Python 3.11+ では 127.0.0.0/8 が is_private=True を返す仕様変更
96
+ assert result.is_global is False
97
+ ```
98
+
99
+ SSRF チェックの `ssrf_safety_check` は `is_loopback` を先にチェックするため影響なし。
100
+
101
+ ### F-3: `ip_range` の `ValueError` が HTTP 500 になる問題(深刻度: 低)
102
+
103
+ **事象**: `ip_range("10.0.0.10", "10.0.0.1")` で `ValueError` が発生するが、
104
+ エンドポイントは 422 を返すべきなのに 500 になっていた。
105
+
106
+ **原因**: Pydantic の Body バリデーションは `start` / `end` を個別に型チェックするが、
107
+ 「end >= start」という相関制約は Pydantic では検出できない。
108
+ `ip_range` から投げられた `ValueError` は FastAPI のデフォルト動作では 500 になる。
109
+
110
+ **対応**: エンドポイントで `ValueError` を捕捉して `HTTPException(status_code=422)` に変換。
111
+
112
+ ```python
113
+ @router.post("/ipaddress/range")
114
+ def range_endpoint(body: RangeBody) -> RangeResult:
115
+ try:
116
+ return ip_range(body.start, body.end)
117
+ except ValueError as exc:
118
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 観察点
124
+
125
+ ### 観察1: `ssrf_safety_check` — SSRF 防御の「セカンドライン」パターン
126
+
127
+ ```python
128
+ def ssrf_safety_check(ip_str: str) -> SafetyCheck:
129
+ addr = ipaddress.ip_address(ip_str)
130
+ if addr.is_loopback:
131
+ return SafetyCheck(address=str(addr), is_safe=False, reason="loopback")
132
+ if addr.is_link_local:
133
+ return SafetyCheck(address=str(addr), is_safe=False, reason="link_local")
134
+ if addr.is_private:
135
+ return SafetyCheck(address=str(addr), is_safe=False, reason="private")
136
+ ...
137
+ ```
138
+
139
+ SSRF 対策は2層構造が安全:
140
+ 1. ファーストライン: URL/ホスト名をドメインパターンで拒否(`localhost`, `*.internal` 等)
141
+ 2. セカンドライン: DNS 解決後に IP を `ssrf_safety_check` で確認
142
+
143
+ FT184(urllib.request)でカバーしたファーストラインと組み合わせることで DNS Rebinding 攻撃にも対応できる。
144
+ `169.254.169.254`(AWS EC2 メタデータ)が `is_link_local=True` で確実にブロックされることを確認した。
145
+
146
+ ### 観察2: `cidr_contains` で `match` 文を使った型安全な分岐
147
+
148
+ ```python
149
+ match (net, addr):
150
+ case (ipaddress.IPv4Network() as net4, ipaddress.IPv4Address() as addr4):
151
+ contains = addr4 in net4
152
+ case (ipaddress.IPv6Network() as net6, ipaddress.IPv6Address() as addr6):
153
+ contains = addr6 in net6
154
+ case _:
155
+ contains = False
156
+ ```
157
+
158
+ `IPv4Address in IPv4Network` は OK だが `IPv6Address in IPv4Network` は `TypeError`。
159
+ `match` で型を同時絞り込みしてバージョン不一致を型レベルで排除できる。
160
+ このパターンは FT193(socket)の `getaddrinfo` 型絞り込みと同じアプローチ。
161
+
162
+ ### 観察3: `@field_validator` で HTTP 境界の IP 形式バリデーション
163
+
164
+ ```python
165
+ class ParseAddressBody(BaseModel):
166
+ address: str = Field(..., max_length=45)
167
+
168
+ @field_validator("address")
169
+ @classmethod
170
+ def validate_ip(cls, v: str) -> str:
171
+ try:
172
+ ipaddress.ip_address(v)
173
+ except ValueError as exc:
174
+ raise ValueError("invalid IP address") from exc
175
+ return v
176
+ ```
177
+
178
+ Pydantic の `str` フィールドはデフォルトで IP 形式を検証しない。
179
+ `@field_validator` で `ipaddress.ip_address()` を呼ぶことで、
180
+ HTTP 境界で不正 IP を 422 として弾ける。
181
+ `ssrf_safety_check` の入力にはあえてバリデータを付けず、
182
+ 内部で `invalid_address` を返す設計にした(サービスの挙動が安定する)。
183
+
184
+ ### 観察4: `ip_range` の `match start:` パターン
185
+
186
+ ```python
187
+ match start:
188
+ case ipaddress.IPv4Address():
189
+ addresses = [str(ipaddress.IPv4Address(int(start) + i)) for i in range(count)]
190
+ case ipaddress.IPv6Address():
191
+ addresses = [str(ipaddress.IPv6Address(int(start) + i)) for i in range(count)]
192
+ ```
193
+
194
+ `ip_address()` の返り値は `IPv4Address | IPv6Address`。
195
+ `ipaddress.ip_address(int(start) + i)` を使うと大きい整数が IPv4 範囲外で IPv6 になるリスクがある。
196
+ `match` でバージョンを先に絞り込んで `IPv4Address(int)` / `IPv6Address(int)` を直接使うことで
197
+ バージョン保存が型レベルで保証される。
198
+
199
+ ---
200
+
201
+ ## nene2-python フレームワークとの統合
202
+
203
+ - `ipaddress` モジュールは nene2 のミドルウェアと直接の接点はないが、
204
+ `ssrf_safety_check` は `nene2.http` や `nene2.middleware` が提供する SSRF 防御ユーティリティの候補。
205
+ - FT184(urllib.request)の SSRF 防御実装では `ipaddress.ip_address(resolved_ip)` をチェックする
206
+ コードが必要になる。今回実装した `ssrf_safety_check` はその実装の参照として使える。
207
+ - HTTP 境界のバリデーションで `@field_validator` + `ipaddress.ip_address()` の組み合わせは
208
+ Pydantic BodyModel に組み込む標準パターンとして CLAUDE.md への追記候補。
209
+ - `ip_range` の `ValueError` → 422 変換は、ドメインロジックの入力エラーをどう HTTP 境界で扱うかの好例。
210
+ `ValidationException` を使う nene2 パターンとの整合も検討余地がある。
211
+
212
+ ---
213
+
214
+ ## Developer Experience (DX) Review
215
+
216
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
217
+
218
+ SSRF という言葉を聞いたことはあるが、「なぜ IP アドレスをコードでチェックするのか」がまだ腑に落ちていない段階。
219
+
220
+ **ドキュメント理解**: `ipaddress.ip_address()` と `ipaddress.ip_network()` の使い方は Python 公式ドキュメントが丁寧で理解しやすい。
221
+ ただし「`is_private` の定義が Python 3.11 で変わった」(F-2)はドキュメントに小さくしか書かれておらず、テストが壊れて初めて気づくことが多い。
222
+ バージョン間の差異を解説する how-to ページがあると助かる。
223
+ **事故リスク**: 中。`is_private` と `is_loopback` を個別にチェックしなければ `127.0.0.1` が抜けるリスクがある(3.11 以前の Python では)。`ssrf_safety_check` のような一元的な関数でラップするパターンを教えると安全。
224
+ **規約の使いやすさ**: `ipaddress.ip_address(str)` → `addr.is_private` のシーケンスは直感的で習得しやすい。
225
+
226
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
227
+
228
+ SSRF チェックを実装したことがあり、`if ip.startswith("192.168")` のような文字列比較でやっていた可能性がある。
229
+
230
+ **コピペ可能性**: `ssrf_safety_check` のコードをそのままコピーできる品質。ただし `is_loopback` と `is_private` の順序を入れ替えると動作が変わる(3.11 以前)ことに気づきにくい。
231
+ **拡張時の罠**: 「IPv4 しかチェックしていない」まま IPv6 対応の要件が来たとき、`is_link_local` の `fe80::/10` などが見落とされやすい。IPv4 と IPv6 を同時にカバーするこの実装は参照価値が高い。
232
+ **セキュリティ的な事故リスク**: 中。`ssrf_safety_check` を使わずに自前チェックを書くと脆弱になる可能性がある。
233
+
234
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
235
+
236
+ IP アドレスの概念は知っているが `is_link_local` や CIDR 表記が馴染み薄い。
237
+
238
+ **エラーレスポンスの質**: 不正 IP に対して `@field_validator` が 422 + `detail: "invalid IP address"` を返すため、クライアント実装が容易。`ssrf_safety_check` が `is_safe=false, reason="link_local"` を返す設計は、クライアント側でブロック理由を表示できて良い。
239
+ **Python 固有概念の学習コスト**: `ipaddress.ip_network("192.168.1.0/24", strict=False)` の `strict=False` の意味は Python 固有。「ホストビットが立っていても正規化して受け入れる」説明が必要。
240
+ **事故リスク**: 低。このエンドポイントは副作用がない解析系。
241
+
242
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
243
+
244
+ IP 検証を `django.core.validators.validate_ipv4_address` で実装してきた経験がある。
245
+
246
+ **他フレームワークとの差異**: Django の `InetAddressField` (django-netfields) より明示的だが、標準ライブラリだけでここまでできる点は評価が高い。`@field_validator` + `ipaddress` の組み合わせは Django フォームのカスタムバリデータと発想が同じで移行コストが低い。
247
+ **nene2-python の薄さへの評価**: SSRF チェックがミドルウェアに組み込まれず「ユースケース層で明示的に呼ぶ」設計は、「魔法を排除して可視化する」nene2 の方針と整合している。どのエンドポイントで SSRF チェックをしているかコードレビューで確認しやすい。
248
+ **本番投入可能性**: `ssrf_safety_check` をユーティリティとして共通化し、外部 URL を受け取るエンドポイントすべてで使う運用が望ましい。
249
+
250
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
251
+
252
+ DNS Rebinding 攻撃を知っており、「SSRF チェックは DNS 解決後に行う」の原則を重視している。
253
+
254
+ **コードレビューチェックポイント**:
255
+ - [x] `ssrf_safety_check` を DNS 解決後に呼んでいるか(解決前に呼んでも意味がない)
256
+ - [x] `is_loopback` の前に `is_link_local` を確認しているか(順序依存なし、全フラグをカバー)
257
+ - [x] IPv6 の `::1` も `is_loopback=True` で弾けているか
258
+ - [x] `169.254.169.254`(AWS メタデータ)が `is_link_local` でブロックされるか
259
+ - [x] バージョン不一致(IPv4 CIDR + IPv6 アドレス)が型安全に `False` を返すか
260
+
261
+ **チームでの安全な共有パターン**: `ssrf_safety_check` を `nene2.http.utils` に追加して、外部 URL を受け取る全エンドポイントで import を強制する仕組みが良い。今後の FT で nene2 コアへの昇格を検討。
262
+ **ツール追加の必要性**: ruff では「SSRF チェックなしで外部 URL を fetch している」パターンを静的に検出できない。コードレビューガイドラインへの追記が実用的。
263
+
264
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
265
+
266
+ CLAUDE.md 「Security first」「初心者でも安全な API」との整合を確認する。
267
+
268
+ **ポリシー達成度**: 高
269
+ **「初心者でも安全な API」達成度**: 高
270
+
271
+ - HTTP 境界で `@field_validator` + `ipaddress.ip_address()` を使って不正 IP を 422 に変換している。初心者がコピーしても「文字列のまま使う」事故を防げる。
272
+ - `ssrf_safety_check` が `loopback` → `link_local` → `private` の順で明示的にチェックするため、SSRF 防御の穴が読みやすい。「何をチェックしているか」が可視化されている。
273
+ - F-2(Python 3.11 の `is_private` 変更)は CLAUDE.md に追記すべき知見。`is_loopback` と `is_private` を別々に扱う必要性と Python バージョン間の差異を記録する。
274
+ - F-3(`ValueError` → 422 変換)は nene2 の `ValidationException` パターンとの整合を今後検討すべき。`HTTPException(status_code=422)` は簡便だが `Problem Details` 形式ではない。
275
+
276
+ **設計上の負債**: `ssrf_safety_check` を nene2 コア (`nene2.http.safety` 等) に昇格させる候補。FT184 で urllib.request に使い、今回で ipaddress との組み合わせが確認できた。
277
+ **Follow-up Issue 候補**: `ssrf_safety_check` の nene2 コア昇格(中優先度)
278
+
279
+ ---
280
+
281
+ ## Follow-up Issues
282
+
283
+ | 優先度 | タイトル | 種別 |
284
+ |---|---|---|
285
+ | 中 | `ssrf_safety_check` ユーティリティを nene2 コアに昇格させる | feat |
286
+ | 低 | Python 3.11+ の `is_private` 仕様変更を docs/how-to に記録 | docs |
287
+
288
+ ※ nene2 コア昇格は FT 単体の PR 内には収めず、別途 Issue/PR で対応する。
289
+
290
+ ---
291
+
292
+ ## まとめ
293
+
294
+ `ipaddress` モジュールは SSRF 防御の実装基盤として非常に実用的だった。
295
+ `169.254.169.254`(AWS メタデータエンドポイント)が `is_link_local=True` で確実にブロックされることを実証し、
296
+ loopback / private / link-local / reserved の全フラグを組み合わせたチェック関数のリファレンス実装を得た。
297
+
298
+ 技術的な発見として、Python 3.11 の `is_private` 定義変更(F-2)と typeshed の `num_hosts` 未定義(F-1)は
299
+ バージョン依存のはまりポイントとして記録価値がある。
300
+
301
+ 次の FT195 は 195 % 3 = 0 → **セキュリティ診断あり**、195 % 4 = 3 → クラッカーペンテストなし。
302
+ テーマ候補: `ssl` モジュール(TLS コンテキスト・証明書検証)または `http.client`(低レベル HTTP クライアント)。
@@ -226,14 +226,16 @@
226
226
  | [FT190](2026-05-field-trial-190.md) | multiprocessing モジュール — プロセスベース並行処理・共有状態・プロセスプール | | |
227
227
  | [FT191](2026-05-field-trial-191.md) | concurrent.futures モジュール — ThreadPoolExecutor / ProcessPoolExecutor / Future | | |
228
228
  | [FT192](2026-05-field-trial-192.md) | asyncio モジュール — コルーチン・タスク・Lock・Event・Semaphore・Queue・TaskGroup | 🔒🔍 | |
229
+ | [FT193](2026-05-field-trial-193.md) | socket モジュール — TCP/UDP socketpair・DNS 解決・ソケットオプション | | |
230
+ | [FT194](2026-05-field-trial-194.md) | ipaddress モジュール — IPv4/IPv6 解析・CIDR 計算・SSRF 防御パターン | | |
229
231
 
230
232
  ---
231
233
 
232
234
  ## セキュリティ診断実施済み一覧(🔒)
233
235
 
234
- FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 121, 124, 127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 174, 177, 180, 183, 186, 189
236
+ FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 121, 124, 127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 174, 177, 180, 183, 186, 189, 192
235
237
 
236
- 合計: **63件**(189 FT 中 約 33%)
238
+ 合計: **64件**(194 FT 中 約 33%)
237
239
 
238
240
  ## クラッカーペンテスト実施済み一覧(🔍)
239
241
 
@@ -241,4 +243,4 @@ FT172, FT176, FT180, FT184, FT188, FT192
241
243
 
242
244
  ---
243
245
 
244
- *最終更新: 2026-05-21 (FT192 / v1.8.64)*
246
+ *最終更新: 2026-05-21 (FT194 / v1.8.66)*