nene2-python 1.8.22__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.22 → nene2_python-1.8.23}/CHANGELOG.md +12 -0
  2. {nene2_python-1.8.22 → nene2_python-1.8.23}/PKG-INFO +1 -1
  3. nene2_python-1.8.23/docs/field-trials/2026-05-field-trial-77.md +179 -0
  4. {nene2_python-1.8.22 → nene2_python-1.8.23}/pyproject.toml +1 -1
  5. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/api_key.py +30 -3
  6. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/bearer_token.py +28 -4
  7. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_api_key.py +69 -0
  8. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_bearer_token.py +81 -0
  9. {nene2_python-1.8.22 → nene2_python-1.8.23}/uv.lock +1 -1
  10. {nene2_python-1.8.22 → nene2_python-1.8.23}/.env.example +0 -0
  11. {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/ci.yml +0 -0
  12. {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/docs.yml +0 -0
  13. {nene2_python-1.8.22 → nene2_python-1.8.23}/.github/workflows/publish.yml +0 -0
  14. {nene2_python-1.8.22 → nene2_python-1.8.23}/.gitignore +0 -0
  15. {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/config.mts +0 -0
  16. {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/theme/custom.css +0 -0
  17. {nene2_python-1.8.22 → nene2_python-1.8.23}/.vitepress/theme/index.ts +0 -0
  18. {nene2_python-1.8.22 → nene2_python-1.8.23}/AGENTS.md +0 -0
  19. {nene2_python-1.8.22 → nene2_python-1.8.23}/CLAUDE.md +0 -0
  20. {nene2_python-1.8.22 → nene2_python-1.8.23}/Dockerfile +0 -0
  21. {nene2_python-1.8.22 → nene2_python-1.8.23}/LICENSE +0 -0
  22. {nene2_python-1.8.22 → nene2_python-1.8.23}/README.md +0 -0
  23. {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/README +0 -0
  24. {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/env.py +0 -0
  25. {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/script.py.mako +0 -0
  26. {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  27. {nene2_python-1.8.22 → nene2_python-1.8.23}/alembic.ini +0 -0
  28. {nene2_python-1.8.22 → nene2_python-1.8.23}/compose.yaml +0 -0
  29. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0001-toolchain.md +0 -0
  30. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0002-clean-architecture.md +0 -0
  31. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0003-security-first.md +0 -0
  32. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0004-ai-first-design.md +0 -0
  33. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0005-logging.md +0 -0
  34. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0006-rate-limiting.md +0 -0
  35. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0009-mcp-design.md +0 -0
  36. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0010-async-use-case.md +0 -0
  37. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  38. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/de/index.md +0 -0
  39. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/de/tutorials/getting-started.md +0 -0
  40. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/explanation/architecture.md +0 -0
  41. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/explanation/design-philosophy.md +0 -0
  42. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  43. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  44. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  45. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  46. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  47. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  48. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  49. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  50. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  51. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  52. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  53. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  54. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  55. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  56. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  57. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  58. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  59. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  60. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  61. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  62. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  63. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  64. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  65. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  66. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  67. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  68. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  69. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  70. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  71. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  72. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  73. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  74. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  75. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  76. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  77. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  78. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  79. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  80. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  81. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  82. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  83. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  84. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  85. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  86. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  87. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  88. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  89. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  90. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  91. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  92. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  93. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  94. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  95. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  96. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  97. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  98. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  99. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  100. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  101. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  102. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  103. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  104. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  105. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  106. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  107. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  108. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  109. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  110. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  111. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  112. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  113. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  114. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  115. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  116. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  117. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  118. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/fr/index.md +0 -0
  119. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/fr/tutorials/getting-started.md +0 -0
  120. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/add-new-domain.md +0 -0
  121. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/async-use-case.md +0 -0
  122. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/configure-auth.md +0 -0
  123. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/middleware-stack.md +0 -0
  124. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/new-project.md +0 -0
  125. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/problem-details.md +0 -0
  126. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/run-tests.md +0 -0
  127. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/sqlalchemy-repository.md +0 -0
  128. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/how-to/validation.md +0 -0
  129. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/howto/mcp-setup.md +0 -0
  130. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/index.md +0 -0
  131. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/explanation/architecture.md +0 -0
  132. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/explanation/design-philosophy.md +0 -0
  133. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/add-new-domain.md +0 -0
  134. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/configure-auth.md +0 -0
  135. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/new-project.md +0 -0
  136. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/run-tests.md +0 -0
  137. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  138. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/howto/mcp-setup.md +0 -0
  139. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/index.md +0 -0
  140. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/api.md +0 -0
  141. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/configuration.md +0 -0
  142. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/reference/framework-modules.md +0 -0
  143. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/tutorials/first-domain.md +0 -0
  144. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/ja/tutorials/getting-started.md +0 -0
  145. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/pt-br/index.md +0 -0
  146. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/pt-br/tutorials/getting-started.md +0 -0
  147. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/api.md +0 -0
  148. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/configuration.md +0 -0
  149. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/reference/framework-modules.md +0 -0
  150. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/roadmap.md +0 -0
  151. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/todo/current.md +0 -0
  152. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/tutorials/first-domain.md +0 -0
  153. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/tutorials/getting-started.md +0 -0
  154. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/zh/index.md +0 -0
  155. {nene2_python-1.8.22 → nene2_python-1.8.23}/docs/zh/tutorials/getting-started.md +0 -0
  156. {nene2_python-1.8.22 → nene2_python-1.8.23}/package-lock.json +0 -0
  157. {nene2_python-1.8.22 → nene2_python-1.8.23}/package.json +0 -0
  158. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/__init__.py +0 -0
  159. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/__main__.py +0 -0
  160. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/app.py +0 -0
  161. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/__init__.py +0 -0
  162. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/entity.py +0 -0
  163. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/exceptions.py +0 -0
  164. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/handler.py +0 -0
  165. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/repository.py +0 -0
  166. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/sqlalchemy_repository.py +0 -0
  167. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/comment/use_case.py +0 -0
  168. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/mcp.py +0 -0
  169. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/__init__.py +0 -0
  170. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/async_use_case.py +0 -0
  171. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/entity.py +0 -0
  172. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/exceptions.py +0 -0
  173. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/handler.py +0 -0
  174. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/repository.py +0 -0
  175. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/sqlalchemy_repository.py +0 -0
  176. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/note/use_case.py +0 -0
  177. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/schema.py +0 -0
  178. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/__init__.py +0 -0
  179. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/entity.py +0 -0
  180. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/exceptions.py +0 -0
  181. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/handler.py +0 -0
  182. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/repository.py +0 -0
  183. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/sqlalchemy_repository.py +0 -0
  184. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/example/tag/use_case.py +0 -0
  185. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/__init__.py +0 -0
  186. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/__init__.py +0 -0
  187. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/exceptions.py +0 -0
  188. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/interfaces.py +0 -0
  189. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/auth/local_verifier.py +0 -0
  190. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/config/__init__.py +0 -0
  191. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/config/settings.py +0 -0
  192. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/__init__.py +0 -0
  193. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/exceptions.py +0 -0
  194. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/health.py +0 -0
  195. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/interfaces.py +0 -0
  196. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/sqlalchemy_executor.py +0 -0
  197. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/database/utils.py +0 -0
  198. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/__init__.py +0 -0
  199. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/health.py +0 -0
  200. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/pagination.py +0 -0
  201. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/http/problem_details.py +0 -0
  202. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/log/__init__.py +0 -0
  203. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/log/setup.py +0 -0
  204. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/__init__.py +0 -0
  205. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/http_client.py +0 -0
  206. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/mcp/server.py +0 -0
  207. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/__init__.py +0 -0
  208. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/domain_exception.py +0 -0
  209. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/error_handler.py +0 -0
  210. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_id.py +0 -0
  211. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_logging.py +0 -0
  212. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/request_size_limit.py +0 -0
  213. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/security_headers.py +0 -0
  214. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/setup.py +0 -0
  215. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/middleware/throttle.py +0 -0
  216. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/py.typed +0 -0
  217. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/use_case/__init__.py +0 -0
  218. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/use_case/protocols.py +0 -0
  219. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/validation/__init__.py +0 -0
  220. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/nene2/validation/exceptions.py +0 -0
  221. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/scripts/__init__.py +0 -0
  222. {nene2_python-1.8.22 → nene2_python-1.8.23}/src/scripts/export_openapi.py +0 -0
  223. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/__init__.py +0 -0
  224. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/__init__.py +0 -0
  225. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/__init__.py +0 -0
  226. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_http.py +0 -0
  227. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_repository.py +0 -0
  228. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/comment/test_comment_use_case.py +0 -0
  229. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/conftest.py +0 -0
  230. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/__init__.py +0 -0
  231. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_async_note_use_case.py +0 -0
  232. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_list_notes.py +0 -0
  233. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/note/test_note_repository.py +0 -0
  234. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/__init__.py +0 -0
  235. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/test_tag_repository.py +0 -0
  236. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/tag/test_tags.py +0 -0
  237. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/test_cors.py +0 -0
  238. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/example/test_mcp.py +0 -0
  239. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/__init__.py +0 -0
  240. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/__init__.py +0 -0
  241. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/auth/test_token_issuer.py +0 -0
  242. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/config/__init__.py +0 -0
  243. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/config/test_settings.py +0 -0
  244. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/__init__.py +0 -0
  245. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/test_transaction.py +0 -0
  246. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/database/test_utils.py +0 -0
  247. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/__init__.py +0 -0
  248. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_health.py +0 -0
  249. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_pagination.py +0 -0
  250. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/http/test_problem_details.py +0 -0
  251. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/log/__init__.py +0 -0
  252. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/log/test_setup.py +0 -0
  253. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/mcp/__init__.py +0 -0
  254. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/mcp/test_http_client.py +0 -0
  255. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/__init__.py +0 -0
  256. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_error_handler.py +0 -0
  257. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_id.py +0 -0
  258. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_logging.py +0 -0
  259. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  260. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_security_headers.py +0 -0
  261. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  262. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  263. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/middleware/test_throttle.py +0 -0
  264. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/__init__.py +0 -0
  265. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/test_protocols.py +0 -0
  266. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  267. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/validation/__init__.py +0 -0
  268. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/nene2/validation/test_exceptions.py +0 -0
  269. {nene2_python-1.8.22 → nene2_python-1.8.23}/tests/scripts/__init__.py +0 -0
  270. {nene2_python-1.8.22 → 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.22
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,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.22"
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 "):
@@ -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
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.8.22"
928
+ version = "1.8.23"
929
929
  source = { editable = "." }
930
930
  dependencies = [
931
931
  { name = "alembic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes