nene2-python 1.8.21__tar.gz → 1.8.23__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 (270) hide show
  1. {nene2_python-1.8.21 → nene2_python-1.8.23}/CHANGELOG.md +12 -0
  2. {nene2_python-1.8.21 → nene2_python-1.8.23}/PKG-INFO +1 -1
  3. nene2_python-1.8.23/docs/field-trials/2026-05-field-trial-76.md +152 -0
  4. nene2_python-1.8.23/docs/field-trials/2026-05-field-trial-77.md +179 -0
  5. {nene2_python-1.8.21 → nene2_python-1.8.23}/pyproject.toml +1 -1
  6. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/api_key.py +30 -3
  7. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/bearer_token.py +28 -4
  8. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/use_case/__init__.py +3 -1
  9. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/auth/test_api_key.py +69 -0
  10. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/auth/test_bearer_token.py +81 -0
  11. nene2_python-1.8.23/tests/nene2/use_case/test_run_in_threadpool.py +61 -0
  12. {nene2_python-1.8.21 → nene2_python-1.8.23}/uv.lock +1 -1
  13. {nene2_python-1.8.21 → nene2_python-1.8.23}/.env.example +0 -0
  14. {nene2_python-1.8.21 → nene2_python-1.8.23}/.github/workflows/ci.yml +0 -0
  15. {nene2_python-1.8.21 → nene2_python-1.8.23}/.github/workflows/docs.yml +0 -0
  16. {nene2_python-1.8.21 → nene2_python-1.8.23}/.github/workflows/publish.yml +0 -0
  17. {nene2_python-1.8.21 → nene2_python-1.8.23}/.gitignore +0 -0
  18. {nene2_python-1.8.21 → nene2_python-1.8.23}/.vitepress/config.mts +0 -0
  19. {nene2_python-1.8.21 → nene2_python-1.8.23}/.vitepress/theme/custom.css +0 -0
  20. {nene2_python-1.8.21 → nene2_python-1.8.23}/.vitepress/theme/index.ts +0 -0
  21. {nene2_python-1.8.21 → nene2_python-1.8.23}/AGENTS.md +0 -0
  22. {nene2_python-1.8.21 → nene2_python-1.8.23}/CLAUDE.md +0 -0
  23. {nene2_python-1.8.21 → nene2_python-1.8.23}/Dockerfile +0 -0
  24. {nene2_python-1.8.21 → nene2_python-1.8.23}/LICENSE +0 -0
  25. {nene2_python-1.8.21 → nene2_python-1.8.23}/README.md +0 -0
  26. {nene2_python-1.8.21 → nene2_python-1.8.23}/alembic/README +0 -0
  27. {nene2_python-1.8.21 → nene2_python-1.8.23}/alembic/env.py +0 -0
  28. {nene2_python-1.8.21 → nene2_python-1.8.23}/alembic/script.py.mako +0 -0
  29. {nene2_python-1.8.21 → nene2_python-1.8.23}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  30. {nene2_python-1.8.21 → nene2_python-1.8.23}/alembic.ini +0 -0
  31. {nene2_python-1.8.21 → nene2_python-1.8.23}/compose.yaml +0 -0
  32. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0001-toolchain.md +0 -0
  33. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0002-clean-architecture.md +0 -0
  34. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0003-security-first.md +0 -0
  35. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0004-ai-first-design.md +0 -0
  36. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0005-logging.md +0 -0
  37. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0006-rate-limiting.md +0 -0
  38. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0009-mcp-design.md +0 -0
  39. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0010-async-use-case.md +0 -0
  40. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  41. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/de/index.md +0 -0
  42. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/de/tutorials/getting-started.md +0 -0
  43. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/explanation/architecture.md +0 -0
  44. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/explanation/design-philosophy.md +0 -0
  45. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  46. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  47. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  48. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  49. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  50. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  51. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  52. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  53. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  54. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  55. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  56. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  57. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  58. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  59. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  60. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  61. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  62. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  63. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  64. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  65. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  66. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  67. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  68. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  69. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  70. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  71. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  72. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  73. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  74. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  75. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  76. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  77. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  78. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  79. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  80. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  81. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  82. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  83. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  84. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  85. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  86. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  87. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  88. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  89. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  90. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  91. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  92. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  93. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  94. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  95. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  96. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  97. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  98. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  99. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  100. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  101. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  102. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  103. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  104. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  105. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  106. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  107. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  108. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  109. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  110. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  111. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  112. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  113. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  114. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  115. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  116. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  117. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  118. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  119. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  120. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/fr/index.md +0 -0
  121. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/fr/tutorials/getting-started.md +0 -0
  122. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/add-new-domain.md +0 -0
  123. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/async-use-case.md +0 -0
  124. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/configure-auth.md +0 -0
  125. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/middleware-stack.md +0 -0
  126. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/new-project.md +0 -0
  127. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/problem-details.md +0 -0
  128. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/run-tests.md +0 -0
  129. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/sqlalchemy-repository.md +0 -0
  130. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/how-to/validation.md +0 -0
  131. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/howto/mcp-setup.md +0 -0
  132. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/index.md +0 -0
  133. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/explanation/architecture.md +0 -0
  134. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/explanation/design-philosophy.md +0 -0
  135. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/how-to/add-new-domain.md +0 -0
  136. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/how-to/configure-auth.md +0 -0
  137. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/how-to/new-project.md +0 -0
  138. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/how-to/run-tests.md +0 -0
  139. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  140. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/howto/mcp-setup.md +0 -0
  141. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/index.md +0 -0
  142. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/reference/api.md +0 -0
  143. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/reference/configuration.md +0 -0
  144. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/reference/framework-modules.md +0 -0
  145. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/tutorials/first-domain.md +0 -0
  146. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/ja/tutorials/getting-started.md +0 -0
  147. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/pt-br/index.md +0 -0
  148. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/pt-br/tutorials/getting-started.md +0 -0
  149. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/reference/api.md +0 -0
  150. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/reference/configuration.md +0 -0
  151. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/reference/framework-modules.md +0 -0
  152. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/roadmap.md +0 -0
  153. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/todo/current.md +0 -0
  154. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/tutorials/first-domain.md +0 -0
  155. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/tutorials/getting-started.md +0 -0
  156. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/zh/index.md +0 -0
  157. {nene2_python-1.8.21 → nene2_python-1.8.23}/docs/zh/tutorials/getting-started.md +0 -0
  158. {nene2_python-1.8.21 → nene2_python-1.8.23}/package-lock.json +0 -0
  159. {nene2_python-1.8.21 → nene2_python-1.8.23}/package.json +0 -0
  160. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/__init__.py +0 -0
  161. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/__main__.py +0 -0
  162. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/app.py +0 -0
  163. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/__init__.py +0 -0
  164. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/entity.py +0 -0
  165. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/exceptions.py +0 -0
  166. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/handler.py +0 -0
  167. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/repository.py +0 -0
  168. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/sqlalchemy_repository.py +0 -0
  169. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/comment/use_case.py +0 -0
  170. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/mcp.py +0 -0
  171. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/__init__.py +0 -0
  172. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/async_use_case.py +0 -0
  173. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/entity.py +0 -0
  174. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/exceptions.py +0 -0
  175. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/handler.py +0 -0
  176. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/repository.py +0 -0
  177. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/sqlalchemy_repository.py +0 -0
  178. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/note/use_case.py +0 -0
  179. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/schema.py +0 -0
  180. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/__init__.py +0 -0
  181. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/entity.py +0 -0
  182. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/exceptions.py +0 -0
  183. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/handler.py +0 -0
  184. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/repository.py +0 -0
  185. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/sqlalchemy_repository.py +0 -0
  186. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/example/tag/use_case.py +0 -0
  187. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/__init__.py +0 -0
  188. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/__init__.py +0 -0
  189. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/exceptions.py +0 -0
  190. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/interfaces.py +0 -0
  191. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/auth/local_verifier.py +0 -0
  192. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/config/__init__.py +0 -0
  193. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/config/settings.py +0 -0
  194. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/__init__.py +0 -0
  195. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/exceptions.py +0 -0
  196. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/health.py +0 -0
  197. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/interfaces.py +0 -0
  198. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/sqlalchemy_executor.py +0 -0
  199. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/database/utils.py +0 -0
  200. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/http/__init__.py +0 -0
  201. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/http/health.py +0 -0
  202. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/http/pagination.py +0 -0
  203. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/http/problem_details.py +0 -0
  204. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/log/__init__.py +0 -0
  205. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/log/setup.py +0 -0
  206. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/mcp/__init__.py +0 -0
  207. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/mcp/http_client.py +0 -0
  208. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/mcp/server.py +0 -0
  209. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/__init__.py +0 -0
  210. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/domain_exception.py +0 -0
  211. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/error_handler.py +0 -0
  212. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/request_id.py +0 -0
  213. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/request_logging.py +0 -0
  214. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/request_size_limit.py +0 -0
  215. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/security_headers.py +0 -0
  216. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/setup.py +0 -0
  217. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/middleware/throttle.py +0 -0
  218. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/py.typed +0 -0
  219. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/use_case/protocols.py +0 -0
  220. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/validation/__init__.py +0 -0
  221. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/nene2/validation/exceptions.py +0 -0
  222. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/scripts/__init__.py +0 -0
  223. {nene2_python-1.8.21 → nene2_python-1.8.23}/src/scripts/export_openapi.py +0 -0
  224. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/__init__.py +0 -0
  225. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/__init__.py +0 -0
  226. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/comment/__init__.py +0 -0
  227. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/comment/test_comment_http.py +0 -0
  228. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/comment/test_comment_repository.py +0 -0
  229. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/comment/test_comment_use_case.py +0 -0
  230. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/conftest.py +0 -0
  231. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/note/__init__.py +0 -0
  232. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/note/test_async_note_use_case.py +0 -0
  233. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/note/test_list_notes.py +0 -0
  234. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/note/test_note_repository.py +0 -0
  235. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/tag/__init__.py +0 -0
  236. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/tag/test_tag_repository.py +0 -0
  237. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/tag/test_tags.py +0 -0
  238. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/test_cors.py +0 -0
  239. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/example/test_mcp.py +0 -0
  240. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/__init__.py +0 -0
  241. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/auth/__init__.py +0 -0
  242. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/auth/test_token_issuer.py +0 -0
  243. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/config/__init__.py +0 -0
  244. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/config/test_settings.py +0 -0
  245. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/database/__init__.py +0 -0
  246. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/database/test_transaction.py +0 -0
  247. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/database/test_utils.py +0 -0
  248. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/http/__init__.py +0 -0
  249. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/http/test_health.py +0 -0
  250. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/http/test_pagination.py +0 -0
  251. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/http/test_problem_details.py +0 -0
  252. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/log/__init__.py +0 -0
  253. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/log/test_setup.py +0 -0
  254. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/mcp/__init__.py +0 -0
  255. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/mcp/test_http_client.py +0 -0
  256. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/__init__.py +0 -0
  257. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_error_handler.py +0 -0
  258. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_id.py +0 -0
  259. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_logging.py +0 -0
  260. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  261. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_security_headers.py +0 -0
  262. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  263. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  264. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/middleware/test_throttle.py +0 -0
  265. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/use_case/__init__.py +0 -0
  266. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/use_case/test_protocols.py +0 -0
  267. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/validation/__init__.py +0 -0
  268. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/nene2/validation/test_exceptions.py +0 -0
  269. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/scripts/__init__.py +0 -0
  270. {nene2_python-1.8.21 → nene2_python-1.8.23}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.22] — 2026-05-20
9
+
10
+ FT76 フィールドトライアル — async def + sync DB ブロッキング問題と run_in_threadpool 追加。
11
+
12
+ ### Added
13
+ - `run_in_threadpool` を `nene2.use_case` から re-export (#326) (FT76)
14
+ — Starlette の `run_in_threadpool` を nene2 公開 API として公開し、
15
+ `async def` ハンドラーから同期 DB 処理を安全にスレッドプールへオフロードできる
16
+ - Field trial report: `docs/field-trials/2026-05-field-trial-76.md` (FT76)
17
+
18
+ ---
19
+
8
20
  ## [1.8.21] — 2026-05-20
9
21
 
10
22
  FT75 フィールドトライアル — ミドルウェアスタック順序問題の発見と根本解決。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.21
3
+ Version: 1.8.23
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,152 @@
1
+ # FT76: async ハンドラー + sync SQLAlchemy のイベントループブロッキング
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: FastAPI の async def ハンドラー内で同期 DB 処理を呼ぶとどうなるか
5
+ **バージョン**: v1.8.21
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft76-async-sync-db/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ FastAPI は `async def` と `def` を混在できるが、同期処理の扱いが根本的に異なる。
13
+ nene2 の DB 層(`SqlAlchemyQueryExecutor` / `SqlAlchemyTransactionManager`)は同期実装のため、
14
+ `async def` ハンドラー内から直接呼ぶとイベントループをブロックする。
15
+ このFTでは3パターンの挙動を実測し、nene2 として何を提供すべきかを検証した。
16
+
17
+ ---
18
+
19
+ ## テスト対象の3パターン
20
+
21
+ | パターン | 実装 | 動作 |
22
+ |---|---|---|
23
+ | A | `async def` 内で `time.sleep()` / 同期DB直呼び | **イベントループをブロック** |
24
+ | B | `async def` 内で `run_in_executor()` 経由 | スレッドプールにオフロード(安全) |
25
+ | C | `def` ハンドラー(同期) | FastAPI が自動でスレッドプールに退避(安全) |
26
+
27
+ ---
28
+
29
+ ## 発見した問題
30
+
31
+ ### 問題1: async ハンドラー + sync DB = サイレントブロッキング
32
+
33
+ ```python
34
+ # ❌ よくある間違い
35
+ @app.post("/tasks")
36
+ async def create_task(body: TaskBody) -> JSONResponse:
37
+ result = _create_task_sync(body.title) # time.sleep / Session(...) など
38
+ return JSONResponse(result, status_code=201)
39
+ ```
40
+
41
+ `async def` 内で `time.sleep()` や同期 `Session` を呼ぶと、実行中は asyncio のイベントループが
42
+ 完全に停止する。他のリクエストはそのリクエストが完了するまで待たされる。
43
+
44
+ **特に危険なのは「少量のデータなら問題が出ない」点**。
45
+ 開発中 / ステージング環境では顕在化せず、本番で同時アクセスが増えてから遅延が爆発する。
46
+
47
+ ### 問題2: nene2 に async DB 層がない
48
+
49
+ ```python
50
+ # v1.8.21 時点: 存在しない
51
+ from nene2.database import AsyncSqlAlchemyQueryExecutor # ImportError
52
+ ```
53
+
54
+ `SqlAlchemyQueryExecutor.fetch_all()` / `SqlAlchemyTransactionManager.begin()` は
55
+ すべて同期実装。async def ハンドラーと組み合わせるには自前で `run_in_executor` を書く必要がある。
56
+
57
+ ### 問題3: run_in_executor パターンが冗長で発見しにくい
58
+
59
+ ```python
60
+ # ✅ 正しいが、毎回これを書くのは辛い
61
+ @app.post("/tasks")
62
+ async def create_task(body: TaskBody) -> JSONResponse:
63
+ loop = asyncio.get_event_loop()
64
+ result = await loop.run_in_executor(None, _create_task_sync, body.title, body.simulate_ms)
65
+ return JSONResponse(result, status_code=201)
66
+ ```
67
+
68
+ - `asyncio.get_event_loop()` は Python 3.10+ で deprecation warning を出すことがある
69
+ - `None` の意味(default executor = ThreadPoolExecutor)が自明でない
70
+ - 複数の引数を渡すには `functools.partial` か lambda が必要になりさらに冗長になる
71
+
72
+ ### 問題4: `def` ハンドラーが実は最もシンプルで安全
73
+
74
+ ```python
75
+ # ✅ sync def は FastAPI が自動でスレッドプールに退避する
76
+ @app.post("/tasks")
77
+ def create_task(body: TaskBody) -> JSONResponse:
78
+ result = _create_task_sync(body.title) # 安全
79
+ return JSONResponse(result, status_code=201)
80
+ ```
81
+
82
+ `async def` を書く必然性がないなら `def` を使うべきだが、
83
+ 「FastAPI = async」というイメージから全ハンドラーを `async def` にするユーザーが多い。
84
+ nene2 のドキュメントがこれを明示していない。
85
+
86
+ ---
87
+
88
+ ## テスト結果(全12件パス)
89
+
90
+ ```
91
+ test_sync_in_async_creates_task PASSED # パターンA: 機能はする(ブロッキングだが)
92
+ test_executor_creates_task PASSED # パターンB: executor 経由
93
+ test_sync_def_creates_task PASSED # パターンC: sync def
94
+ test_list_tasks PASSED
95
+ test_sync_def_does_not_block_event_loop PASSED # イベントループが自由であることを確認
96
+ test_sync_in_async_blocks_when_slow PASSED # 200ms スリープがそのまま待機時間になる
97
+ test_executor_avoids_blocking PASSED
98
+ test_middlewares_work_with_all_patterns PASSED # setup_middlewares() は全パターンで動作
99
+ test_422_on_invalid_title_length PASSED
100
+ test_422_on_negative_simulate_ms PASSED
101
+ test_no_async_db_layer_in_nene2 PASSED # 非同期DB層が存在しないことを確認
102
+ test_threadpool_pattern_is_verbose PASSED # 冗長さのドキュメンタリーテスト
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 摩擦ポイント一覧
108
+
109
+ | ID | 内容 | 深刻度 |
110
+ |---|---|---|
111
+ | F76-1 | nene2 に async DB 層がなく、async ハンドラーでは run_in_executor が必要 | 高 |
112
+ | F76-2 | async def + sync DB のブロッキングがサイレント(警告なし) | 高 |
113
+ | F76-3 | run_in_executor の書き方が冗長で、正しい引数渡しが分かりにくい | 中 |
114
+ | F76-4 | ドキュメントが async def vs def の使い分けを説明していない | 中 |
115
+
116
+ ---
117
+
118
+ ## 使用感(主観評価)
119
+
120
+ ### 直感性 ★★☆☆☆
121
+
122
+ 「FastAPI を使っているので全ハンドラーを async def にする」のは自然な発想。
123
+ しかし nene2 の DB 層は同期なので、これが罠になる。
124
+ `async def` で書いたコードが正常に動き、テストも全部通る — でも本番では爆発する。
125
+ 「動いているから正しい」と思ってしまう構造が非常にやっかいで直感に反する。
126
+
127
+ ### 実害の深刻さ ★★★★☆
128
+
129
+ 本番環境で同時アクセスが増えてからはじめて顕在化する。
130
+ レスポンスが遅い → サーバーがダウン、という流れをたどる典型的なパフォーマンス地雷。
131
+ 「なぜか重い」「スケールしない」という症状で現れるため、根本原因の特定も遅れやすい。
132
+
133
+ ### 修正のしやすさ ★★☆☆☆
134
+
135
+ `run_in_executor` パターンを知っていれば直せるが、記述が冗長で毎回ハンドラーに書くのはつらい。
136
+ 根本解決(async DB 層の実装)はSQLAlchemy async対応が必要で、工数が大きい。
137
+ `def` ハンドラーへの移行が最もシンプルだが、`async` 依存のロジックが混在していると難しい。
138
+
139
+ ### 総合コメント
140
+
141
+ FastAPI + SQLAlchemy の組み合わせは最も多いユースケースの一つ。
142
+ 「nene2 を使えばすぐ始められる」が「パフォーマンス問題で詰まる」という流れは
143
+ ユーザー離れを引き起こす可能性がある。
144
+ `def` vs `async def` の使い分けガイドラインと、将来的な async DB サポートが必要。
145
+
146
+ ---
147
+
148
+ ## 推奨アクション
149
+
150
+ 1. **Issue**: `async def` ハンドラー内での `SqlAlchemy` 同期呼び出し警告をドキュメントに追加
151
+ 2. **Issue**: `run_in_threadpool()` ヘルパー(または `asyncify()` パターン)の提供
152
+ 3. **将来**: `AsyncSqlAlchemyQueryExecutor` の実装(SQLAlchemy 2.0 async 対応)
@@ -0,0 +1,179 @@
1
+ # FT77: BearerToken + ApiKey 混在認証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: 同一アプリで認証方式を混在させる — `/admin/*` は JWT Bearer、`/webhook/*` は API Key
5
+ **バージョン**: v1.8.22
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft77-mixed-auth/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ 実運用では「認証方式がルートによって異なる」は非常に一般的なニーズ。
13
+ nene2 の `BearerTokenMiddleware` と `ApiKeyAuthMiddleware` を混在させる方法を検証した。
14
+ ミドルウェアはアプリ全体をカバーするため、`exclude_paths` を使った回避策が必要で、
15
+ ここに大きな摩擦があることが判明した。
16
+
17
+ ---
18
+
19
+ ## 実装したパターン
20
+
21
+ ```
22
+ /admin/* → BearerTokenMiddleware(JWT Bearer 必須)
23
+ /webhook/* → ApiKeyAuthMiddleware(X-Api-Key 必須)
24
+ /public/* → 認証不要
25
+ ```
26
+
27
+ ### 設定コード
28
+
29
+ ```python
30
+ app.add_middleware(
31
+ BearerTokenMiddleware,
32
+ verifier=admin_verifier,
33
+ exclude_paths=[
34
+ "/docs", "/openapi.json", "/redoc", "/health",
35
+ "/public/hello", "/public/status",
36
+ "/webhook/event", "/webhook/ping", # ← webhook は除外
37
+ ],
38
+ )
39
+
40
+ app.add_middleware(
41
+ ApiKeyAuthMiddleware,
42
+ verifier=webhook_verifier,
43
+ exclude_paths=[
44
+ "/docs", "/openapi.json", "/redoc", "/health",
45
+ "/public/hello", "/public/status",
46
+ "/admin/dashboard", "/admin/users", # ← admin は除外
47
+ ],
48
+ )
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 発見した問題
54
+
55
+ ### 問題1: exclude_paths の二重管理
56
+
57
+ 認証対象でないパスを **各ミドルウェアに個別に列挙** しなければならない。
58
+
59
+ - 新しい `/admin/settings` ルートを追加 → `ApiKeyAuthMiddleware` の `exclude_paths` にも追加必要
60
+ - 忘れると: Bearer トークンを持っていても、API Key がないため 401 になる
61
+ - **サイレント障害**: 認証エラーなので「なぜ 401?」と混乱しやすい
62
+
63
+ ### 問題2: exclude_paths はルートではなく完全一致パス
64
+
65
+ `exclude_paths` はプレフィックスマッチングをサポートしない。
66
+
67
+ ```python
68
+ # ❌ プレフィックスマッチしない
69
+ exclude_paths=["/admin"] # /admin/dashboard にマッチしない
70
+
71
+ # ✅ 完全一致のみ
72
+ exclude_paths=["/admin/dashboard", "/admin/users"] # 各パスを列挙
73
+ ```
74
+
75
+ ルート数が増えると管理が煩雑になる。
76
+
77
+ ### 問題3: per-route 認証デコレーターがない
78
+
79
+ FastAPI の Depends() パターンでルートごとに認証を指定することが nene2 では直接できない。
80
+
81
+ ```python
82
+ # FastAPI 標準 (nene2 提供なし)
83
+ from fastapi.security import HTTPBearer
84
+ security = HTTPBearer()
85
+
86
+ @app.get("/admin/dashboard")
87
+ def admin(token = Depends(security)):
88
+ ...
89
+ ```
90
+
91
+ nene2 でこれをやるには生の FastAPI `Depends()` を使う必要があり、
92
+ nene2 の `TokenVerifierProtocol` との統合方法が不明瞭。
93
+
94
+ ### 問題4: 「どちらかで認証」が実現できない
95
+
96
+ Bearer でも API Key でも OK という「OR 認証」がミドルウェア方式では実現困難。
97
+
98
+ ```
99
+ # 現状では:
100
+ BearerMiddleware: 対象パスはBearer必須
101
+ ApiKeyMiddleware: 対象パスはApiKey必須
102
+
103
+ # 実現できない:
104
+ "Bearer OR ApiKey どちらかがあれば OK"
105
+ ```
106
+
107
+ ---
108
+
109
+ ## テスト結果(全17件パス)
110
+
111
+ ```
112
+ test_public_hello_no_auth PASSED # 公開エンドポイント
113
+ test_public_status_no_auth PASSED
114
+ test_health_no_auth PASSED
115
+ test_admin_with_valid_bearer PASSED # 正常認証
116
+ test_admin_without_auth_returns_401 PASSED
117
+ test_admin_with_invalid_bearer_returns_401 PASSED
118
+ test_admin_with_api_key_instead_of_bearer_returns_401 PASSED # 認証方式が違う
119
+ test_admin_users_with_valid_bearer PASSED
120
+ test_webhook_with_valid_api_key PASSED
121
+ test_webhook_without_auth_returns_401 PASSED
122
+ test_webhook_with_invalid_api_key_returns_401 PASSED
123
+ test_webhook_with_bearer_instead_of_api_key_returns_401 PASSED
124
+ test_admin_with_both_headers_bearer_wins PASSED # 両方送ると適切に処理
125
+ test_webhook_with_both_headers_apikey_wins PASSED
126
+ test_friction_new_admin_route_needs_exclude_update PASSED # 摩擦の記録
127
+ test_friction_exclude_paths_is_per_middleware PASSED
128
+ test_nene2_has_no_per_route_auth_decorator PASSED # per-route 機能なし確認
129
+ ```
130
+
131
+ ---
132
+
133
+ ## 摩擦ポイント一覧
134
+
135
+ | ID | 内容 | 深刻度 |
136
+ |---|---|---|
137
+ | F77-1 | exclude_paths の二重管理 — 新ルートを追加するたびに各ミドルウェアを更新 | 高 |
138
+ | F77-2 | exclude_paths がプレフィックスマッチをサポートしない(完全一致のみ)| 中 |
139
+ | F77-3 | per-route 認証デコレーターがない — Depends() との統合方法が不明 | 中 |
140
+ | F77-4 | Bearer OR ApiKey の OR 認証が実現できない | 中 |
141
+
142
+ ---
143
+
144
+ ## 使用感(主観評価)
145
+
146
+ ### 直感性 ★★☆☆☆
147
+
148
+ 「ミドルウェアは全体をカバーする」という原則は理解できる。
149
+ しかし「特定のルートだけ認証したい」という非常に一般的なニーズに、
150
+ `exclude_paths` という「否定のリスト」で対応するのは直感に反する。
151
+
152
+ Express の `passport.authenticate()` や Spring Security の `requestMatchers()` は
153
+ 「守りたいパスを指定」するモデル。nene2 は「除外するパスを指定」という逆のモデルで混乱しやすい。
154
+
155
+ ### 実害の深刻さ ★★★★☆
156
+
157
+ 新しいルートを追加したとき、`exclude_paths` の更新を忘れると **サイレントな認証エラー** が発生する。
158
+ 本番でユーザーが急に 401 になり、コードを読んでも一見問題がないように見える。
159
+ ミドルウェアの設定を疑わないと根本原因に辿り着けない。
160
+
161
+ ### 修正のしやすさ ★★★☆☆
162
+
163
+ 根本解決は「プレフィックスマッチ対応」または「per-route auth」の提供。
164
+ プレフィックスマッチは小さな変更で実現できる。
165
+ per-route auth の提供は FastAPI Depends() との統合が必要で中程度の工数。
166
+
167
+ ### 総合コメント
168
+
169
+ 「守りたいパスのプレフィックスを指定する」モデルへの転換が最も効果的。
170
+ `include_paths: list[str] | None` または `path_prefix: str` パラメーターを追加するだけで、
171
+ 「`/admin` 以下は全部 Bearer 必須」と書けるようになり、UX が大幅に改善する。
172
+
173
+ ---
174
+
175
+ ## 推奨アクション
176
+
177
+ 1. **Issue**: `BearerTokenMiddleware` / `ApiKeyAuthMiddleware` に `include_paths` または `path_prefixes` パラメーターを追加
178
+ 2. **Issue**: `exclude_paths` にプレフィックスマッチ(`/admin/*` 形式)をサポート
179
+ 3. **将来**: `nene2.auth` に FastAPI `Depends()` 統合ヘルパーを提供
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.21"
3
+ version = "1.8.23"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -17,10 +17,30 @@ _DEFAULT_API_KEY_HEADER = "X-Api-Key"
17
17
 
18
18
 
19
19
  class ApiKeyAuthMiddleware(BaseHTTPMiddleware):
20
- """Require a valid API key header on every request.
20
+ """Require a valid API key header on matching requests.
21
21
 
22
- The header name defaults to ``X-Api-Key`` but can be customised::
22
+ The header name defaults to ``X-Api-Key`` but can be customised.
23
23
 
24
+ **Path filtering** — two complementary options (mutually exclusive):
25
+
26
+ - ``include_paths``: only protect paths whose prefix matches one of these values.
27
+ All other paths pass through without authentication.
28
+ Ideal for protecting a specific sub-tree (e.g. ``["/webhook"]``).
29
+ - ``exclude_paths``: protect every path **except** these exact paths.
30
+ Ideal for skipping docs / health endpoints.
31
+
32
+ When both are provided, ``include_paths`` takes precedence.
33
+
34
+ Examples::
35
+
36
+ # Protect only /webhook/* routes (prefix match)
37
+ app.add_middleware(
38
+ ApiKeyAuthMiddleware,
39
+ verifier=LocalTokenVerifier(api_keys),
40
+ include_paths=["/webhook"],
41
+ )
42
+
43
+ # Protect everything except docs/health (exact match)
24
44
  app.add_middleware(
25
45
  ApiKeyAuthMiddleware,
26
46
  verifier=LocalTokenVerifier(api_keys),
@@ -36,14 +56,21 @@ class ApiKeyAuthMiddleware(BaseHTTPMiddleware):
36
56
  verifier: TokenVerifierProtocol,
37
57
  header_name: str = _DEFAULT_API_KEY_HEADER,
38
58
  exclude_paths: list[str] | None = None,
59
+ include_paths: list[str] | None = None,
39
60
  ) -> None:
40
61
  super().__init__(app) # type: ignore[arg-type]
41
62
  self._verifier = verifier
42
63
  self._header_name = header_name
43
64
  self._exclude_paths = set(exclude_paths or [])
65
+ self._include_paths = list(include_paths or [])
66
+
67
+ def _should_authenticate(self, path: str) -> bool:
68
+ if self._include_paths:
69
+ return any(path.startswith(prefix) for prefix in self._include_paths)
70
+ return path not in self._exclude_paths
44
71
 
45
72
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
46
- if request.url.path in self._exclude_paths:
73
+ if not self._should_authenticate(request.url.path):
47
74
  return await call_next(request)
48
75
  api_key = request.headers.get(self._header_name, "")
49
76
  try:
@@ -17,11 +17,28 @@ _WWW_AUTH = 'Bearer realm="api"'
17
17
 
18
18
 
19
19
  class BearerTokenMiddleware(BaseHTTPMiddleware):
20
- """Require a valid Bearer token on every request.
20
+ """Require a valid Bearer token on matching requests.
21
21
 
22
- Use ``exclude_paths`` to skip authentication for specific paths such as
23
- health-check endpoints or API documentation::
22
+ **Path filtering** two complementary options (mutually exclusive):
24
23
 
24
+ - ``include_paths``: only protect paths whose prefix matches one of these values.
25
+ All other paths pass through without authentication.
26
+ Ideal for protecting a specific sub-tree (e.g. ``["/admin"]``).
27
+ - ``exclude_paths``: protect every path **except** these exact paths.
28
+ Ideal for skipping docs / health endpoints.
29
+
30
+ When both are provided, ``include_paths`` takes precedence.
31
+
32
+ Examples::
33
+
34
+ # Protect only /admin/* routes (prefix match)
35
+ app.add_middleware(
36
+ BearerTokenMiddleware,
37
+ verifier=LocalTokenVerifier(tokens),
38
+ include_paths=["/admin"],
39
+ )
40
+
41
+ # Protect everything except docs/health (exact match)
25
42
  app.add_middleware(
26
43
  BearerTokenMiddleware,
27
44
  verifier=LocalTokenVerifier(tokens),
@@ -35,13 +52,20 @@ class BearerTokenMiddleware(BaseHTTPMiddleware):
35
52
  *,
36
53
  verifier: TokenVerifierProtocol,
37
54
  exclude_paths: list[str] | None = None,
55
+ include_paths: list[str] | None = None,
38
56
  ) -> None:
39
57
  super().__init__(app) # type: ignore[arg-type]
40
58
  self._verifier = verifier
41
59
  self._exclude_paths = set(exclude_paths or [])
60
+ self._include_paths = list(include_paths or [])
61
+
62
+ def _should_authenticate(self, path: str) -> bool:
63
+ if self._include_paths:
64
+ return any(path.startswith(prefix) for prefix in self._include_paths)
65
+ return path not in self._exclude_paths
42
66
 
43
67
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
44
- if request.url.path in self._exclude_paths:
68
+ if not self._should_authenticate(request.url.path):
45
69
  return await call_next(request)
46
70
  auth = request.headers.get("Authorization", "")
47
71
  if not auth.startswith("Bearer "):
@@ -1,5 +1,7 @@
1
1
  """UseCase contracts — synchronous and asynchronous Protocol definitions."""
2
2
 
3
+ from starlette.concurrency import run_in_threadpool
4
+
3
5
  from .protocols import AsyncUseCaseProtocol, UseCaseProtocol
4
6
 
5
- __all__ = ["AsyncUseCaseProtocol", "UseCaseProtocol"]
7
+ __all__ = ["AsyncUseCaseProtocol", "UseCaseProtocol", "run_in_threadpool"]
@@ -122,3 +122,72 @@ def test_custom_header_name_in_error_message() -> None:
122
122
  response = client.get("/secret")
123
123
  assert response.status_code == 401
124
124
  assert "X-Internal-Key" in response.json().get("detail", "")
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # include_paths tests
129
+ # ---------------------------------------------------------------------------
130
+ def test_include_paths_protects_matching_prefix() -> None:
131
+ app = FastAPI()
132
+ app.add_middleware(
133
+ ApiKeyAuthMiddleware,
134
+ verifier=LocalTokenVerifier(["key"]),
135
+ include_paths=["/webhook"],
136
+ )
137
+
138
+ @app.get("/webhook/event")
139
+ async def webhook_event() -> JSONResponse:
140
+ return JSONResponse({"received": True})
141
+
142
+ @app.get("/public/hello")
143
+ async def public_hello() -> JSONResponse:
144
+ return JSONResponse({"hello": True})
145
+
146
+ client = TestClient(app)
147
+ assert client.get("/webhook/event").status_code == 401
148
+ assert client.get("/webhook/event", headers={"X-Api-Key": "key"}).status_code == 200
149
+ assert client.get("/public/hello").status_code == 200
150
+
151
+
152
+ def test_include_paths_multiple_prefixes() -> None:
153
+ app = FastAPI()
154
+ app.add_middleware(
155
+ ApiKeyAuthMiddleware,
156
+ verifier=LocalTokenVerifier(["key"]),
157
+ include_paths=["/webhook", "/internal"],
158
+ )
159
+
160
+ @app.get("/webhook/x")
161
+ async def webhook_x() -> JSONResponse:
162
+ return JSONResponse({"ok": True})
163
+
164
+ @app.get("/internal/y")
165
+ async def internal_y() -> JSONResponse:
166
+ return JSONResponse({"ok": True})
167
+
168
+ @app.get("/public/z")
169
+ async def public_z() -> JSONResponse:
170
+ return JSONResponse({"ok": True})
171
+
172
+ client = TestClient(app)
173
+ assert client.get("/webhook/x").status_code == 401
174
+ assert client.get("/internal/y").status_code == 401
175
+ assert client.get("/public/z").status_code == 200
176
+
177
+
178
+ def test_include_paths_takes_precedence_over_exclude_paths() -> None:
179
+ """両方指定されたときは include_paths が優先される。"""
180
+ app = FastAPI()
181
+ app.add_middleware(
182
+ ApiKeyAuthMiddleware,
183
+ verifier=LocalTokenVerifier(["key"]),
184
+ include_paths=["/webhook"],
185
+ exclude_paths=["/webhook/open"], # include_paths があるので無視される
186
+ )
187
+
188
+ @app.get("/webhook/open")
189
+ async def webhook_open() -> JSONResponse:
190
+ return JSONResponse({"ok": True})
191
+
192
+ client = TestClient(app)
193
+ assert client.get("/webhook/open").status_code == 401
@@ -126,3 +126,84 @@ def test_local_verifier_accepts_frozenset() -> None:
126
126
  verifier = LocalTokenVerifier(frozenset({"tok-x", "tok-y"}))
127
127
  assert verifier.verify("tok-x") is True
128
128
  assert verifier.verify("unknown") is False
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # include_paths tests
133
+ # ---------------------------------------------------------------------------
134
+ def _make_app_with_include_paths(tokens: list[str], include_paths: list[str]) -> FastAPI:
135
+ app = FastAPI()
136
+ app.add_middleware(
137
+ BearerTokenMiddleware,
138
+ verifier=LocalTokenVerifier(tokens),
139
+ include_paths=include_paths,
140
+ )
141
+
142
+ @app.get("/admin/dashboard")
143
+ async def admin_dashboard() -> JSONResponse:
144
+ return JSONResponse({"admin": True})
145
+
146
+ @app.get("/public/hello")
147
+ async def public_hello() -> JSONResponse:
148
+ return JSONResponse({"hello": True})
149
+
150
+ return app
151
+
152
+
153
+ def test_include_paths_protects_matching_prefix() -> None:
154
+ client = TestClient(_make_app_with_include_paths(["tok"], ["/admin"]))
155
+ assert client.get("/admin/dashboard").status_code == 401
156
+ assert (
157
+ client.get("/admin/dashboard", headers={"Authorization": "Bearer tok"}).status_code == 200
158
+ )
159
+
160
+
161
+ def test_include_paths_skips_non_matching_prefix() -> None:
162
+ """include_paths にないパスは認証なしで通過する。"""
163
+ client = TestClient(_make_app_with_include_paths(["tok"], ["/admin"]))
164
+ assert client.get("/public/hello").status_code == 200
165
+
166
+
167
+ def test_include_paths_multiple_prefixes() -> None:
168
+ app = FastAPI()
169
+ app.add_middleware(
170
+ BearerTokenMiddleware,
171
+ verifier=LocalTokenVerifier(["tok"]),
172
+ include_paths=["/admin", "/private"],
173
+ )
174
+
175
+ @app.get("/admin/x")
176
+ async def admin_x() -> JSONResponse:
177
+ return JSONResponse({"ok": True})
178
+
179
+ @app.get("/private/y")
180
+ async def private_y() -> JSONResponse:
181
+ return JSONResponse({"ok": True})
182
+
183
+ @app.get("/public/z")
184
+ async def public_z() -> JSONResponse:
185
+ return JSONResponse({"ok": True})
186
+
187
+ client = TestClient(app)
188
+ assert client.get("/admin/x").status_code == 401
189
+ assert client.get("/private/y").status_code == 401
190
+ assert client.get("/public/z").status_code == 200
191
+
192
+
193
+ def test_include_paths_takes_precedence_over_exclude_paths() -> None:
194
+ """両方指定されたときは include_paths が優先される。"""
195
+ app = FastAPI()
196
+ app.add_middleware(
197
+ BearerTokenMiddleware,
198
+ verifier=LocalTokenVerifier(["tok"]),
199
+ include_paths=["/admin"],
200
+ exclude_paths=["/admin/open"], # include_paths があるので無視される
201
+ )
202
+
203
+ @app.get("/admin/open")
204
+ async def admin_open() -> JSONResponse:
205
+ return JSONResponse({"ok": True})
206
+
207
+ client = TestClient(app)
208
+ # include_paths=["/admin"] が優先 → /admin/open も保護される
209
+ assert client.get("/admin/open").status_code == 401