nene2-python 1.8.47__tar.gz → 1.8.49__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 (401) hide show
  1. {nene2_python-1.8.47 → nene2_python-1.8.49}/PKG-INFO +1 -1
  2. nene2_python-1.8.49/docs/field-trials/2026-05-field-trial-177.md +416 -0
  3. nene2_python-1.8.49/docs/field-trials/2026-05-field-trial-178.md +242 -0
  4. nene2_python-1.8.49/docs/field-trials/INDEX.md +229 -0
  5. nene2_python-1.8.49/docs/todo/current.md +63 -0
  6. {nene2_python-1.8.47 → nene2_python-1.8.49}/pyproject.toml +2 -1
  7. nene2_python-1.8.47/docs/todo/current.md +0 -52
  8. {nene2_python-1.8.47 → nene2_python-1.8.49}/.env.example +0 -0
  9. {nene2_python-1.8.47 → nene2_python-1.8.49}/.github/workflows/ci.yml +0 -0
  10. {nene2_python-1.8.47 → nene2_python-1.8.49}/.github/workflows/docs.yml +0 -0
  11. {nene2_python-1.8.47 → nene2_python-1.8.49}/.github/workflows/publish.yml +0 -0
  12. {nene2_python-1.8.47 → nene2_python-1.8.49}/.gitignore +0 -0
  13. {nene2_python-1.8.47 → nene2_python-1.8.49}/.vitepress/config.mts +0 -0
  14. {nene2_python-1.8.47 → nene2_python-1.8.49}/.vitepress/theme/custom.css +0 -0
  15. {nene2_python-1.8.47 → nene2_python-1.8.49}/.vitepress/theme/index.ts +0 -0
  16. {nene2_python-1.8.47 → nene2_python-1.8.49}/AGENTS.md +0 -0
  17. {nene2_python-1.8.47 → nene2_python-1.8.49}/CHANGELOG.md +0 -0
  18. {nene2_python-1.8.47 → nene2_python-1.8.49}/CLAUDE.md +0 -0
  19. {nene2_python-1.8.47 → nene2_python-1.8.49}/Dockerfile +0 -0
  20. {nene2_python-1.8.47 → nene2_python-1.8.49}/LICENSE +0 -0
  21. {nene2_python-1.8.47 → nene2_python-1.8.49}/README.md +0 -0
  22. {nene2_python-1.8.47 → nene2_python-1.8.49}/alembic/README +0 -0
  23. {nene2_python-1.8.47 → nene2_python-1.8.49}/alembic/env.py +0 -0
  24. {nene2_python-1.8.47 → nene2_python-1.8.49}/alembic/script.py.mako +0 -0
  25. {nene2_python-1.8.47 → nene2_python-1.8.49}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  26. {nene2_python-1.8.47 → nene2_python-1.8.49}/alembic.ini +0 -0
  27. {nene2_python-1.8.47 → nene2_python-1.8.49}/compose.yaml +0 -0
  28. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0001-toolchain.md +0 -0
  29. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0002-clean-architecture.md +0 -0
  30. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0003-security-first.md +0 -0
  31. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0004-ai-first-design.md +0 -0
  32. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0005-logging.md +0 -0
  33. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0006-rate-limiting.md +0 -0
  34. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0009-mcp-design.md +0 -0
  35. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0010-async-use-case.md +0 -0
  36. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  37. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/de/index.md +0 -0
  38. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/de/tutorials/getting-started.md +0 -0
  39. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/explanation/architecture.md +0 -0
  40. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/explanation/design-philosophy.md +0 -0
  41. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  42. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  43. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-100.md +0 -0
  44. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-101.md +0 -0
  45. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-102.md +0 -0
  46. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-103.md +0 -0
  47. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-104.md +0 -0
  48. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-105.md +0 -0
  49. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-106.md +0 -0
  50. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-107.md +0 -0
  51. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-108.md +0 -0
  52. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-109.md +0 -0
  53. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  54. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-110.md +0 -0
  55. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-111.md +0 -0
  56. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-112.md +0 -0
  57. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-113.md +0 -0
  58. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-114.md +0 -0
  59. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-115.md +0 -0
  60. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-116.md +0 -0
  61. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-117.md +0 -0
  62. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-118.md +0 -0
  63. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-119.md +0 -0
  64. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  65. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-120.md +0 -0
  66. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-121.md +0 -0
  67. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-122.md +0 -0
  68. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-123.md +0 -0
  69. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-124.md +0 -0
  70. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-125.md +0 -0
  71. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-126.md +0 -0
  72. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-127.md +0 -0
  73. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-128.md +0 -0
  74. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-129.md +0 -0
  75. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  76. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-130.md +0 -0
  77. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-131.md +0 -0
  78. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-132.md +0 -0
  79. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-133.md +0 -0
  80. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-134.md +0 -0
  81. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-135.md +0 -0
  82. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-136.md +0 -0
  83. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-137.md +0 -0
  84. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-138.md +0 -0
  85. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-139.md +0 -0
  86. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  87. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-140.md +0 -0
  88. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-141.md +0 -0
  89. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-142.md +0 -0
  90. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-143.md +0 -0
  91. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-144.md +0 -0
  92. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-145.md +0 -0
  93. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-146.md +0 -0
  94. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-147.md +0 -0
  95. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-148.md +0 -0
  96. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-149.md +0 -0
  97. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  98. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-150.md +0 -0
  99. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-151.md +0 -0
  100. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-152.md +0 -0
  101. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-153.md +0 -0
  102. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-154.md +0 -0
  103. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-155.md +0 -0
  104. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-156.md +0 -0
  105. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-157.md +0 -0
  106. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-158.md +0 -0
  107. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-159.md +0 -0
  108. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  109. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-160.md +0 -0
  110. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-161.md +0 -0
  111. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-162.md +0 -0
  112. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-163.md +0 -0
  113. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-164.md +0 -0
  114. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-165.md +0 -0
  115. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-166.md +0 -0
  116. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-167.md +0 -0
  117. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-168.md +0 -0
  118. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-169.md +0 -0
  119. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  120. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-170.md +0 -0
  121. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-171.md +0 -0
  122. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-172.md +0 -0
  123. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-173.md +0 -0
  124. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-174.md +0 -0
  125. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-175.md +0 -0
  126. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-176.md +0 -0
  127. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  128. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  129. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  130. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  131. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  132. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  133. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  134. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  135. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  136. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  137. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  138. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  139. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  140. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  141. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  142. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  143. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  144. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  145. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  146. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  147. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  148. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  149. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  150. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  151. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  152. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  153. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  154. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  155. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  156. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  157. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  158. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  159. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  160. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  161. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  162. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  163. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  164. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  165. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  166. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  167. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  168. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  169. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  170. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  171. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  172. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  173. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  174. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  175. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  176. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  177. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  178. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  179. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  180. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  181. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  182. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  183. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  184. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  185. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  186. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  187. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  188. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  189. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  190. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  191. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  192. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  193. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  194. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  195. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  196. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  197. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  198. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  199. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  200. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  201. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  202. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  203. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  204. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  205. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  206. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  207. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  208. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  209. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  210. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  211. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  212. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  213. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  214. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-97.md +0 -0
  215. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-98.md +0 -0
  216. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/field-trials/2026-05-field-trial-99.md +0 -0
  217. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/fr/index.md +0 -0
  218. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/fr/tutorials/getting-started.md +0 -0
  219. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/add-new-domain.md +0 -0
  220. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/api-versioning.md +0 -0
  221. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/async-use-case.md +0 -0
  222. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/background-tasks.md +0 -0
  223. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/configure-auth.md +0 -0
  224. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/cors.md +0 -0
  225. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/custom-auth-middleware.md +0 -0
  226. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/dependency-injection.md +0 -0
  227. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/domain-events.md +0 -0
  228. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/file-upload.md +0 -0
  229. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/lifespan-and-app-state.md +0 -0
  230. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/middleware-stack.md +0 -0
  231. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/new-project.md +0 -0
  232. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/problem-details.md +0 -0
  233. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/response-patterns.md +0 -0
  234. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/run-tests.md +0 -0
  235. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/soft-delete.md +0 -0
  236. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/sqlalchemy-repository.md +0 -0
  237. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/streaming.md +0 -0
  238. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/structured-logging.md +0 -0
  239. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/validation.md +0 -0
  240. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/how-to/webhook.md +0 -0
  241. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/howto/mcp-setup.md +0 -0
  242. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/index.md +0 -0
  243. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/explanation/architecture.md +0 -0
  244. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/explanation/design-philosophy.md +0 -0
  245. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/how-to/add-new-domain.md +0 -0
  246. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/how-to/configure-auth.md +0 -0
  247. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/how-to/new-project.md +0 -0
  248. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/how-to/run-tests.md +0 -0
  249. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  250. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/howto/mcp-setup.md +0 -0
  251. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/index.md +0 -0
  252. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/reference/api.md +0 -0
  253. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/reference/configuration.md +0 -0
  254. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/reference/framework-modules.md +0 -0
  255. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/tutorials/first-domain.md +0 -0
  256. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/ja/tutorials/getting-started.md +0 -0
  257. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/pt-br/index.md +0 -0
  258. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/pt-br/tutorials/getting-started.md +0 -0
  259. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/reference/api.md +0 -0
  260. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/reference/configuration.md +0 -0
  261. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/reference/framework-modules.md +0 -0
  262. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/roadmap.md +0 -0
  263. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/templates/field-trial-report.md +0 -0
  264. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/tutorials/first-domain.md +0 -0
  265. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/tutorials/getting-started.md +0 -0
  266. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/zh/index.md +0 -0
  267. {nene2_python-1.8.47 → nene2_python-1.8.49}/docs/zh/tutorials/getting-started.md +0 -0
  268. {nene2_python-1.8.47 → nene2_python-1.8.49}/package-lock.json +0 -0
  269. {nene2_python-1.8.47 → nene2_python-1.8.49}/package.json +0 -0
  270. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/__init__.py +0 -0
  271. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/__main__.py +0 -0
  272. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/app.py +0 -0
  273. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/__init__.py +0 -0
  274. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/entity.py +0 -0
  275. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/exceptions.py +0 -0
  276. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/handler.py +0 -0
  277. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/repository.py +0 -0
  278. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/sqlalchemy_repository.py +0 -0
  279. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/comment/use_case.py +0 -0
  280. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/mcp.py +0 -0
  281. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/__init__.py +0 -0
  282. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/async_use_case.py +0 -0
  283. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/entity.py +0 -0
  284. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/exceptions.py +0 -0
  285. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/handler.py +0 -0
  286. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/repository.py +0 -0
  287. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/sqlalchemy_repository.py +0 -0
  288. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/note/use_case.py +0 -0
  289. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/schema.py +0 -0
  290. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/__init__.py +0 -0
  291. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/entity.py +0 -0
  292. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/exceptions.py +0 -0
  293. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/handler.py +0 -0
  294. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/repository.py +0 -0
  295. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/sqlalchemy_repository.py +0 -0
  296. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/example/tag/use_case.py +0 -0
  297. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/__init__.py +0 -0
  298. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/__init__.py +0 -0
  299. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/api_key.py +0 -0
  300. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/bearer_token.py +0 -0
  301. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/deps.py +0 -0
  302. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/exceptions.py +0 -0
  303. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/interfaces.py +0 -0
  304. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/auth/local_verifier.py +0 -0
  305. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/cache/__init__.py +0 -0
  306. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/cache/ttl.py +0 -0
  307. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/config/__init__.py +0 -0
  308. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/config/settings.py +0 -0
  309. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/__init__.py +0 -0
  310. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/exceptions.py +0 -0
  311. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/health.py +0 -0
  312. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/interfaces.py +0 -0
  313. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/sqlalchemy_executor.py +0 -0
  314. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/database/utils.py +0 -0
  315. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/http/__init__.py +0 -0
  316. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/http/etag.py +0 -0
  317. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/http/health.py +0 -0
  318. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/http/pagination.py +0 -0
  319. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/http/problem_details.py +0 -0
  320. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/log/__init__.py +0 -0
  321. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/log/setup.py +0 -0
  322. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/mcp/__init__.py +0 -0
  323. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/mcp/http_client.py +0 -0
  324. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/mcp/server.py +0 -0
  325. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/__init__.py +0 -0
  326. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/domain_exception.py +0 -0
  327. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/error_handler.py +0 -0
  328. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/request_id.py +0 -0
  329. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/request_logging.py +0 -0
  330. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/request_size_limit.py +0 -0
  331. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/security_headers.py +0 -0
  332. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/setup.py +0 -0
  333. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/middleware/throttle.py +0 -0
  334. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/py.typed +0 -0
  335. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/security/__init__.py +0 -0
  336. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/security/webhook.py +0 -0
  337. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/use_case/__init__.py +0 -0
  338. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/use_case/protocols.py +0 -0
  339. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/validation/__init__.py +0 -0
  340. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/nene2/validation/exceptions.py +0 -0
  341. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/scripts/__init__.py +0 -0
  342. {nene2_python-1.8.47 → nene2_python-1.8.49}/src/scripts/export_openapi.py +0 -0
  343. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/__init__.py +0 -0
  344. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/conftest.py +0 -0
  345. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/__init__.py +0 -0
  346. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/comment/__init__.py +0 -0
  347. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/comment/test_comment_http.py +0 -0
  348. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/comment/test_comment_repository.py +0 -0
  349. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/comment/test_comment_use_case.py +0 -0
  350. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/conftest.py +0 -0
  351. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/note/__init__.py +0 -0
  352. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/note/test_async_note_use_case.py +0 -0
  353. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/note/test_list_notes.py +0 -0
  354. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/note/test_note_repository.py +0 -0
  355. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/tag/__init__.py +0 -0
  356. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/tag/test_tag_repository.py +0 -0
  357. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/tag/test_tags.py +0 -0
  358. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/test_cors.py +0 -0
  359. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/example/test_mcp.py +0 -0
  360. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/__init__.py +0 -0
  361. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/auth/__init__.py +0 -0
  362. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/auth/test_api_key.py +0 -0
  363. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/auth/test_bearer_token.py +0 -0
  364. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/auth/test_make_require_auth.py +0 -0
  365. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/auth/test_token_issuer.py +0 -0
  366. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/cache/__init__.py +0 -0
  367. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/cache/test_ttl.py +0 -0
  368. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/config/__init__.py +0 -0
  369. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/config/test_settings.py +0 -0
  370. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/database/__init__.py +0 -0
  371. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/database/test_transaction.py +0 -0
  372. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/database/test_utils.py +0 -0
  373. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/http/__init__.py +0 -0
  374. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/http/test_etag.py +0 -0
  375. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/http/test_health.py +0 -0
  376. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/http/test_pagination.py +0 -0
  377. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/http/test_problem_details.py +0 -0
  378. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/log/__init__.py +0 -0
  379. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/log/test_setup.py +0 -0
  380. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/mcp/__init__.py +0 -0
  381. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/mcp/test_http_client.py +0 -0
  382. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/mcp/test_server.py +0 -0
  383. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/__init__.py +0 -0
  384. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_error_handler.py +0 -0
  385. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_request_id.py +0 -0
  386. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_request_logging.py +0 -0
  387. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  388. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_security_headers.py +0 -0
  389. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  390. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  391. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/middleware/test_throttle.py +0 -0
  392. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/security/__init__.py +0 -0
  393. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/security/test_webhook.py +0 -0
  394. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/use_case/__init__.py +0 -0
  395. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/use_case/test_protocols.py +0 -0
  396. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  397. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/validation/__init__.py +0 -0
  398. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/nene2/validation/test_exceptions.py +0 -0
  399. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/scripts/__init__.py +0 -0
  400. {nene2_python-1.8.47 → nene2_python-1.8.49}/tests/scripts/test_export_openapi.py +0 -0
  401. {nene2_python-1.8.47 → nene2_python-1.8.49}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.47
3
+ Version: 1.8.49
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,416 @@
1
+ # FT177: hashlib モジュール
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: 安全なハッシュ・パスワード保護・ファイル整合性検証(PBKDF2 / scrypt / Blake2)
5
+ **セキュリティ診断**: **あり**(177 % 3 = 0)
6
+
7
+ ---
8
+
9
+ ## 概要
10
+
11
+ Python 標準の `hashlib` モジュールを nene2-python 上で検証する。
12
+ 単なる SHA-256 だけでなく、パスワード保護 (PBKDF2・scrypt)、MAC 生成 (Blake2 キー付き)、
13
+ ファイル整合性検証(チャンク読み込み)まで網羅し、暗号的に安全な実装パターンを確立する。
14
+ FT177 は 3 の倍数のためセキュリティ診断も実施する。
15
+
16
+ ---
17
+
18
+ ## 実装したサンプルアプリ
19
+
20
+ **場所**: `/home/xi/docker/nene2-python-FT/ft177-hashlib/`
21
+
22
+ ### 主要機能
23
+
24
+ | 関数/定数 | 概要 |
25
+ |---|---|
26
+ | `SECURE_ALGORITHMS` | 許可リスト (`sha256`, `sha512`, `sha3_256`, `sha3_512`, `blake2b`, `blake2s`) |
27
+ | `INSECURE_ALGORITHMS` | 拒否リスト (`md5`, `sha1`, `sha224`) |
28
+ | `hash_text(text, algorithm)` | テキストをハッシュ化(非推奨アルゴリズムは `None` 返却) |
29
+ | `hash_bytes(data, algorithm)` | バイト列をハッシュ化 |
30
+ | `hash_streaming(chunks, algorithm)` | イテレータからチャンク単位でハッシュ化 |
31
+ | `available_algorithms()` | 安全/非推奨/その他に分類した一覧 |
32
+ | `generate_salt()` | `secrets.token_hex(32)` — 256-bit 暗号論的乱数 salt |
33
+ | `hash_password(password, salt, iterations)` | PBKDF2-HMAC-SHA256(最低 200,000 イテレーション) |
34
+ | `verify_password(password, stored_hash, salt)` | `hmac.compare_digest` によるタイミングセーフ検証 |
35
+ | `scrypt_hash(password, salt)` | scrypt (N=16384, r=8, p=1) — メモリハード KDF |
36
+ | `blake2_keyed_hash(data, key)` | Blake2b キー付きハッシュで MAC 生成 |
37
+ | `verify_blake2_mac(data, key, expected_mac)` | Blake2b MAC のタイミングセーフ検証 |
38
+ | `hash_file_content(content, algorithm)` | 64 KB チャンク処理でファイルをハッシュ化 |
39
+ | `verify_file_integrity(content, expected_hash)` | ファイル整合性のタイミングセーフ検証 |
40
+ | `generate_token(length)` | SHA-256 ベースのセキュアトークン生成 |
41
+ | `derive_key_for_purpose(master_key, purpose)` | 用途別鍵導出(PBKDF2 で purpose を salt に使用) |
42
+
43
+ ### HTTP エンドポイント
44
+
45
+ | メソッド | パス | 概要 |
46
+ |---|---|---|
47
+ | GET | `/algorithms` | 安全/非推奨アルゴリズム一覧 |
48
+ | POST | `/hash` | テキストをハッシュ化 |
49
+ | POST | `/password/hash` | PBKDF2 パスワードハッシュ(salt 自動生成可) |
50
+ | POST | `/password/verify` | パスワード検証(タイミングセーフ) |
51
+ | POST | `/password/scrypt` | scrypt パスワードハッシュ |
52
+ | POST | `/mac/blake2` | Blake2b MAC 生成 |
53
+ | POST | `/mac/verify` | Blake2b MAC 検証(タイミングセーフ) |
54
+ | POST | `/file/verify` | ファイル整合性検証 |
55
+
56
+ ---
57
+
58
+ ## テスト結果
59
+
60
+ **62 passed**
61
+
62
+ ```
63
+ 62 passed in 2.04s
64
+ ```
65
+
66
+ mypy: Success(3 ファイル、エラーゼロ)
67
+ ruff check: All checks passed
68
+ ruff format: 3 files reformatted → 再チェックで already formatted
69
+
70
+ ---
71
+
72
+ ## 摩擦ポイント
73
+
74
+ ### F-1: `@app.post` デコレータが `create_app()` 返却インスタンスに適用されない(深刻度: 中)
75
+
76
+ **事象**: app.py でモジュールレベルに `app = create_app()` を作成し `@app.post(...)` でルートを定義した。
77
+ `TestClient(create_app())` が新しい FastAPI インスタンスを返すためルートが空になり、全エンドポイントで 404 が返る。
78
+
79
+ **原因**: デコレータは定義時の `app` オブジェクトにルートを登録する。
80
+ `create_app()` は呼ぶたびに新規インスタンスを返すので、モジュールレベルの `app` と別インスタンスになる。
81
+
82
+ **対応**: `APIRouter` を使用し、`create_app()` の中で `application.include_router(router)` する。
83
+ FT170 以降で繰り返し発生しているため、init-ft.sh のテンプレートまたは CLAUDE.md に「FastAPI アプリファクトリは `APIRouter` パターンを使う」旨を追記する。
84
+
85
+ ---
86
+
87
+ ## 観察点
88
+
89
+ ### 観察1: アルゴリズム許可リスト — 拒否側ではなく許可側で制御
90
+
91
+ ```python
92
+ SECURE_ALGORITHMS = frozenset({"sha256", "sha512", "sha3_256", "sha3_512", "blake2b", "blake2s"})
93
+
94
+ def hash_text(text: str, algorithm: str = "sha256") -> str | None:
95
+ if algorithm not in SECURE_ALGORITHMS:
96
+ return None
97
+ ...
98
+ ```
99
+
100
+ `if algorithm in INSECURE_ALGORITHMS: return None` ではなく許可リストで制御することで、
101
+ 未知の新アルゴリズムが追加されても自動的に拒否される。セキュリティ設計の基本原則。
102
+
103
+ ### 観察2: PBKDF2 最低イテレーション数の強制
104
+
105
+ ```python
106
+ MIN_PBKDF2_ITERATIONS = 200_000 # NIST SP 800-132 推奨最低値
107
+
108
+ def hash_password(password: str, salt: str, iterations: int = MIN_PBKDF2_ITERATIONS) -> str:
109
+ if iterations < MIN_PBKDF2_ITERATIONS:
110
+ iterations = MIN_PBKDF2_ITERATIONS
111
+ ...
112
+ ```
113
+
114
+ API 側で `ge=200_000` の Pydantic バリデーションも設けているが、`demos.py` 側でも下限を強制する二重防御。
115
+ ドメイン層がセキュリティ制約を持つことでHTTP 以外から呼ばれても安全を保てる。
116
+
117
+ ### 観察3: `hmac.compare_digest` の徹底
118
+
119
+ パスワード検証・MAC 検証・ファイル整合性検証の全 3 か所で `==` 比較ではなく
120
+ `hmac.compare_digest` を使用。タイミングサイドチャネル攻撃(timing attack)への対策。
121
+
122
+ ### 観察4: scrypt vs PBKDF2 の使い分け
123
+
124
+ scrypt はメモリハード(N=16384 では約 16 MB のメモリを消費)のため、
125
+ GPU/ASIC による総当たり攻撃に対して PBKDF2 より強い。
126
+ ただしサーバーリソースも多く消費するため、高負荷環境では PBKDF2 が現実的。
127
+ テストで両者が異なる出力を生成することを確認済み。
128
+
129
+ ---
130
+
131
+ ## nene2-python フレームワークとの統合
132
+
133
+ - `APIRouter` + `create_app()` パターンは nene2-python の標準的な設計に沿う(F-1 の解決策)
134
+ - `hash_password` / `verify_password` は UseCase 層に組み込める純粋関数として設計
135
+ - Pydantic の `max_length` 制限で DoS 防止(`password` は 1,000文字、`text` は 10,000文字)
136
+ - HTTP 境界での入力バリデーション(`ge=200_000` など)とドメイン層の二重防御がポリシー準拠
137
+
138
+ ---
139
+
140
+ ## Developer Experience (DX) Review
141
+
142
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
143
+
144
+ 認証機能を初めて実装しようとしており、「パスワードをハッシュ化する」という要件がある。
145
+
146
+ **ドキュメント理解**: `hash_password()` 関数の存在を見つけられれば使えるが、
147
+ 「なぜ `iterations=200_000` が必要か」「salt とは何か」の説明がコード内コメントにないため、
148
+ コピペして使うとイテレーション数を削って「速くした」事故が起きやすい。
149
+ **事故リスク**: 中。`hash_password("secret", generate_salt(), iterations=1000)` は動作するが危険。
150
+ ドキュメントなしでは最低イテレーション数の意味が分からない。
151
+ **規約の使いやすさ**: `generate_salt()` → `hash_password()` → `verify_password()` の3点セットが分かりやすい。
152
+ 型注釈があるので補完で辿れる。
153
+
154
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
155
+
156
+ 既存の `hashlib.md5(password.encode()).hexdigest()` を使ったコードを見て「これでいいか」と思っている。
157
+
158
+ **コピペ可能性**: `hash_text("password", "md5")` が `None` を返すことで明示的に失敗する設計は良い。
159
+ ただし `None` チェックを怠ると `None.hex()` で AttributeError になり、デバッグが難しい場面もある。
160
+ **拡張時の罠**: `SECURE_ALGORITHMS` に直接 `"md5"` を追加してしまうと全防御が崩れる。
161
+ 定数を immutable (`frozenset`) にしているのは良い。
162
+ **セキュリティ的な事故リスク**: 中。パスワードに `hash_text()` を使うと salt なし → 同じパスワードが同じハッシュになるという致命的なミスが可能。`hash_password()` を使わせるガイドが必要。
163
+
164
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
165
+
166
+ パスワード登録・ログイン API を TypeScript クライアントから叩く立場。
167
+
168
+ **エラーレスポンスの質**: `algorithm: "md5"` を送ると `400 Bad Request` が返り、
169
+ `"Insecure or unsupported algorithm: md5"` というメッセージが返る。クライアント実装には十分。
170
+ **Python 固有概念の学習コスト**: `bytes.fromhex(salt)` や `derived.hex()` はバイト⇔文字列変換の理解が必要。
171
+ TypeScript の `Buffer.from(hex, 'hex')` に相当するが、知らないと戸惑う。
172
+ **事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実しており、不正入力は400で弾かれる。
173
+
174
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
175
+
176
+ Django の `make_password()` / `check_password()` を使い慣れており、代替実装を評価する。
177
+
178
+ **他フレームワークとの差異**: Django は `pbkdf2_sha256$N$salt$hash` 形式で保存するが、
179
+ このFTでは hash と salt を別フィールドで管理するシンプルな形式。
180
+ 移行時にはシリアライズ形式を揃える必要がある。
181
+ **nene2-python の薄さへの評価**: `hash_password` / `verify_password` が純粋関数なのは好評。
182
+ UseCase に組み込んでもHTTPの知識が不要なのは Django の `auth` モジュール依存より明快。
183
+ **本番投入可能性**: scrypt の `N=2**14` は WSL2 環境で問題ないが本番では `N=2**16` 以上を推奨。
184
+ テストが遅くなるのでテスト専用の低 N 設定が必要になる場面がある。
185
+
186
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
187
+
188
+ チームでこのコードをレビューする際のリスク評価。
189
+
190
+ **コードレビューチェックポイント**:
191
+ - [x] `hash_text()` をパスワードハッシュに使っていないか(salt なし → レインボーテーブル攻撃可能)
192
+ - [x] `iterations` パラメータが 200,000 未満にオーバーライドされていないか
193
+ - [x] `hmac.compare_digest` ではなく `==` でハッシュ比較していないか(timing attack)
194
+ - [x] salt が `generate_salt()` 以外(固定文字列など)で生成されていないか
195
+
196
+ **チームでの安全な共有パターン**: `hash_password` / `verify_password` の組み合わせで使うことを
197
+ ADR またはドキュメントで明文化すると良い。`hash_text` はファイル整合性など非パスワード用途専用と明示。
198
+ **ツール追加の必要性**: `ruff S303`(`hashlib.md5`/`sha1` を使用している場合の警告)が有効。
199
+ ただし `SECURE_ALGORITHMS` チェックがあるため検出は二重になる。
200
+
201
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
202
+
203
+ CLAUDE.md との整合性確認。
204
+
205
+ **ポリシー達成度**: 高
206
+ **「初心者でも安全な API」達成度**: 中
207
+ — `hash_text()` がパスワードハッシュに誤用される余地がある点でやや低い。
208
+ 関数名を `hash_content()` に変え、パスワード用途は明示的に `hash_password()` のみにする設計も検討できる。
209
+ **設計上の負債・ドキュメント不足**:
210
+ - F-1 (APIRouter パターン) は CLAUDE.md または init-ft.sh テンプレートに追記が必要
211
+ - scrypt の N パラメータが環境依存なのでテスト時と本番時の設定分離ガイドが欲しい
212
+ **Follow-up Issue 候補**: F-1 の APIRouter パターン文書化 → Issue #501
213
+
214
+ ---
215
+
216
+ ## セキュリティ診断
217
+
218
+ > **診断方針**: 今回は hashlib 自体がセキュリティモジュールのため、
219
+ > 「hashlib の誤用パターン」が主な検査対象となる。
220
+
221
+ ### 1. OWASP API Security Top 10 (2023)
222
+
223
+ #### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
224
+ - 認証・認可が絡むエンドポイントなし(ハッシュ計算と検証のみ)
225
+ - **結果**: ✅ 該当なし
226
+
227
+ #### API2: 認証の破損 (Broken Authentication)
228
+ - 認証ミドルウェアなし(FT なので最小構成)
229
+ - パスワード検証ロジック自体は `hmac.compare_digest` でタイミングセーフ
230
+ - **結果**: ✅ 設計上の問題なし
231
+
232
+ #### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
233
+ - Pydantic の `BaseModel` はデフォルトで定義外フィールドを無視
234
+ - `{"password": "x", "is_admin": true}` を POST → `is_admin` は無視される
235
+ - **結果**: ✅ 問題なし
236
+
237
+ #### API4: 無制限リソース消費 (Unrestricted Resource Consumption)
238
+ - `password`: `max_length=1_000` / `text`: `max_length=10_000` / `content_hex`: `max_length=20_971_520`
239
+ - `demos.py` 側でも `MAX_INPUT_BYTES = 10 * 1024 * 1024` チェック
240
+ - scrypt は計算コストが高い(`N=16384` で約 50ms)— Pydantic 入力バリデーションでペイロードサイズは制限済み
241
+ - **結果**: ✅ DoS ベクター対策済み
242
+
243
+ #### API5〜API10
244
+ - SSRF: 外部 URL を受け取るエンドポイントなし ✅
245
+ - セキュリティヘッダー: FT 最小構成のため nene2 ミドルウェアなし(本番投入時に追加必須)
246
+ - デバッグエンドポイント: `/docs` は残存(FT 環境は許容)
247
+ - **結果**: ✅ FT スコープ内では問題なし
248
+
249
+ ---
250
+
251
+ ### 2. インジェクション攻撃
252
+
253
+ #### SQL インジェクション
254
+ - DB アクセスなし
255
+ - **結果**: ✅ 該当なし
256
+
257
+ #### コマンドインジェクション
258
+ - `subprocess` / `os.system` 使用なし(ruff S602/S605 確認済み)
259
+ - **結果**: ✅
260
+
261
+ #### パストラバーサル
262
+ - ファイルパス操作なし(`content` は bytes として受け取る)
263
+ - **結果**: ✅ 該当なし
264
+
265
+ #### SSTI / HTTP ヘッダーインジェクション
266
+ - テンプレートエンジン使用なし / レスポンスヘッダーへのユーザー入力反映なし
267
+ - **結果**: ✅
268
+
269
+ ---
270
+
271
+ ### 3. 認証・認可
272
+
273
+ - **パスワードハッシュ**: PBKDF2-HMAC-SHA256 (200,000 iterations) / scrypt (N=16384) 実装済み ✅
274
+ - **salt 生成**: `secrets.token_hex(32)` — 256-bit 暗号論的乱数 ✅
275
+ - **MD5・SHA-1 拒否**: `SECURE_ALGORITHMS` 許可リストで完全拒否 ✅
276
+ - **タイミング攻撃**: `hmac.compare_digest` を全検証箇所で使用 ✅
277
+ - **JWT**: 使用なし(PYSEC-2025-183 の影響なし)✅
278
+
279
+ ---
280
+
281
+ ### 4. 入力バリデーション
282
+
283
+ 実際の攻撃ペイロードを TestClient で送信:
284
+
285
+ ```python
286
+ # 巨大入力
287
+ POST /hash {"text": "A" * 100_001}
288
+ # → 422 Unprocessable Entity (max_length=10_000)
289
+
290
+ # 最低イテレーション未満
291
+ POST /password/hash {"password": "x", "iterations": 1000}
292
+ # → 422 (ge=200_000)
293
+
294
+ # 無効な hex
295
+ POST /file/verify {"content_hex": "ZZZZ", "expected_hash": "..."}
296
+ # → 400 "Invalid hex content"
297
+
298
+ # 非 ASCII algorithm
299
+ POST /hash {"text": "hello", "algorithm": "SHA-256"}
300
+ # → 400 "Insecure or unsupported algorithm: SHA-256"
301
+ ```
302
+
303
+ - **結果**: ✅ 全ペイロードで適切なエラーが返る
304
+
305
+ ---
306
+
307
+ ### 5. 情報漏洩
308
+
309
+ - hashlib の内部エラーは `except ValueError` でキャッチして `400` に変換
310
+ - スタックトレースは FastAPI のデフォルト動作で非公開(`APP_DEBUG` 未設定)
311
+ - `pip-audit` 結果: `PYSEC-2025-183` (PyJWT 2.12.1 / mcp 経由の推移的依存)
312
+ — 直接使用なし、Fix バージョン未提供のため**許容**(FT174 以降と同じ判断)
313
+ - **結果**: ⚠️ PYSEC-2025-183 は継続監視(修正版リリース待ち)
314
+
315
+ ---
316
+
317
+ ### 6. Python / FastAPI 固有の攻撃ベクター
318
+
319
+ #### ReDoS
320
+ - 正規表現使用なし
321
+ - **結果**: ✅ 該当なし
322
+
323
+ #### pickle / yaml インジェクション
324
+ - `pickle` / `yaml.load` 使用なし
325
+ - **結果**: ✅
326
+
327
+ #### 非同期レースコンディション
328
+ - グローバルな mutable 状態なし(全関数が純粋関数)
329
+ - **結果**: ✅
330
+
331
+ #### 型強制攻撃 (Pydantic)
332
+
333
+ ```python
334
+ POST /password/hash {"password": "x", "iterations": "2e5"}
335
+ # → 200 OK (iterations = 200000 として型変換)
336
+
337
+ POST /password/hash {"password": "x", "iterations": 1.5}
338
+ # → 422 (int フィールドに float は不正)
339
+ ```
340
+
341
+ `"2e5"` が 200000 に変換されるのは Pydantic v2 の仕様(許容範囲内、200,000 >= `ge=200_000`)。
342
+ **結果**: ✅ セキュリティ上の問題なし(`ge` 制約が通過するため)
343
+
344
+ #### hashlib 特有: アルゴリズム名インジェクション
345
+
346
+ ```python
347
+ POST /hash {"text": "hello", "algorithm": "sha256;DROP TABLE--"}
348
+ # → 400 "Insecure or unsupported algorithm: sha256;DROP TABLE--"
349
+ # SECURE_ALGORITHMS の frozenset チェックで即拒否
350
+ ```
351
+
352
+ - **結果**: ✅ 許可リスト方式で任意文字列を `hashlib.new()` に渡さない
353
+
354
+ ---
355
+
356
+ ### 7. 依存関係の脆弱性スキャン
357
+
358
+ ```
359
+ Found 1 known vulnerability in 1 package
360
+ Name Version ID Fix Versions
361
+ ----- ------- -------------- ------------
362
+ pyjwt 2.12.1 PYSEC-2025-183
363
+ ```
364
+
365
+ - **スキャン結果**: CRITICAL: 0件 / HIGH: 0件 / MEDIUM: 0件 / LOW: 1件(PyJWT 経由)
366
+ - **対応方針**: 直接使用なし・mcp の推移的依存・Fix バージョン未提供のため許容
367
+
368
+ ---
369
+
370
+ ### 診断サマリー
371
+
372
+ | カテゴリ | 結果 | 最重要発見 |
373
+ |---|---|---|
374
+ | OWASP API Security Top 10 | ✅ 全通過 | - |
375
+ | SQL インジェクション | ✅ 該当なし | - |
376
+ | コマンドインジェクション | ✅ | - |
377
+ | パストラバーサル | ✅ 該当なし | - |
378
+ | SSTI | ✅ 該当なし | - |
379
+ | 認証・認可 | ✅ | PBKDF2+scrypt+Blake2 全対応 |
380
+ | 入力バリデーション | ✅ | 全境界で max_length/ge/le 設定済み |
381
+ | 情報漏洩 | ⚠️ | PYSEC-2025-183(継続監視) |
382
+ | ReDoS | ✅ 該当なし | - |
383
+ | pickle / yaml | ✅ | - |
384
+ | 非同期レースコンディション | ✅ | - |
385
+ | 型強制攻撃 | ✅ | `"2e5"` 変換は仕様範囲内 |
386
+ | アルゴリズム名インジェクション | ✅ | 許可リスト方式で完全防御 |
387
+ | 依存関係 CVE | ⚠️ 1件 | PYSEC-2025-183(PyJWT/mcp 経由) |
388
+
389
+ **総合評価**: 条件付き合格(PYSEC-2025-183 を継続監視)
390
+ **発見した脆弱性**: 0件(CRITICAL: 0 / HIGH: 0 / MEDIUM: 0 / LOW: 0 ※CVE は推移的依存)
391
+ **新規セキュリティ Issue**: #501(APIRouter パターン文書化)
392
+
393
+ ---
394
+
395
+ ## Follow-up Issues
396
+
397
+ | 優先度 | タイトル | 種別 |
398
+ |---|---|---|
399
+ | 中 | F-1: FastAPI アプリファクトリで APIRouter パターンを CLAUDE.md または init-ft.sh に記載 | docs |
400
+ | 低 | scrypt の N パラメータをテスト/本番で分ける設定ガイドを追加 | docs |
401
+
402
+ ---
403
+
404
+ ## まとめ
405
+
406
+ FT177 では `hashlib` を中心に、PBKDF2・scrypt・Blake2 キー付きハッシュという3種のセキュリティプリミティブを実装した。
407
+ 62 テストが全通過し、mypy/ruff も問題なし。
408
+
409
+ セキュリティ診断では「hashlib そのものがセキュリティモジュール」という特性から
410
+ インジェクション系のリスクはほぼ皆無だったが、**アルゴリズム名インジェクション**に対する
411
+ 許可リスト方式の有効性を実証でき、今後の設計パターンとして参照できる。
412
+
413
+ 主要摩擦点は F-1 の `@app.post` と `create_app()` の分離問題(毎 FT で発生)で、
414
+ `APIRouter` パターンへの移行を次 FT 以降の標準とする。
415
+
416
+ v1.8.48 としてリリース。
@@ -0,0 +1,242 @@
1
+ # FT178: base64 モジュール
2
+
3
+ **日付**: 2026-05-21
4
+ **テーマ**: エンコード・デコード・URL セーフ変換・データ URI・HTTP Basic Auth パース
5
+ **セキュリティ診断**: なし(178 % 3 = 1)
6
+
7
+ ---
8
+
9
+ ## 概要
10
+
11
+ Web API で頻繁に使う `base64` モジュールを検証する。
12
+ 標準エンコーディングと URL セーフ変換の違い、パディング扱い、データ URI 生成、
13
+ HTTP Basic Auth ヘッダーのパースまで網羅し、落とし穴となる箇所を洗い出す。
14
+
15
+ ---
16
+
17
+ ## 実装したサンプルアプリ
18
+
19
+ **場所**: `/home/xi/docker/nene2-python-FT/ft178-base64/`
20
+
21
+ ### 主要機能
22
+
23
+ | 関数 | 概要 |
24
+ |---|---|
25
+ | `encode_standard(data)` | 標準 base64 エンコード(RFC 4648 §4) |
26
+ | `decode_standard(s)` | 標準 base64 デコード(`validate=True` で厳格検証) |
27
+ | `encode_url_safe(data)` | URL セーフ base64(パディングなし) |
28
+ | `decode_url_safe(s)` | URL セーフ base64 デコード(パディング自動補完 + 文字セット検証) |
29
+ | `encode_text(text)` | UTF-8 テキスト → base64 |
30
+ | `decode_text(s)` | base64 → UTF-8 テキスト(非 UTF-8 は `None`) |
31
+ | `is_valid_base64(s)` | 長さ・文字セット・パディングを検証 |
32
+ | `is_valid_url_safe_base64(s)` | URL セーフ文字セット検証 |
33
+ | `make_data_uri(content, mime_type)` | RFC 2397 データ URI 生成 |
34
+ | `parse_data_uri(uri)` | データ URI → `(mime_type, bytes)` |
35
+ | `encode_basic_auth(username, password)` | HTTP Basic Auth ヘッダー値生成 |
36
+ | `parse_basic_auth(header)` | Authorization ヘッダー → `(username, password)` |
37
+
38
+ ### HTTP エンドポイント
39
+
40
+ | メソッド | パス | 概要 |
41
+ |---|---|---|
42
+ | POST | `/encode` | 標準 base64 エンコード |
43
+ | POST | `/decode` | 標準 base64 デコード |
44
+ | POST | `/encode/url-safe` | URL セーフ base64 エンコード |
45
+ | POST | `/decode/url-safe` | URL セーフ base64 デコード |
46
+ | POST | `/encode/text` | テキスト → base64 |
47
+ | POST | `/decode/text` | base64 → テキスト |
48
+ | POST | `/data-uri/encode` | データ URI 生成 |
49
+ | POST | `/data-uri/parse` | データ URI 解析 |
50
+ | POST | `/auth/basic/encode` | Basic Auth ヘッダー生成 |
51
+ | POST | `/auth/basic/parse` | Basic Auth ヘッダー解析 |
52
+
53
+ ---
54
+
55
+ ## テスト結果
56
+
57
+ **58 passed**(初回 1 失敗 → 修正後 58 全通過)
58
+
59
+ ```
60
+ 58 passed in 0.43s
61
+ ```
62
+
63
+ mypy: Success / ruff: All checks passed / pip-audit: PYSEC-2025-183(継続監視)
64
+
65
+ ---
66
+
67
+ ## 摩擦ポイント
68
+
69
+ ### F-1: `urlsafe_b64decode` が不正文字をサイレントに無視する(深刻度: 高)
70
+
71
+ **事象**: `decode_url_safe("!!!invalid!!!")` が `None` を返さず `b"\x8a{\xda\x96'"` を返した。
72
+
73
+ **原因**: `base64.urlsafe_b64decode()` は RFC 4648 の「ignore non-alphabet characters」モードで動作し、
74
+ `!` などの不正文字を黙って無視してデコードを続ける。
75
+ 一方、標準の `b64decode(s, validate=True)` は不正文字で `binascii.Error` を raise する。
76
+
77
+ **対応**: `urlsafe_b64decode` の前に文字セット正規表現で事前バリデーション:
78
+ ```python
79
+ _URL_SAFE_CHARS_RE = re.compile(r"^[A-Za-z0-9_\-=]*$")
80
+
81
+ def decode_url_safe(s: str) -> bytes | None:
82
+ stripped = s.rstrip("=")
83
+ if not stripped or not _URL_SAFE_CHARS_RE.match(stripped):
84
+ return None
85
+ ...
86
+ ```
87
+
88
+ **ライブラリ設計上の問題**: `urlsafe_b64decode` に `validate=True` パラメータが存在しない(`b64decode` にはある)。
89
+ URL セーフ版は自前バリデーションが必要という非対称な API 設計。
90
+
91
+ ---
92
+
93
+ ## 観察点
94
+
95
+ ### 観察1: 標準 base64 vs URL セーフ — `validate=True` の非対称性
96
+
97
+ ```python
98
+ # 標準版: validate パラメータがある
99
+ base64.b64decode("not!!!valid", validate=True) # → binascii.Error
100
+
101
+ # URL セーフ版: validate パラメータがない
102
+ base64.urlsafe_b64decode("not!!!valid") # → サイレントに無視してデコード
103
+ ```
104
+
105
+ JWT トークンや OAuth コードは URL セーフ base64 を使う。
106
+ `validate=True` に相当するガードを自前で実装しないと、
107
+ 不正トークンを誤って「有効」として処理する脆弱性になりうる。
108
+
109
+ ### 観察2: パディング補完の必要性
110
+
111
+ RFC 4648 §5 では URL セーフ base64 のパディング(`=`)は省略可能とされており、
112
+ JWT の `alg`・`typ` フィールドなど実際のトークンはパディングなしで渡ってくる。
113
+
114
+ ```python
115
+ # パディングなし JWT ヘッダーを補完してデコード
116
+ stripped = s.rstrip("=")
117
+ padding = 4 - len(stripped) % 4
118
+ if padding != 4:
119
+ s = stripped + "=" * padding
120
+ ```
121
+
122
+ `padding != 4` の条件で「すでに 4 の倍数の場合はパディングを追加しない」ことも重要。
123
+
124
+ ### 観察3: `partition(":")` で パスワード中のコロン対応
125
+
126
+ ```python
127
+ username, _, password = text.partition(":") # partition は最初の : で分割
128
+ ```
129
+
130
+ `split(":", 1)` でも同じだが `partition` の方が意図が明示的で、
131
+ `a:b:c:d` → `("a", ":", "b:c:d")` の分解が 1 行で書ける。
132
+
133
+ ### 観察4: データ URI の MIME タイプ検証
134
+
135
+ ```python
136
+ re.match(r"^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*$", mime_type)
137
+ ```
138
+
139
+ `image/png`・`application/octet-stream`・`text/plain; charset=utf-8` のような MIME タイプは正規表現で検証。
140
+ `javascript:`・`vbscript:` などの XSS ペイロードをデータ URI に埋め込む試みをブロックできる。
141
+
142
+ ---
143
+
144
+ ## nene2-python フレームワークとの統合
145
+
146
+ - `encode_basic_auth` / `parse_basic_auth` は `ApiKeyAuthMiddleware` の拡張として組み込み可能
147
+ - `make_data_uri` はファイルアップロード API のレスポンス形式として使える
148
+ - Pydantic `max_length` フィールドで全エンドポイントの DoS 対策済み
149
+ - `APIRouter` + `create_app()` パターン(FT177 摩擦 F-1 の対応)を最初から適用
150
+
151
+ ---
152
+
153
+ ## Developer Experience (DX) Review
154
+
155
+ ### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
156
+
157
+ 画像アップロード API でクライアントから base64 エンコードされたデータを受け取る実装をしている。
158
+
159
+ **ドキュメント理解**: `b64encode` / `b64decode` のペアは分かりやすい。
160
+ URL セーフ版との違い(`+/` vs `-_`、パディング省略)はドキュメントに書いてないと気づきにくい。
161
+ **事故リスク**: 中。標準版と URL セーフ版を混在させると復号に失敗し、バイナリ化けとして現れる。
162
+ エラーより「壊れたデータ」として扱われるため気づきにくい。
163
+ **規約の使いやすさ**: `encode_standard(data)` が `str` を返し、`decode_standard(s)` が `bytes | None` を返すのは直感的。
164
+
165
+ ### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
166
+
167
+ JWT パースのコードを既存プロジェクトからコピーしており、URL セーフ base64 を使っている。
168
+
169
+ **コピペ可能性**: `decode_url_safe` の自前バリデーションが必要な点はコメントがないと気づかない。
170
+ `urlsafe_b64decode` を直接使うと F-1 の罠にはまる。
171
+ **拡張時の罠**: パディング補完のコードを「なんか動いてるから削ってもいいかな」と消すと壊れる。
172
+ なぜ必要かのコメントが欲しい。
173
+ **セキュリティ的な事故リスク**: 高。JWT 検証で `urlsafe_b64decode` を `validate=True` なしで使うと、
174
+ 改ざんトークンが「デコード成功」として扱われる可能性がある。
175
+
176
+ ### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
177
+
178
+ TypeScript の `atob()` / `btoa()` に慣れており、Python で同じことをしようとしている。
179
+
180
+ **エラーレスポンスの質**: 400 Bad Request に具体的なメッセージ("Invalid base64 input" 等)が返るのは良い。
181
+ クライアント側でデバッグしやすい。
182
+ **Python 固有概念の学習コスト**: `bytes.hex()` / `bytes.fromhex()` の往復はTS では意識しない変換。
183
+ `atob()` は文字列を返すが `b64decode` は `bytes` を返す差異に戸惑う可能性。
184
+ **事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実。
185
+
186
+ ### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
187
+
188
+ JWT ライブラリの内部実装を理解しており、raw base64 操作をすることもある。
189
+
190
+ **他フレームワークとの差異**: Django の `base64.urlsafe_b64decode` 直接利用パターンと同じ罠(F-1)が
191
+ nene2-python でも起きる。フレームワーク側での救済ではなく実装者が知識として持つ必要がある。
192
+ **nene2-python の薄さへの評価**: base64 は stdlib を薄くラップするだけが適切。
193
+ `decode_url_safe` のバリデーション付きラッパーは価値あるユーティリティ。
194
+ **本番投入可能性**: Basic Auth のパースは `parse_basic_auth` 一本で安全に使える設計が好評価。
195
+
196
+ ### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
197
+
198
+ **コードレビューチェックポイント**:
199
+ - [x] `urlsafe_b64decode` を自前バリデーションなしで使っていないか(F-1 の罠)
200
+ - [x] パスワードが base64 エンコードのみで「保護されている」と勘違いしていないか
201
+ (base64 は暗号化ではなくエンコード)
202
+ - [x] Basic Auth をデコードして得たパスワードをそのまま平文比較していないか
203
+ (`hmac.compare_digest` が必要)
204
+
205
+ **チームでの安全な共有パターン**: `decode_url_safe` に自前バリデーションを含めたラッパーを
206
+ ユーティリティとして共有すると、チーム全員が安全に使える。
207
+ **ツール追加の必要性**: `ruff` に base64 固有のルールはなし。コードレビューで担保。
208
+
209
+ ### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
210
+
211
+ **ポリシー達成度**: 高
212
+ **「初心者でも安全な API」達成度**: 中
213
+ — F-1(`urlsafe_b64decode` の無バリデーション問題)は初心者が直接 `base64.urlsafe_b64decode` を使うと再発する。
214
+ `decode_url_safe` ラッパーを使う運用を周知する必要がある。
215
+ **設計上の負債**: `validate=True` が URL セーフ版に存在しないのは Python stdlib の設計問題。
216
+ ユーザー向けに警告コメントを `decode_url_safe` に追記する価値がある。
217
+ **Follow-up Issue 候補**: なし(現状の実装で十分)
218
+
219
+ ---
220
+
221
+ ## Follow-up Issues
222
+
223
+ 今回の FT では実装上の重大な摩擦はなし(F-1 は実装内で解消済み)。
224
+
225
+ | 優先度 | タイトル | 種別 |
226
+ |---|---|---|
227
+ | 低 | `decode_url_safe` に「`urlsafe_b64decode` は validate=True がないため自前バリデーションが必要」コメントを追記 | docs |
228
+
229
+ ---
230
+
231
+ ## まとめ
232
+
233
+ FT178 では `base64` モジュールを中心に、Web API でよく使うエンコード操作を実装した。
234
+ 58 テストが全通過し、mypy/ruff も問題なし。
235
+
236
+ 最大の発見は F-1: `base64.urlsafe_b64decode` に `validate=True` がなくサイレントに不正入力を処理してしまう問題。
237
+ JWT・OAuth コード等を URL セーフ base64 で扱うコードが多い中、この挙動は高リスクな落とし穴。
238
+ 文字セット正規表現による事前バリデーションで対応済み。
239
+
240
+ APIRouter パターン(FT177 F-1 からの改善)を最初から適用し、テストが一発で通過した。
241
+
242
+ v1.8.49 としてリリース。