nene2-python 1.8.60__tar.gz → 1.8.63__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 (415) hide show
  1. {nene2_python-1.8.60 → nene2_python-1.8.63}/CLAUDE.md +42 -2
  2. {nene2_python-1.8.60 → nene2_python-1.8.63}/PKG-INFO +1 -1
  3. nene2_python-1.8.63/docs/field-trials/2026-05-field-trial-190.md +220 -0
  4. nene2_python-1.8.63/docs/field-trials/2026-05-field-trial-191.md +214 -0
  5. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/INDEX.md +3 -1
  6. nene2_python-1.8.63/docs/how-to/decimal-unicode-input.md +61 -0
  7. nene2_python-1.8.63/docs/how-to/email-address-parsing.md +57 -0
  8. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/todo/current.md +9 -20
  9. {nene2_python-1.8.60 → nene2_python-1.8.63}/pyproject.toml +1 -1
  10. {nene2_python-1.8.60 → nene2_python-1.8.63}/.env.example +0 -0
  11. {nene2_python-1.8.60 → nene2_python-1.8.63}/.github/workflows/ci.yml +0 -0
  12. {nene2_python-1.8.60 → nene2_python-1.8.63}/.github/workflows/docs.yml +0 -0
  13. {nene2_python-1.8.60 → nene2_python-1.8.63}/.github/workflows/publish.yml +0 -0
  14. {nene2_python-1.8.60 → nene2_python-1.8.63}/.gitignore +0 -0
  15. {nene2_python-1.8.60 → nene2_python-1.8.63}/.vitepress/config.mts +0 -0
  16. {nene2_python-1.8.60 → nene2_python-1.8.63}/.vitepress/theme/custom.css +0 -0
  17. {nene2_python-1.8.60 → nene2_python-1.8.63}/.vitepress/theme/index.ts +0 -0
  18. {nene2_python-1.8.60 → nene2_python-1.8.63}/AGENTS.md +0 -0
  19. {nene2_python-1.8.60 → nene2_python-1.8.63}/CHANGELOG.md +0 -0
  20. {nene2_python-1.8.60 → nene2_python-1.8.63}/Dockerfile +0 -0
  21. {nene2_python-1.8.60 → nene2_python-1.8.63}/LICENSE +0 -0
  22. {nene2_python-1.8.60 → nene2_python-1.8.63}/README.md +0 -0
  23. {nene2_python-1.8.60 → nene2_python-1.8.63}/alembic/README +0 -0
  24. {nene2_python-1.8.60 → nene2_python-1.8.63}/alembic/env.py +0 -0
  25. {nene2_python-1.8.60 → nene2_python-1.8.63}/alembic/script.py.mako +0 -0
  26. {nene2_python-1.8.60 → nene2_python-1.8.63}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  27. {nene2_python-1.8.60 → nene2_python-1.8.63}/alembic.ini +0 -0
  28. {nene2_python-1.8.60 → nene2_python-1.8.63}/compose.yaml +0 -0
  29. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0001-toolchain.md +0 -0
  30. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0002-clean-architecture.md +0 -0
  31. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0003-security-first.md +0 -0
  32. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0004-ai-first-design.md +0 -0
  33. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0005-logging.md +0 -0
  34. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0006-rate-limiting.md +0 -0
  35. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0009-mcp-design.md +0 -0
  36. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0010-async-use-case.md +0 -0
  37. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  38. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/de/index.md +0 -0
  39. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/de/tutorials/getting-started.md +0 -0
  40. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/explanation/architecture.md +0 -0
  41. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/explanation/design-philosophy.md +0 -0
  42. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  43. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  44. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-100.md +0 -0
  45. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-101.md +0 -0
  46. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-102.md +0 -0
  47. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-103.md +0 -0
  48. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-104.md +0 -0
  49. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-105.md +0 -0
  50. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-106.md +0 -0
  51. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-107.md +0 -0
  52. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-108.md +0 -0
  53. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-109.md +0 -0
  54. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  55. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-110.md +0 -0
  56. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-111.md +0 -0
  57. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-112.md +0 -0
  58. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-113.md +0 -0
  59. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-114.md +0 -0
  60. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-115.md +0 -0
  61. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-116.md +0 -0
  62. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-117.md +0 -0
  63. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-118.md +0 -0
  64. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-119.md +0 -0
  65. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  66. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-120.md +0 -0
  67. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-121.md +0 -0
  68. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-122.md +0 -0
  69. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-123.md +0 -0
  70. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-124.md +0 -0
  71. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-125.md +0 -0
  72. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-126.md +0 -0
  73. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-127.md +0 -0
  74. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-128.md +0 -0
  75. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-129.md +0 -0
  76. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  77. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-130.md +0 -0
  78. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-131.md +0 -0
  79. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-132.md +0 -0
  80. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-133.md +0 -0
  81. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-134.md +0 -0
  82. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-135.md +0 -0
  83. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-136.md +0 -0
  84. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-137.md +0 -0
  85. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-138.md +0 -0
  86. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-139.md +0 -0
  87. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  88. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-140.md +0 -0
  89. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-141.md +0 -0
  90. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-142.md +0 -0
  91. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-143.md +0 -0
  92. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-144.md +0 -0
  93. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-145.md +0 -0
  94. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-146.md +0 -0
  95. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-147.md +0 -0
  96. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-148.md +0 -0
  97. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-149.md +0 -0
  98. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  99. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-150.md +0 -0
  100. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-151.md +0 -0
  101. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-152.md +0 -0
  102. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-153.md +0 -0
  103. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-154.md +0 -0
  104. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-155.md +0 -0
  105. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-156.md +0 -0
  106. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-157.md +0 -0
  107. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-158.md +0 -0
  108. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-159.md +0 -0
  109. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  110. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-160.md +0 -0
  111. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-161.md +0 -0
  112. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-162.md +0 -0
  113. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-163.md +0 -0
  114. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-164.md +0 -0
  115. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-165.md +0 -0
  116. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-166.md +0 -0
  117. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-167.md +0 -0
  118. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-168.md +0 -0
  119. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-169.md +0 -0
  120. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  121. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-170.md +0 -0
  122. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-171.md +0 -0
  123. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-172.md +0 -0
  124. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-173.md +0 -0
  125. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-174.md +0 -0
  126. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-175.md +0 -0
  127. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-176.md +0 -0
  128. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-177.md +0 -0
  129. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-178.md +0 -0
  130. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-179.md +0 -0
  131. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  132. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-180.md +0 -0
  133. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-181.md +0 -0
  134. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-182.md +0 -0
  135. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-183.md +0 -0
  136. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-184.md +0 -0
  137. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-185.md +0 -0
  138. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-186.md +0 -0
  139. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-187.md +0 -0
  140. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-188.md +0 -0
  141. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-189.md +0 -0
  142. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  143. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  144. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  145. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  146. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  147. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  148. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  149. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  150. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  151. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  152. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  153. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  154. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  155. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  156. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  157. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  158. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  159. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  160. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  161. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  162. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  163. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  164. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  165. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  166. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  167. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  168. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  169. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  170. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  171. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  172. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  173. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  174. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  175. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  176. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  177. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  178. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  179. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  180. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  181. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  182. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  183. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  184. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  185. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  186. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  187. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  188. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  189. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  190. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  191. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  192. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  193. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  194. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  195. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  196. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  197. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  198. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  199. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  200. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  201. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  202. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  203. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  204. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  205. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  206. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  207. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  208. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  209. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  210. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  211. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  212. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  213. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  214. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  215. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  216. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  217. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  218. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  219. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  220. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  221. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  222. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  223. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  224. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  225. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  226. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  227. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  228. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-97.md +0 -0
  229. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-98.md +0 -0
  230. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/field-trials/2026-05-field-trial-99.md +0 -0
  231. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/fr/index.md +0 -0
  232. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/fr/tutorials/getting-started.md +0 -0
  233. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/add-new-domain.md +0 -0
  234. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/api-versioning.md +0 -0
  235. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/async-use-case.md +0 -0
  236. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/background-tasks.md +0 -0
  237. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/configure-auth.md +0 -0
  238. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/cors.md +0 -0
  239. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/custom-auth-middleware.md +0 -0
  240. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/dependency-injection.md +0 -0
  241. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/domain-events.md +0 -0
  242. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/file-upload.md +0 -0
  243. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/lifespan-and-app-state.md +0 -0
  244. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/middleware-stack.md +0 -0
  245. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/new-project.md +0 -0
  246. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/problem-details.md +0 -0
  247. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/response-patterns.md +0 -0
  248. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/run-tests.md +0 -0
  249. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/soft-delete.md +0 -0
  250. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/sqlalchemy-repository.md +0 -0
  251. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/streaming.md +0 -0
  252. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/structured-logging.md +0 -0
  253. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/validation.md +0 -0
  254. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/how-to/webhook.md +0 -0
  255. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/howto/mcp-setup.md +0 -0
  256. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/index.md +0 -0
  257. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/explanation/architecture.md +0 -0
  258. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/explanation/design-philosophy.md +0 -0
  259. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/how-to/add-new-domain.md +0 -0
  260. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/how-to/configure-auth.md +0 -0
  261. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/how-to/new-project.md +0 -0
  262. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/how-to/run-tests.md +0 -0
  263. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  264. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/howto/mcp-setup.md +0 -0
  265. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/index.md +0 -0
  266. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/reference/api.md +0 -0
  267. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/reference/configuration.md +0 -0
  268. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/reference/framework-modules.md +0 -0
  269. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/tutorials/first-domain.md +0 -0
  270. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/ja/tutorials/getting-started.md +0 -0
  271. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/pt-br/index.md +0 -0
  272. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/pt-br/tutorials/getting-started.md +0 -0
  273. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/reference/api.md +0 -0
  274. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/reference/configuration.md +0 -0
  275. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/reference/framework-modules.md +0 -0
  276. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/roadmap.md +0 -0
  277. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/templates/field-trial-report.md +0 -0
  278. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/tutorials/first-domain.md +0 -0
  279. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/tutorials/getting-started.md +0 -0
  280. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/zh/index.md +0 -0
  281. {nene2_python-1.8.60 → nene2_python-1.8.63}/docs/zh/tutorials/getting-started.md +0 -0
  282. {nene2_python-1.8.60 → nene2_python-1.8.63}/package-lock.json +0 -0
  283. {nene2_python-1.8.60 → nene2_python-1.8.63}/package.json +0 -0
  284. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/__init__.py +0 -0
  285. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/__main__.py +0 -0
  286. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/app.py +0 -0
  287. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/__init__.py +0 -0
  288. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/entity.py +0 -0
  289. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/exceptions.py +0 -0
  290. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/handler.py +0 -0
  291. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/repository.py +0 -0
  292. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/sqlalchemy_repository.py +0 -0
  293. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/comment/use_case.py +0 -0
  294. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/mcp.py +0 -0
  295. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/__init__.py +0 -0
  296. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/async_use_case.py +0 -0
  297. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/entity.py +0 -0
  298. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/exceptions.py +0 -0
  299. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/handler.py +0 -0
  300. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/repository.py +0 -0
  301. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/sqlalchemy_repository.py +0 -0
  302. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/note/use_case.py +0 -0
  303. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/schema.py +0 -0
  304. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/__init__.py +0 -0
  305. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/entity.py +0 -0
  306. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/exceptions.py +0 -0
  307. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/handler.py +0 -0
  308. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/repository.py +0 -0
  309. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/sqlalchemy_repository.py +0 -0
  310. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/example/tag/use_case.py +0 -0
  311. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/__init__.py +0 -0
  312. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/__init__.py +0 -0
  313. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/api_key.py +0 -0
  314. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/bearer_token.py +0 -0
  315. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/deps.py +0 -0
  316. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/exceptions.py +0 -0
  317. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/interfaces.py +0 -0
  318. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/auth/local_verifier.py +0 -0
  319. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/cache/__init__.py +0 -0
  320. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/cache/ttl.py +0 -0
  321. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/config/__init__.py +0 -0
  322. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/config/settings.py +0 -0
  323. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/__init__.py +0 -0
  324. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/exceptions.py +0 -0
  325. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/health.py +0 -0
  326. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/interfaces.py +0 -0
  327. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/sqlalchemy_executor.py +0 -0
  328. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/database/utils.py +0 -0
  329. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/http/__init__.py +0 -0
  330. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/http/etag.py +0 -0
  331. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/http/health.py +0 -0
  332. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/http/pagination.py +0 -0
  333. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/http/problem_details.py +0 -0
  334. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/log/__init__.py +0 -0
  335. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/log/setup.py +0 -0
  336. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/mcp/__init__.py +0 -0
  337. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/mcp/http_client.py +0 -0
  338. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/mcp/server.py +0 -0
  339. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/__init__.py +0 -0
  340. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/domain_exception.py +0 -0
  341. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/error_handler.py +0 -0
  342. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/request_id.py +0 -0
  343. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/request_logging.py +0 -0
  344. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/request_size_limit.py +0 -0
  345. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/security_headers.py +0 -0
  346. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/setup.py +0 -0
  347. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/middleware/throttle.py +0 -0
  348. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/py.typed +0 -0
  349. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/security/__init__.py +0 -0
  350. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/security/webhook.py +0 -0
  351. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/use_case/__init__.py +0 -0
  352. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/use_case/protocols.py +0 -0
  353. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/validation/__init__.py +0 -0
  354. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/nene2/validation/exceptions.py +0 -0
  355. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/scripts/__init__.py +0 -0
  356. {nene2_python-1.8.60 → nene2_python-1.8.63}/src/scripts/export_openapi.py +0 -0
  357. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/__init__.py +0 -0
  358. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/conftest.py +0 -0
  359. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/__init__.py +0 -0
  360. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/comment/__init__.py +0 -0
  361. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/comment/test_comment_http.py +0 -0
  362. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/comment/test_comment_repository.py +0 -0
  363. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/comment/test_comment_use_case.py +0 -0
  364. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/conftest.py +0 -0
  365. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/note/__init__.py +0 -0
  366. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/note/test_async_note_use_case.py +0 -0
  367. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/note/test_list_notes.py +0 -0
  368. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/note/test_note_repository.py +0 -0
  369. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/tag/__init__.py +0 -0
  370. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/tag/test_tag_repository.py +0 -0
  371. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/tag/test_tags.py +0 -0
  372. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/test_cors.py +0 -0
  373. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/example/test_mcp.py +0 -0
  374. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/__init__.py +0 -0
  375. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/auth/__init__.py +0 -0
  376. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/auth/test_api_key.py +0 -0
  377. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/auth/test_bearer_token.py +0 -0
  378. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/auth/test_make_require_auth.py +0 -0
  379. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/auth/test_token_issuer.py +0 -0
  380. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/cache/__init__.py +0 -0
  381. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/cache/test_ttl.py +0 -0
  382. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/config/__init__.py +0 -0
  383. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/config/test_settings.py +0 -0
  384. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/database/__init__.py +0 -0
  385. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/database/test_transaction.py +0 -0
  386. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/database/test_utils.py +0 -0
  387. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/http/__init__.py +0 -0
  388. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/http/test_etag.py +0 -0
  389. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/http/test_health.py +0 -0
  390. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/http/test_pagination.py +0 -0
  391. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/http/test_problem_details.py +0 -0
  392. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/log/__init__.py +0 -0
  393. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/log/test_setup.py +0 -0
  394. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/mcp/__init__.py +0 -0
  395. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/mcp/test_http_client.py +0 -0
  396. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/mcp/test_server.py +0 -0
  397. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/__init__.py +0 -0
  398. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_error_handler.py +0 -0
  399. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_request_id.py +0 -0
  400. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_request_logging.py +0 -0
  401. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  402. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_security_headers.py +0 -0
  403. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  404. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  405. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/middleware/test_throttle.py +0 -0
  406. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/security/__init__.py +0 -0
  407. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/security/test_webhook.py +0 -0
  408. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/use_case/__init__.py +0 -0
  409. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/use_case/test_protocols.py +0 -0
  410. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  411. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/validation/__init__.py +0 -0
  412. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/nene2/validation/test_exceptions.py +0 -0
  413. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/scripts/__init__.py +0 -0
  414. {nene2_python-1.8.60 → nene2_python-1.8.63}/tests/scripts/test_export_openapi.py +0 -0
  415. {nene2_python-1.8.60 → nene2_python-1.8.63}/uv.lock +0 -0
@@ -130,6 +130,11 @@ print(sensitive_data) # logging モジュールを使う
130
130
  - **セキュリティヘッダーをミドルウェアで付与**(X-Content-Type-Options, X-Frame-Options, etc.)
131
131
  - **SQL はパラメータ化クエリのみ**。文字列フォーマット禁止
132
132
  - **ファイルパスは `pathlib.Path` で操作**し、パストラバーサルを防ぐ
133
+ - **XML 処理には `defusedxml` を使用**。標準の `xml.etree.ElementTree` は XXE・展開爆弾に脆弱(FT180 で確認)
134
+ ```bash
135
+ uv add defusedxml
136
+ ```
137
+ `import xml.etree.ElementTree` の代わりに `import defusedxml.ElementTree` を使う。
133
138
 
134
139
  ### 依存関係の脆弱性スキャン
135
140
 
@@ -233,6 +238,37 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
233
238
  - `nene2.http.problem_details_response()` で RFC 9457 エラー応答
234
239
  - `nene2.http.PaginationQueryParser` でページネーション
235
240
 
241
+ ### APIRouter パターン(必須)
242
+
243
+ すべての FastAPI アプリで `APIRouter` + `create_app()` ファクトリパターンを使うこと。
244
+
245
+ ```python
246
+ # ✅ 正しい構造 — app.py
247
+ router = APIRouter()
248
+
249
+ @router.post("/items") # ← すべてのルート定義は router に紐付ける
250
+ def create_item(...): ...
251
+
252
+ @router.get("/items/{item_id}")
253
+ def get_item(...): ...
254
+
255
+ def create_app() -> FastAPI: # ← create_app() はファイル末尾に定義する
256
+ application = FastAPI(title="...")
257
+ application.include_router(router)
258
+ return application
259
+
260
+ app = create_app() # ← モジュールレベルの app は最終行
261
+ ```
262
+
263
+ **`create_app()` はファイルの末尾**(全 `@router.xxx()` デコレーター定義の後)に置くこと。
264
+ 先に `app = create_app()` を呼ぶと `router` にルートが登録される前に `include_router()` が実行され、
265
+ エンドポイントが空になるバグが発生する(FT182 で発見)。
266
+
267
+ - `router = APIRouter()` → ファイル先頭の定数・モデル定義の後
268
+ - `@router.post(...)` デコレーター → ハンドラー関数の定義
269
+ - `create_app()` → ファイル末尾
270
+ - `app = create_app()` → ファイル最終行
271
+
236
272
  ### ミドルウェアスタック順序(重要)
237
273
 
238
274
  `app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
@@ -378,7 +414,11 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
378
414
  テンプレート: docs/templates/field-trial-report.md
379
415
  6. DX Review(6ペルソナ)を実施(後述)
380
416
  7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
381
- 8. Follow-up Issues を GitHub Issue に変換
417
+ 8. Follow-up Issues をその場で修正してからクローズする
418
+ - 発見した問題(摩擦点・セキュリティ指摘)は FT PR に含めて修正する
419
+ - 修正 → テスト全通過 → PR に含める → GitHub Issue は PR 内でクローズ(Closes #NNN)
420
+ - CLAUDE.md 追記・docs 更新・サンドボックスのコード修正すべてを同じ PR に含める
421
+ - 「外部依存の修正待ち」など対応不可能な理由がある場合のみ Issue を残し、理由を PR 説明に記載する
382
422
  9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
383
423
  ```
384
424
 
@@ -418,7 +458,7 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
418
458
 
419
459
  **合否判定**:
420
460
  - **合格**: 全カテゴリ問題なし
421
- - **条件付き合格**: MEDIUM 以下の指摘のみ、次 FT までに修正
461
+ - **条件付き合格**: MEDIUM 以下の指摘のみ → **同 FT の PR 内で修正してからマージ**
422
462
  - **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正
423
463
 
424
464
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.60
3
+ Version: 1.8.63
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,220 @@
1
+ # FT190: multiprocessing モジュール
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: プロセスベース並行処理・共有状態・プロセスプール
5
+ **セキュリティ診断**: なし(190 % 3 = 1)
6
+
7
+ ---
8
+
9
+ ## 概要
10
+
11
+ `multiprocessing` は threading と異なりプロセスを分離して実行するため、GIL(Global Interpreter Lock)の影響を受けず CPU バウンドタスクの並列化に有効。本 FT では `Pool.map` / `Pool.starmap` / `Pool.imap` / `Pool.apply_async`・共有メモリ(`Value`)・プロセス間キュー(`Queue`)・初期化関数付きプールなどの主要パターンを FastAPI エンドポイントから検証する。
12
+
13
+ FT188(threading)・FT189(subprocess)の直後として、プロセスベース並行処理の違いと型安全上の注意点を記録することも目的とする。
14
+
15
+ ---
16
+
17
+ ## 実装したサンプルアプリ
18
+
19
+ **場所**: `/home/xi/docker/nene2-python-FT/ft190-multiprocessing/`
20
+
21
+ ### 主要機能
22
+
23
+ | 関数/クラス | 概要 |
24
+ |---|---|
25
+ | `spawn_process(name)` | 単一プロセスを起動して PID・Alive 状態を返す |
26
+ | `pool_map(values, workers)` | Pool.map で並列二乗計算 |
27
+ | `pool_map_cube(values, workers)` | Pool.map で並列三乗計算 |
28
+ | `pool_starmap(pairs, workers)` | Pool.starmap で並列加算 |
29
+ | `apply_async_demo(values, delay)` | apply_async で非同期タスク |
30
+ | `shared_counter_demo(num_processes, increments_each)` | Value + Lock で共有カウンター |
31
+ | `queue_producer_consumer(items)` | Queue でプロデューサー/コンシューマー |
32
+ | `pool_imap_ordered(values, workers)` | Pool.imap(順序保証) |
33
+ | `pool_imap_unordered(values, workers)` | Pool.imap_unordered(順序不定) |
34
+ | `pool_map_chunksize(values, chunksize, workers)` | チャンクサイズ付き Pool.map |
35
+ | `pool_with_initializer(values, config)` | 初期化関数付き Pool |
36
+ | `build_task_func(operation)` | 操作名から関数を返す(match 文使用) |
37
+
38
+ ### HTTP エンドポイント
39
+
40
+ | メソッド | パス | 概要 |
41
+ |---|---|---|
42
+ | POST | `/multiprocessing/spawn` | プロセス起動・PID 取得 |
43
+ | GET | `/multiprocessing/cpu-count` | CPU コア数・start method |
44
+ | POST | `/multiprocessing/pool-map` | 並列二乗計算 |
45
+ | POST | `/multiprocessing/pool-map-cube` | 並列三乗計算 |
46
+ | POST | `/multiprocessing/pool-starmap` | 並列加算 |
47
+ | POST | `/multiprocessing/apply-async` | 非同期タスク |
48
+ | POST | `/multiprocessing/shared-counter` | 共有カウンター(Lock 付き) |
49
+ | POST | `/multiprocessing/queue` | プロデューサー/コンシューマー |
50
+ | POST | `/multiprocessing/imap-ordered` | 順序保証 imap |
51
+ | POST | `/multiprocessing/imap-unordered` | 順序不定 imap |
52
+ | POST | `/multiprocessing/chunksize` | チャンクサイズ指定 map |
53
+ | POST | `/multiprocessing/with-initializer` | 初期化関数付きプール |
54
+ | GET | `/multiprocessing/daemon-demo` | デーモンプロセスデモ |
55
+
56
+ ---
57
+
58
+ ## テスト結果
59
+
60
+ **56 passed**
61
+
62
+ ```
63
+ 56 passed in 1.18s
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 摩擦ポイント
69
+
70
+ ### F-1: Pool.starmap にローカル関数を渡すと PicklingError(深刻度: 中)
71
+
72
+ **事象**: `pool_starmap()` 内でローカル関数 `add` を定義して `pool.starmap(add, pairs)` に渡したところ、`_pickle.PicklingError: Can't pickle local object` が発生した。
73
+
74
+ **原因**: multiprocessing はワーカープロセスにタスクを pickle で送信する。ローカル関数はモジュールトップレベルに存在しないため、ワーカーが unpickle できない。threading ではそのまま渡せるため、つい同じように書いてしまう。
75
+
76
+ **対応**: `_add(a, b)` としてモジュールレベルに定義。ワーカー用関数は必ずモジュールレベルに置くルールを確認(threading と multiprocessing の違い)。
77
+
78
+ ### F-2: `multiprocessing.Value` に `Synchronized[c_int]` を使うと mypy エラー(深刻度: 低)
79
+
80
+ **事象**: `counter: Synchronized[c_int] = Value(c_int, 0)` と書くと、`counter.value += 1` に対して `Unsupported operand types for + ("c_int" and "int")` エラーが発生した。
81
+
82
+ **原因**: typeshed の `Synchronized[_CT]` は `.value` を `_CT` 型として定義しており、`_CT = c_int` のとき `c_int + int` が不正になる。しかし実際の runtime では `Value("i", 0).value` は Python の `int` を返す。型スタブが実態と乖離している。
83
+
84
+ **対応**: `Synchronized[int]` とアノテーションし `Value("i", 0)` (文字列フォーマットコード) を使用。mypy は文字列フォーマットコードからジェネリック型を解決できないため `Synchronized[int]` の明示アノテーションで型整合が取れる。
85
+
86
+ ---
87
+
88
+ ## 観察点
89
+
90
+ ### 観察1: Pool ワーカー関数のスコープ制約
91
+
92
+ multiprocessing の Pool ワーカーは pickle でシリアライズしてワーカープロセスに送信する。pickle できるのはモジュールトップレベルで定義された関数のみ。lambda・クロージャ・ローカル関数は pickle できない。
93
+
94
+ ```python
95
+ # ❌ PicklingError
96
+ def my_func(pairs):
97
+ def add(a, b): return a + b
98
+ with Pool() as pool:
99
+ return pool.starmap(add, pairs)
100
+
101
+ # ✅ 正しい
102
+ def _add(a: int, b: int) -> int: # モジュールレベル
103
+ return a + b
104
+
105
+ def my_func(pairs):
106
+ with Pool() as pool:
107
+ return pool.starmap(_add, pairs)
108
+ ```
109
+
110
+ ### 観察2: GIL と multiprocessing
111
+
112
+ threading では GIL により Python バイトコードの並列実行が制限される(I/O バウンドは並行可)が、multiprocessing は別プロセスなので GIL を回避して CPU バウンドタスクを並列化できる。プロセス起動コスト(~50ms)があるため、軽量タスクには Pool よりも threading が適する。
113
+
114
+ ### 観察3: Value の型アノテーション戦略
115
+
116
+ ```python
117
+ # 実態と合うアノテーション
118
+ from multiprocessing.sharedctypes import Synchronized
119
+ counter: Synchronized[int] = Value("i", 0) # 文字列フォーマットコードを使う
120
+ ```
121
+
122
+ `Synchronized[int]` とすることで `counter.value` が `int` として扱われ、`counter.value += 1` が mypy --strict を通過する。`Value(c_int, 0)` の形式は typeshed との不整合を生む。
123
+
124
+ ---
125
+
126
+ ## nene2-python フレームワークとの統合
127
+
128
+ - Pool ワーカー関数はモジュールレベルに置く制約があるため、プロセスプールを使う UseCase では関数をモジュールレベルの `_private` 関数として分離するパターンが必要
129
+ - FastAPI のリクエストハンドラー内で `Pool` を生成する場合、`with Pool() as pool:` のコンテキストマネージャーで確実にクリーンアップすること(`pool.terminate()` の漏れ防止)
130
+ - multiprocessing は `__main__` ガード(`if __name__ == "__main__":`)が必要な start method(spawn/forkserver)があるが、FastAPI アプリとして使う場合は `fork`(Linux デフォルト)なので不要。ただし Windows 移植時は注意
131
+ - `MAX_WORKERS: int = 8` 定数でワーカー数を上限制限し、DoS を防止
132
+
133
+ ---
134
+
135
+ ## Developer Experience (DX) Review
136
+
137
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
138
+
139
+ チュートリアルで threading を学んだ後、multiprocessing に入門する段階。
140
+
141
+ **ドキュメント理解**: `Pool.map` の使い方は直感的で理解しやすい。ワーカー関数がモジュールレベルでないと `PicklingError` になる制約は、threading と混同して気づきにくい(F-1)。エラーメッセージが英語で `Can't pickle local object` と出るので原因の特定は可能だが、初心者には意味が分かりにくい。
142
+ **事故リスク**: 中。PicklingError は実行時エラーで早期発見できるが、初心者が「なぜ動かないのか」を理解するまでに時間がかかる。
143
+ **規約の使いやすさ**: `with Pool(processes=n) as pool:` のパターンは覚えやすい。ワーカー関数をモジュールレベルに置く規則は一度理解すれば機械的に適用できる。
144
+
145
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
146
+
147
+ threading コードをコピーして multiprocessing に変えようとする場面。
148
+
149
+ **コピペ可能性**: `Pool.map` のサンプルは分かりやすい。ただし、ラムダや内側クロージャを気軽に使うと PicklingError になる。threading では動いたコードをそのまま転用できないケースがある。
150
+ **拡張時の罠**: `Value("i", 0)` の型アノテーションを `Synchronized[c_int]` と書くと mypy エラーになる(F-2)。型を「直しよう」としてはまる。`Synchronized[int]` と書く正解はドキュメントに明示されていない。
151
+ **セキュリティ的な事故リスク**: 中。`workers` 上限がなければ `Pool(processes=99999)` でプロセス枯渇 DoS が可能。本実装では `MAX_WORKERS = 8` で上限制限している。
152
+
153
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
154
+
155
+ 並行処理の概念(Promise.all 的なもの)はわかるが、プロセスとスレッドの違いが曖昧な段階。
156
+
157
+ **エラーレスポンスの質**: 422 Unprocessable Entity が `workers` 超過・`values` 超過で正しく返る。`PicklingError` は HTTP 500 になるが、デモコードのワーカー関数はモジュールレベルに固定しているため HTTP 経由では発生しない。
158
+ **Python 固有概念の学習コスト**: `Pool` が Python オブジェクトプールではなくプロセスプールであること、`fork` vs `spawn` の start method の違いは非直感的。
159
+ **事故リスク**: 低。HTTP 入力のバリデーションが Pydantic で守られており、エンドポイント経由では PicklingError には到達できない。
160
+
161
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
162
+
163
+ Celery・concurrent.futures との比較で評価する。
164
+
165
+ **他フレームワークとの差異**: `concurrent.futures.ProcessPoolExecutor` と `multiprocessing.Pool` は機能が重複する。FastAPI アプリで重い CPU 処理をオフロードするなら `ProcessPoolExecutor` の方が Python 公式の高レベル API として推奨されている。本 FT が `Pool` を選んだのは stdlib の低レベル API を直接学ぶため。
166
+ **nene2-python の薄さへの評価**: UseCase 層が HTTP・DB 非依存なので、Pool ワーカーに UseCase を渡す設計も可能(ただし pickle 可能なオブジェクトに限る)。
167
+ **本番投入可能性**: `MAX_WORKERS` の上限設定・`with Pool() as pool:` のコンテキスト管理が適切。本番では `ProcessPoolExecutor` との使い分けガイドが欲しい。
168
+
169
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
170
+
171
+ チームで multiprocessing を使う場合のリスクをレビューする。
172
+
173
+ **コードレビューチェックポイント**:
174
+ - [ ] `Pool.map` のワーカー関数がモジュールレベルか(ローカル関数・lambda は PicklingError)
175
+ - [ ] `with Pool() as pool:` でコンテキストマネージャーを使い、確実に終了しているか
176
+ - [ ] `workers` に上限制限があるか(`min(workers, MAX_WORKERS)` パターン)
177
+ - [ ] `join(timeout=N)` でゾンビプロセス防止が書かれているか
178
+ - [ ] `Value` の型アノテーションが `Synchronized[int]` か(`Synchronized[c_int]` は mypy 不整合)
179
+
180
+ **チームでの安全な共有パターン**: ワーカー関数ファイルを `_workers.py` として分離する規則を設けると pickle 可能性が明確になる。
181
+ **ツール追加の必要性**: ruff には multiprocessing 固有のルールはない。`pool.map(lambda x: x, [])` のようなラムダ誤用は静的解析では検出できない(実行時エラー)。
182
+
183
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
184
+
185
+ CLAUDE.md ポリシーとの整合性を確認する。
186
+
187
+ **ポリシー達成度**: 高
188
+ **「初心者でも安全な API」達成度**: 中(PicklingError は HTTP 経由では発生しないが、demos.py を直接使う場面では踏みやすい)
189
+ **設計上の負債・ドキュメント不足**: `multiprocessing.Value` の型アノテーション方法(`Synchronized[int]` vs `Synchronized[c_int]`)が typeshed の実態と乖離している点は How-to に記録する価値がある(→ Follow-up Issue)
190
+ **Follow-up Issues**: 下記参照
191
+
192
+ ---
193
+
194
+ ## Follow-up Issues
195
+
196
+ 今回の FT で発見した問題を同 FT PR 内で即時対応済み(バックログを残さないルール)。
197
+
198
+ ### 即時対応済み
199
+
200
+ | 対応内容 | 対応方法 |
201
+ |---|---|
202
+ | Pool.starmap にローカル関数を渡して PicklingError(F-1) | `_add()` をモジュールレベルへ移動 |
203
+ | `Synchronized[c_int]` mypy 不整合(F-2) | `Synchronized[int]` + `Value("i", 0)` に変更 |
204
+
205
+ ### 文書化 Issue(同 PR で作成・クローズ)
206
+
207
+ | タイトル | 種別 |
208
+ |---|---|
209
+ | multiprocessing.Value のアノテーションには `Synchronized[int]` を使う | docs |
210
+
211
+ ---
212
+
213
+ ## まとめ
214
+
215
+ multiprocessing の主要パターン(Pool.map/starmap/imap/apply_async・Value・Queue・初期化関数)を 13 エンドポイント・56 テストで検証した。FT190 固有の発見は 2 点:
216
+
217
+ 1. **PicklingError**: Pool ワーカー関数のモジュールレベル配置制約(threading との差異)
218
+ 2. **Value 型アノテーション**: `Synchronized[c_int]` は typeshed と乖離、`Synchronized[int]` + 文字列フォーマットコードで回避
219
+
220
+ いずれも実装中に即時修正済み。FT191 に向けた懸案はなし。
@@ -0,0 +1,214 @@
1
+ # FT191: concurrent.futures モジュール
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: ThreadPoolExecutor / ProcessPoolExecutor / Future — 高レベル並行処理 API
5
+ **セキュリティ診断**: なし(191 % 3 = 2)
6
+
7
+ ---
8
+
9
+ ## 概要
10
+
11
+ `concurrent.futures` は threading と multiprocessing の上に薄い高レベル API を提供する stdlib モジュール。`ThreadPoolExecutor` / `ProcessPoolExecutor` を同一インターフェースで操作でき、`Future` オブジェクトで非同期タスクを管理できる。
12
+
13
+ 本 FT では `.map()` / `.submit()` / `as_completed()` / `wait()` / タイムアウト / キャンセル / エラーハンドリングを FastAPI エンドポイントから検証する。FT188(threading)・FT189(subprocess)・FT190(multiprocessing)の高レベル抽象として位置付ける。
14
+
15
+ ---
16
+
17
+ ## 実装したサンプルアプリ
18
+
19
+ **場所**: `/home/xi/docker/nene2-python-FT/ft191-concurrent-futures/`
20
+
21
+ ### 主要機能
22
+
23
+ | 関数/クラス | 概要 |
24
+ |---|---|
25
+ | `thread_pool_map(values, workers)` | ThreadPoolExecutor.map で並列二乗 |
26
+ | `thread_pool_submit(values, workers)` | ThreadPoolExecutor.submit で Future 取得 |
27
+ | `thread_as_completed(values, workers)` | as_completed で完了順に収集 |
28
+ | `thread_wait_all_completed(values, workers)` | wait(ALL_COMPLETED) |
29
+ | `thread_wait_first_completed(values, workers)` | wait(FIRST_COMPLETED) |
30
+ | `thread_wait_first_exception(values, workers)` | wait(FIRST_EXCEPTION) |
31
+ | `batch_with_error_handling(values, workers)` | 例外を握りつぶさず成功/失敗分類 |
32
+ | `process_pool_map(values, workers)` | ProcessPoolExecutor.map で CPU バウンド |
33
+ | `process_pool_submit(values, workers)` | ProcessPoolExecutor.submit |
34
+ | `process_as_completed(values, workers)` | ProcessPoolExecutor + as_completed |
35
+ | `submit_with_timeout(seconds, timeout)` | タイムアウト付き Future |
36
+ | `submit_and_cancel(values)` | キャンセル試行 |
37
+ | `thread_map_with_chunksize(values, chunksize, workers)` | チャンクサイズ指定 map |
38
+
39
+ ### HTTP エンドポイント
40
+
41
+ | メソッド | パス | 概要 |
42
+ |---|---|---|
43
+ | POST | `/futures/thread-map` | ThreadPoolExecutor.map |
44
+ | POST | `/futures/thread-submit` | submit で Future 取得 |
45
+ | POST | `/futures/thread-as-completed` | as_completed |
46
+ | POST | `/futures/thread-wait-all` | wait(ALL_COMPLETED) |
47
+ | POST | `/futures/thread-wait-first` | wait(FIRST_COMPLETED) |
48
+ | POST | `/futures/thread-wait-exception` | wait(FIRST_EXCEPTION) |
49
+ | POST | `/futures/batch-errors` | エラーハンドリング付きバッチ |
50
+ | POST | `/futures/process-map` | ProcessPoolExecutor.map |
51
+ | POST | `/futures/process-submit` | ProcessPoolExecutor.submit |
52
+ | POST | `/futures/process-as-completed` | ProcessPoolExecutor + as_completed |
53
+ | POST | `/futures/timeout` | タイムアウト付き Future |
54
+ | POST | `/futures/chunksize` | チャンクサイズ指定 |
55
+ | POST | `/futures/cancel` | キャンセル試行 |
56
+ | GET | `/futures/info` | 実行環境情報 |
57
+
58
+ ---
59
+
60
+ ## テスト結果
61
+
62
+ **51 passed**
63
+
64
+ ```
65
+ 51 passed in 10.86s
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 摩擦ポイント
71
+
72
+ ### F-1: `submit_and_cancel` の戻り値型 `dict[str, int]` が `dict[str, object]` に非互換(深刻度: 低)
73
+
74
+ **事象**: `submit_and_cancel()` が `dict[str, int]` を返し、エンドポイントの戻り値型 `dict[str, object]` に対して mypy --strict が `Incompatible return value type` エラーを出した。
75
+
76
+ **原因**: mypy では `dict[str, int]` は `dict[str, object]` の部分型でない(`dict` は invariant)。TS の `Record<string, number>` を `Record<string, unknown>` に代入できないのと同じ理屈。
77
+
78
+ **対応**: `submit_and_cancel` の戻り値型を `dict[str, object]` に変更。戻り値の幅を広げても型安全性は失われない(返す値はすべて `int`)。
79
+
80
+ ---
81
+
82
+ ## 観察点
83
+
84
+ ### 観察1: ThreadPoolExecutor vs ProcessPoolExecutor の使い分け
85
+
86
+ ```python
87
+ # I/O バウンド → ThreadPoolExecutor(GIL 解放待ちの間に他スレッドが走る)
88
+ with ThreadPoolExecutor(max_workers=4) as executor:
89
+ results = list(executor.map(fetch_url, urls))
90
+
91
+ # CPU バウンド → ProcessPoolExecutor(GIL を完全に回避)
92
+ with ProcessPoolExecutor(max_workers=4) as executor:
93
+ results = list(executor.map(heavy_compute, values))
94
+ ```
95
+
96
+ threading と multiprocessing の低レベル API と同一の選択基準だが、インターフェースが統一されているため交換が容易。
97
+
98
+ ### 観察2: as_completed vs map の選択
99
+
100
+ ```python
101
+ # map: 送信順で結果が返る(遅いタスクがブロック)
102
+ results = list(executor.map(func, values))
103
+
104
+ # as_completed: 完了順で返る(高速タスクの結果を先に処理可能)
105
+ for future in as_completed(futures):
106
+ result = future.result()
107
+ process_early(result)
108
+ ```
109
+
110
+ HTTP API でストリームレスポンスを返す場合や、部分結果を早期返却する設計では `as_completed` が有利。
111
+
112
+ ### 観察3: wait() の return_when フラグ
113
+
114
+ | フラグ | 用途 |
115
+ |---|---|
116
+ | `ALL_COMPLETED` | 全タスク完了を待つ(デフォルト) |
117
+ | `FIRST_COMPLETED` | 最初のタスクが終わったら戻る |
118
+ | `FIRST_EXCEPTION` | 最初の例外発生で戻る(残タスクはキャンセルしない) |
119
+
120
+ `FIRST_EXCEPTION` は例外をすぐ検知したいが残タスクは並行継続したい場合に使う。
121
+
122
+ ### 観察4: Future.cancel() の制約
123
+
124
+ `cancel()` はタスクが**まだ実行開始されていない**場合のみ成功する。既に実行中のタスクはキャンセルできない(Python の Future はキャンセル可能 Flag のみで、OS レベルのプロセス終了は行わない)。max_workers=1 でタスクを大量投入した場合のみキャンセルが効果的。
125
+
126
+ ---
127
+
128
+ ## nene2-python フレームワークとの統合
129
+
130
+ - `ThreadPoolExecutor` は I/O バウンド UseCase(外部 API 並列呼び出し等)に適用可能。ただし FastAPI はデフォルト非同期(asyncio)であり、重い I/O は `httpx.AsyncClient` での `asyncio.gather` が自然な選択
131
+ - `ProcessPoolExecutor` は CPU バウンド変換処理(画像変換・暗号化・データ集計)を同期 UseCase として切り出す際に使う
132
+ - `max_workers` の上限制限は DoS 防止のために必須。`min(workers, MAX_WORKERS)` パターンを全関数で適用
133
+ - ProcessPoolExecutor のワーカー関数も multiprocessing と同様 pickle 可能なモジュールレベル関数に限定される(FT190 F-1 と同じ制約)
134
+
135
+ ---
136
+
137
+ ## Developer Experience (DX) Review
138
+
139
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
140
+
141
+ threading/multiprocessing を学んだ後、高レベル API として concurrent.futures を使おうとしている段階。
142
+
143
+ **ドキュメント理解**: `with ThreadPoolExecutor() as executor: executor.map(func, data)` のパターンは直感的で理解しやすい。`as_completed`・`wait` は公式ドキュメントの例が豊富で困らない。`FIRST_EXCEPTION` フラグの意味は名前から推測できる。
144
+ **事故リスク**: 低。エラーハンドリングを省略すると `future.result()` で例外が再 raise されるため、未処理の例外は実行時に気づける。
145
+ **規約の使いやすさ**: `with executor:` の `with` ブロックは必須習慣で、抜け漏れ時は executor が自動終了するため安全。
146
+
147
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
148
+
149
+ 既存スクリプトの `for` ループを並列化したくてコピーして使うスタイル。
150
+
151
+ **コピペ可能性**: `executor.map(func, data)` のサンプルはそのままコピーして動く。`as_completed` のパターンも明確。
152
+ **拡張時の罠**: ProcessPoolExecutor でラムダを渡すと PicklingError(FT190 F-1 の再現)。threading でも同じコードで動くため気づきにくい。
153
+ **セキュリティ的な事故リスク**: 中。`max_workers` に上限がないと DoS につながる。本実装では `MAX_WORKERS = 8` で制限。
154
+
155
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
156
+
157
+ JavaScript の `Promise.all` / `Promise.race` との比較で理解しようとしている段階。
158
+
159
+ **エラーレスポンスの質**: `batch_with_error_handling` パターンで成功/失敗を分けて返すと、クライアントが部分成功を処理しやすい。422 バリデーションエラーは自動返却される。
160
+ **Python 固有概念の学習コスト**: `Future` は JS の `Promise` に近い。`as_completed` は `Promise.race` の複数解決版として理解できる。`wait(FIRST_COMPLETED)` が `Promise.race`、`wait(ALL_COMPLETED)` が `Promise.all` に相当する。
161
+ **事故リスク**: 低。HTTP 入力のバリデーションが Pydantic で保護されている。
162
+
163
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
164
+
165
+ asyncio vs concurrent.futures の使い分けを判断する立場。
166
+
167
+ **他フレームワークとの差異**: FastAPI は async/await が基本なので、I/O 並列は `asyncio.gather` が自然な選択。`concurrent.futures` は CPU バウンドと、非同期対応していないレガシーライブラリの同期 I/O をスレッドプールで包む用途に限定される。`loop.run_in_executor()` で asyncio と統合できる。
168
+ **nene2-python の薄さへの評価**: UseCase 層が HTTP 非依存なので、`ThreadPoolExecutor` を UseCase 内で直接使う設計も許容される。ただし `asyncio` 移行を前提とする場合は技術的負債になりやすい。
169
+ **本番投入可能性**: チームが asyncio に慣れているなら concurrent.futures は補助的な役割に留めるべき。混在するとコードの可読性が下がる。
170
+
171
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
172
+
173
+ コードレビューで concurrent.futures の誤用を検出する立場。
174
+
175
+ **コードレビューチェックポイント**:
176
+ - [ ] `max_workers` に上限制限があるか(無制限はリソース枯渇)
177
+ - [ ] `with executor:` のコンテキストマネージャーを使っているか(`executor.shutdown()` の漏れ防止)
178
+ - [ ] ProcessPoolExecutor のワーカー関数がモジュールレベルか(PicklingError 防止)
179
+ - [ ] `future.result()` の例外ハンドリングが書かれているか(未処理は実行時エラーが伝播する)
180
+ - [ ] タイムアウトが指定されているか(`future.result(timeout=N)` や `wait(timeout=N)`)
181
+
182
+ **チームでの安全な共有パターン**: ワーカー関数を `_workers.py` に分離する規則を設けると、pickle 可能性と単体テスト可能性が高まる。
183
+ **ツール追加の必要性**: なし(ruff には concurrent.futures 固有の追加ルールはない)。
184
+
185
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
186
+
187
+ CLAUDE.md ポリシーとの整合性を確認する。
188
+
189
+ **ポリシー達成度**: 高
190
+ **「初心者でも安全な API」達成度**: 高(`with executor:` がリソースリーク防止を保証、HTTP 境界の Pydantic バリデーションで DoS 制限)
191
+ **設計上の負債・ドキュメント不足**: `asyncio` と concurrent.futures の使い分けガイドが CLAUDE.md に不足。FastAPI アプリでは asyncio が優先される旨を追記する価値がある。
192
+ **Follow-up Issues**: なし(即時対応済み)
193
+
194
+ ---
195
+
196
+ ## Follow-up Issues
197
+
198
+ ### 即時対応済み
199
+
200
+ | 対応内容 | 対応方法 |
201
+ |---|---|
202
+ | `dict[str, int]` を `dict[str, object]` に変更(F-1) | `submit_and_cancel` の戻り値型を修正 |
203
+
204
+ ### 新規 Issue
205
+
206
+ なし(セキュリティ診断なし、全問題は即時解決済み)
207
+
208
+ ---
209
+
210
+ ## まとめ
211
+
212
+ concurrent.futures の主要パターン(ThreadPoolExecutor / ProcessPoolExecutor・submit / map / as_completed / wait・タイムアウト・キャンセル・エラーハンドリング)を 14 エンドポイント・51 テストで検証した。FT191 固有の発見は 1 点: `dict[str, int]` → `dict[str, object]` の invariant 問題(mypy --strict 即時検出)。
213
+
214
+ threading(FT188)・multiprocessing(FT190)の高レベル API として concurrent.futures は使いやすく、処理系(スレッド/プロセス)の交換コストが低い。FastAPI + asyncio 環境では補助的な位置付けになるが、CPU バウンド処理のオフロードには有効。
@@ -223,6 +223,8 @@
223
223
  | [FT187](2026-05-field-trial-187.md) | collections モジュール — Counter・defaultdict・deque・ChainMap・NamedTuple・OrderedDict | | |
224
224
  | [FT188](2026-05-field-trial-188.md) | threading モジュール — Thread・Lock・RLock・Semaphore・Event・ThreadPoolExecutor・Queue・Timer | 🔍 | |
225
225
  | [FT189](2026-05-field-trial-189.md) | subprocess モジュール — 安全なプロセス実行・stdin/stdout 制御・ストリーミング | 🔒 | [#524](https://github.com/hideyukiMORI/nene2-python/issues/524) |
226
+ | [FT190](2026-05-field-trial-190.md) | multiprocessing モジュール — プロセスベース並行処理・共有状態・プロセスプール | | |
227
+ | [FT191](2026-05-field-trial-191.md) | concurrent.futures モジュール — ThreadPoolExecutor / ProcessPoolExecutor / Future | | |
226
228
 
227
229
  ---
228
230
 
@@ -238,4 +240,4 @@ FT172, FT176, FT180, FT184, FT188
238
240
 
239
241
  ---
240
242
 
241
- *最終更新: 2026-05-21 (FT189 / v1.8.60)*
243
+ *最終更新: 2026-05-21 (FT191 / v1.8.63)*
@@ -0,0 +1,61 @@
1
+ # How-to: decimal モジュールと Unicode 数字入力
2
+
3
+ ## Python の Decimal は Unicode 全角数字を受け入れる
4
+
5
+ `decimal.Decimal()` は Unicode の全角数字(U+FF10〜U+FF19: 0123456789)を
6
+ そのまま数値として解釈します。HTTP API を通じてユーザーが全角数字を入力した場合、
7
+ **Pydantic の `str` フィールドを通過してしまう**ことがあります。
8
+
9
+ ```python
10
+ from decimal import Decimal
11
+
12
+ Decimal("123") # → Decimal('123') ← 正常に変換される
13
+ Decimal("1.5") # → Decimal('1.5')
14
+ ```
15
+
16
+ ## 問題: 想定外の入力が通過する可能性
17
+
18
+ 金融計算 API で `price: str = Field(...)` としている場合、
19
+ クライアントが `"1000"` を送ると `Decimal("1000")` → `Decimal('1000')` として処理されます。
20
+ これ自体はエラーではありませんが、**入力の正規化が必要な場合**(ログ記録・DB 保存等)は
21
+ Unicode 正規化を行ってから処理してください。
22
+
23
+ ```python
24
+ import unicodedata
25
+ from decimal import Decimal
26
+
27
+ def parse_decimal_safe(value: str) -> Decimal:
28
+ """Unicode 正規化(NFKC)して Decimal に変換する."""
29
+ normalized = unicodedata.normalize("NFKC", value.strip())
30
+ return Decimal(normalized)
31
+ ```
32
+
33
+ `unicodedata.normalize("NFKC", ...)` は全角数字を半角に変換します。
34
+
35
+ ```python
36
+ unicodedata.normalize("NFKC", "123") # → "123"
37
+ unicodedata.normalize("NFKC", "1.5") # → "1.5"
38
+ ```
39
+
40
+ ## Pydantic でのバリデーション
41
+
42
+ Pydantic の `model_validator` を使って入力値の正規化を強制することを推奨します。
43
+
44
+ ```python
45
+ from pydantic import BaseModel, Field, model_validator
46
+
47
+ class PriceRequest(BaseModel):
48
+ price: str = Field(max_length=20, description="価格(半角数字)")
49
+
50
+ @model_validator(mode="before")
51
+ @classmethod
52
+ def normalize_unicode(cls, values: dict) -> dict:
53
+ import unicodedata
54
+ if "price" in values and isinstance(values["price"], str):
55
+ values["price"] = unicodedata.normalize("NFKC", values["price"])
56
+ return values
57
+ ```
58
+
59
+ ## 関連 Issue
60
+
61
+ - [FT176] #500: parse_decimal_safe() の Unicode 全角数字受け入れ挙動を文書化
@@ -0,0 +1,57 @@
1
+ # How-to: メールアドレスのパースと parseaddr() の挙動
2
+
3
+ ## parseaddr() は寛容なパーサー
4
+
5
+ `email.utils.parseaddr()` は RFC 2822 準拠のフォーマット(`"Name <addr@example.com>"` 形式)を解析しますが、
6
+ **不正なアドレスを渡してもエラーを送出せず、空文字列を返します**。
7
+
8
+ ```python
9
+ from email.utils import parseaddr
10
+
11
+ # 正常ケース
12
+ parseaddr("Alice <alice@example.com>") # → ("Alice", "alice@example.com")
13
+ parseaddr("alice@example.com") # → ("", "alice@example.com")
14
+
15
+ # 不正なアドレス — エラーにならず ("", "") を返す
16
+ parseaddr("not-an-email") # → ("", "")
17
+ parseaddr("") # → ("", "")
18
+ parseaddr("bad @ format") # → ("", "")
19
+ ```
20
+
21
+ ## HTTP 境界での検証は別途行うこと
22
+
23
+ `parseaddr()` の戻り値が空かどうかで有効性を確認しても、
24
+ **セキュリティ上の検証としては不十分**です。ユーザーが入力したアドレスは
25
+ Pydantic の `EmailStr` や正規表現で検証した後に `parseaddr()` を使ってください。
26
+
27
+ ```python
28
+ import re
29
+ from email.utils import parseaddr
30
+
31
+ _EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$")
32
+
33
+ def validate_and_parse(raw: str) -> tuple[str, str] | None:
34
+ name, addr = parseaddr(raw)
35
+ if not addr or not _EMAIL_RE.match(addr):
36
+ return None
37
+ return name, addr
38
+ ```
39
+
40
+ ## ヘッダーインジェクション対策
41
+
42
+ `Subject` や `From` ヘッダーに CR/LF (`\r\n`) が含まれると
43
+ **メールヘッダーインジェクション**が発生します。`email.message.EmailMessage` を使えば
44
+ 自動的にエスケープされますが、`smtplib.sendmail()` に生文字列を渡す場合は
45
+ 事前に CR/LF を除去してください。
46
+
47
+ ```python
48
+ import re
49
+ _INJECT_RE = re.compile(r"[\r\n]")
50
+
51
+ def sanitize_header(value: str) -> str:
52
+ return _INJECT_RE.sub("", value)
53
+ ```
54
+
55
+ ## 関連 Issue
56
+
57
+ - [FT182] #511: parseaddr() の寛容な挙動を How-to ドキュメントに記載