nene2-python 1.8.22__tar.gz → 1.8.24__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 (271) hide show
  1. {nene2_python-1.8.22 → nene2_python-1.8.24}/CHANGELOG.md +23 -0
  2. {nene2_python-1.8.22 → nene2_python-1.8.24}/PKG-INFO +1 -1
  3. nene2_python-1.8.24/docs/field-trials/2026-05-field-trial-77.md +179 -0
  4. nene2_python-1.8.24/docs/field-trials/2026-05-field-trial-78.md +164 -0
  5. {nene2_python-1.8.22 → nene2_python-1.8.24}/pyproject.toml +1 -1
  6. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/api_key.py +30 -3
  7. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/bearer_token.py +28 -4
  8. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/setup.py +6 -0
  9. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/throttle.py +14 -0
  10. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/auth/test_api_key.py +69 -0
  11. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/auth/test_bearer_token.py +81 -0
  12. {nene2_python-1.8.22 → nene2_python-1.8.24}/uv.lock +1 -1
  13. {nene2_python-1.8.22 → nene2_python-1.8.24}/.env.example +0 -0
  14. {nene2_python-1.8.22 → nene2_python-1.8.24}/.github/workflows/ci.yml +0 -0
  15. {nene2_python-1.8.22 → nene2_python-1.8.24}/.github/workflows/docs.yml +0 -0
  16. {nene2_python-1.8.22 → nene2_python-1.8.24}/.github/workflows/publish.yml +0 -0
  17. {nene2_python-1.8.22 → nene2_python-1.8.24}/.gitignore +0 -0
  18. {nene2_python-1.8.22 → nene2_python-1.8.24}/.vitepress/config.mts +0 -0
  19. {nene2_python-1.8.22 → nene2_python-1.8.24}/.vitepress/theme/custom.css +0 -0
  20. {nene2_python-1.8.22 → nene2_python-1.8.24}/.vitepress/theme/index.ts +0 -0
  21. {nene2_python-1.8.22 → nene2_python-1.8.24}/AGENTS.md +0 -0
  22. {nene2_python-1.8.22 → nene2_python-1.8.24}/CLAUDE.md +0 -0
  23. {nene2_python-1.8.22 → nene2_python-1.8.24}/Dockerfile +0 -0
  24. {nene2_python-1.8.22 → nene2_python-1.8.24}/LICENSE +0 -0
  25. {nene2_python-1.8.22 → nene2_python-1.8.24}/README.md +0 -0
  26. {nene2_python-1.8.22 → nene2_python-1.8.24}/alembic/README +0 -0
  27. {nene2_python-1.8.22 → nene2_python-1.8.24}/alembic/env.py +0 -0
  28. {nene2_python-1.8.22 → nene2_python-1.8.24}/alembic/script.py.mako +0 -0
  29. {nene2_python-1.8.22 → nene2_python-1.8.24}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  30. {nene2_python-1.8.22 → nene2_python-1.8.24}/alembic.ini +0 -0
  31. {nene2_python-1.8.22 → nene2_python-1.8.24}/compose.yaml +0 -0
  32. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0001-toolchain.md +0 -0
  33. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0002-clean-architecture.md +0 -0
  34. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0003-security-first.md +0 -0
  35. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0004-ai-first-design.md +0 -0
  36. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0005-logging.md +0 -0
  37. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0006-rate-limiting.md +0 -0
  38. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0009-mcp-design.md +0 -0
  39. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0010-async-use-case.md +0 -0
  40. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  41. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/de/index.md +0 -0
  42. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/de/tutorials/getting-started.md +0 -0
  43. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/explanation/architecture.md +0 -0
  44. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/explanation/design-philosophy.md +0 -0
  45. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  46. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  47. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  48. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  49. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  50. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  51. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  52. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  53. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  54. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  55. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  56. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  57. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  58. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  59. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  60. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  61. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  62. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  63. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  64. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  65. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  66. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  67. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  68. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  69. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  70. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  71. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  72. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  73. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  74. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  75. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  76. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  77. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  78. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  79. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  80. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  81. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  82. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  83. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  84. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  85. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  86. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  87. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  88. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  89. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  90. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  91. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  92. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  93. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  94. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  95. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  96. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  97. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  98. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  99. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  100. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  101. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  102. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  103. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  104. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  105. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  106. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  107. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  108. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  109. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  110. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  111. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  112. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  113. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  114. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  115. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  116. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  117. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  118. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  119. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  120. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  121. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/fr/index.md +0 -0
  122. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/fr/tutorials/getting-started.md +0 -0
  123. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/add-new-domain.md +0 -0
  124. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/async-use-case.md +0 -0
  125. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/configure-auth.md +0 -0
  126. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/middleware-stack.md +0 -0
  127. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/new-project.md +0 -0
  128. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/problem-details.md +0 -0
  129. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/run-tests.md +0 -0
  130. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/sqlalchemy-repository.md +0 -0
  131. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/how-to/validation.md +0 -0
  132. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/howto/mcp-setup.md +0 -0
  133. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/index.md +0 -0
  134. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/explanation/architecture.md +0 -0
  135. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/explanation/design-philosophy.md +0 -0
  136. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/how-to/add-new-domain.md +0 -0
  137. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/how-to/configure-auth.md +0 -0
  138. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/how-to/new-project.md +0 -0
  139. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/how-to/run-tests.md +0 -0
  140. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  141. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/howto/mcp-setup.md +0 -0
  142. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/index.md +0 -0
  143. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/reference/api.md +0 -0
  144. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/reference/configuration.md +0 -0
  145. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/reference/framework-modules.md +0 -0
  146. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/tutorials/first-domain.md +0 -0
  147. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/ja/tutorials/getting-started.md +0 -0
  148. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/pt-br/index.md +0 -0
  149. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/pt-br/tutorials/getting-started.md +0 -0
  150. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/reference/api.md +0 -0
  151. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/reference/configuration.md +0 -0
  152. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/reference/framework-modules.md +0 -0
  153. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/roadmap.md +0 -0
  154. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/todo/current.md +0 -0
  155. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/tutorials/first-domain.md +0 -0
  156. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/tutorials/getting-started.md +0 -0
  157. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/zh/index.md +0 -0
  158. {nene2_python-1.8.22 → nene2_python-1.8.24}/docs/zh/tutorials/getting-started.md +0 -0
  159. {nene2_python-1.8.22 → nene2_python-1.8.24}/package-lock.json +0 -0
  160. {nene2_python-1.8.22 → nene2_python-1.8.24}/package.json +0 -0
  161. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/__init__.py +0 -0
  162. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/__main__.py +0 -0
  163. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/app.py +0 -0
  164. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/__init__.py +0 -0
  165. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/entity.py +0 -0
  166. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/exceptions.py +0 -0
  167. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/handler.py +0 -0
  168. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/repository.py +0 -0
  169. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/sqlalchemy_repository.py +0 -0
  170. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/comment/use_case.py +0 -0
  171. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/mcp.py +0 -0
  172. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/__init__.py +0 -0
  173. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/async_use_case.py +0 -0
  174. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/entity.py +0 -0
  175. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/exceptions.py +0 -0
  176. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/handler.py +0 -0
  177. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/repository.py +0 -0
  178. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/sqlalchemy_repository.py +0 -0
  179. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/note/use_case.py +0 -0
  180. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/schema.py +0 -0
  181. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/__init__.py +0 -0
  182. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/entity.py +0 -0
  183. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/exceptions.py +0 -0
  184. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/handler.py +0 -0
  185. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/repository.py +0 -0
  186. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/sqlalchemy_repository.py +0 -0
  187. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/example/tag/use_case.py +0 -0
  188. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/__init__.py +0 -0
  189. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/__init__.py +0 -0
  190. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/exceptions.py +0 -0
  191. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/interfaces.py +0 -0
  192. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/auth/local_verifier.py +0 -0
  193. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/config/__init__.py +0 -0
  194. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/config/settings.py +0 -0
  195. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/__init__.py +0 -0
  196. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/exceptions.py +0 -0
  197. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/health.py +0 -0
  198. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/interfaces.py +0 -0
  199. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/sqlalchemy_executor.py +0 -0
  200. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/database/utils.py +0 -0
  201. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/http/__init__.py +0 -0
  202. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/http/health.py +0 -0
  203. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/http/pagination.py +0 -0
  204. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/http/problem_details.py +0 -0
  205. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/log/__init__.py +0 -0
  206. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/log/setup.py +0 -0
  207. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/mcp/__init__.py +0 -0
  208. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/mcp/http_client.py +0 -0
  209. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/mcp/server.py +0 -0
  210. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/__init__.py +0 -0
  211. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/domain_exception.py +0 -0
  212. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/error_handler.py +0 -0
  213. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/request_id.py +0 -0
  214. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/request_logging.py +0 -0
  215. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/request_size_limit.py +0 -0
  216. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/middleware/security_headers.py +0 -0
  217. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/py.typed +0 -0
  218. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/use_case/__init__.py +0 -0
  219. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/use_case/protocols.py +0 -0
  220. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/validation/__init__.py +0 -0
  221. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/nene2/validation/exceptions.py +0 -0
  222. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/scripts/__init__.py +0 -0
  223. {nene2_python-1.8.22 → nene2_python-1.8.24}/src/scripts/export_openapi.py +0 -0
  224. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/__init__.py +0 -0
  225. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/__init__.py +0 -0
  226. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/comment/__init__.py +0 -0
  227. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/comment/test_comment_http.py +0 -0
  228. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/comment/test_comment_repository.py +0 -0
  229. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/comment/test_comment_use_case.py +0 -0
  230. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/conftest.py +0 -0
  231. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/note/__init__.py +0 -0
  232. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/note/test_async_note_use_case.py +0 -0
  233. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/note/test_list_notes.py +0 -0
  234. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/note/test_note_repository.py +0 -0
  235. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/tag/__init__.py +0 -0
  236. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/tag/test_tag_repository.py +0 -0
  237. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/tag/test_tags.py +0 -0
  238. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/test_cors.py +0 -0
  239. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/example/test_mcp.py +0 -0
  240. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/__init__.py +0 -0
  241. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/auth/__init__.py +0 -0
  242. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/auth/test_token_issuer.py +0 -0
  243. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/config/__init__.py +0 -0
  244. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/config/test_settings.py +0 -0
  245. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/database/__init__.py +0 -0
  246. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/database/test_transaction.py +0 -0
  247. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/database/test_utils.py +0 -0
  248. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/http/__init__.py +0 -0
  249. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/http/test_health.py +0 -0
  250. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/http/test_pagination.py +0 -0
  251. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/http/test_problem_details.py +0 -0
  252. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/log/__init__.py +0 -0
  253. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/log/test_setup.py +0 -0
  254. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/mcp/__init__.py +0 -0
  255. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/mcp/test_http_client.py +0 -0
  256. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/__init__.py +0 -0
  257. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_error_handler.py +0 -0
  258. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_request_id.py +0 -0
  259. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_request_logging.py +0 -0
  260. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  261. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_security_headers.py +0 -0
  262. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  263. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  264. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/middleware/test_throttle.py +0 -0
  265. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/use_case/__init__.py +0 -0
  266. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/use_case/test_protocols.py +0 -0
  267. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  268. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/validation/__init__.py +0 -0
  269. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/nene2/validation/test_exceptions.py +0 -0
  270. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/scripts/__init__.py +0 -0
  271. {nene2_python-1.8.22 → nene2_python-1.8.24}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.23] — 2026-05-20
9
+
10
+ FT77 フィールドトライアル — BearerToken + ApiKey 混在認証と include_paths 追加。
11
+
12
+ ### Added
13
+ - `BearerTokenMiddleware` / `ApiKeyAuthMiddleware` に `include_paths` パラメーターを追加 (#331) (FT77)
14
+ — プレフィックスマッチで「守りたいパス」を直接指定でき、混在認証の `exclude_paths` 二重管理を解消
15
+ - Field trial report: `docs/field-trials/2026-05-field-trial-77.md` (FT77)
16
+
17
+ ---
18
+
19
+ ## [1.8.22] — 2026-05-20
20
+
21
+ FT76 フィールドトライアル — async def + sync DB ブロッキング問題と run_in_threadpool 追加。
22
+
23
+ ### Added
24
+ - `run_in_threadpool` を `nene2.use_case` から re-export (#326) (FT76)
25
+ — Starlette の `run_in_threadpool` を nene2 公開 API として公開し、
26
+ `async def` ハンドラーから同期 DB 処理を安全にスレッドプールへオフロードできる
27
+ - Field trial report: `docs/field-trials/2026-05-field-trial-76.md` (FT76)
28
+
29
+ ---
30
+
8
31
  ## [1.8.21] — 2026-05-20
9
32
 
10
33
  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.24
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()` 統合ヘルパーを提供
@@ -0,0 +1,164 @@
1
+ # FT78: ThrottleMiddleware 境界動作
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: レート制限のウィンドウリセット・バースト動作・path_limits の挙動検証
5
+ **バージョン**: v1.8.23
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft78-throttle-boundary/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `ThrottleMiddleware` の境界動作を詳細に検証した。
13
+ 基本機能(429 返却、Retry-After ヘッダー、path_limits)は期待通り動作した。
14
+ 一方で、Fixed Window アルゴリズムの構造的な制限と、
15
+ マルチプロセス環境での in-memory 共有不可という制約が明確になった。
16
+
17
+ ---
18
+
19
+ ## 動作確認
20
+
21
+ ### 429 レスポンス形式
22
+
23
+ ```json
24
+ {
25
+ "type": "https://httpstatuses.com/too-many-requests",
26
+ "title": "Too Many Requests",
27
+ "status": 429,
28
+ "detail": "Rate limit exceeded. Retry after 58 seconds."
29
+ }
30
+ ```
31
+
32
+ Retry-After ヘッダーあり ✅
33
+
34
+ ### レート制限ヘッダー(全レスポンスに付与)
35
+
36
+ ```
37
+ X-RateLimit-Limit: 3
38
+ X-RateLimit-Remaining: 2
39
+ X-RateLimit-Reset: 1747742980
40
+ ```
41
+
42
+ 全レスポンス(200 も 429 も)にヘッダーが付く ✅
43
+
44
+ ### path_limits: グローバルカウンターと独立 ✅
45
+
46
+ ```python
47
+ app.add_middleware(ThrottleMiddleware, limit=100, path_limits={"/api/search": 5})
48
+ ```
49
+
50
+ `/api/search` の制限 (5) は `/api/data` のグローバル制限 (100) と完全に独立。
51
+
52
+ ---
53
+
54
+ ## 発見した問題
55
+
56
+ ### 問題1: Fixed Window バースト問題(設計上の制限)
57
+
58
+ 固定ウィンドウ方式では、ウィンドウ境界で最大 `2 × limit` のリクエストが短時間に通過できる。
59
+
60
+ ```
61
+ window=60s, limit=3 の場合:
62
+ t=59s: 3 req 通過(ウィンドウ1の末尾)
63
+ t=61s: 3 req 通過(ウィンドウ2の先頭)
64
+ → 2秒間に 6 req が通ってしまう
65
+ ```
66
+
67
+ Flask-Limiter デフォルトの Sliding Window では防げるが、
68
+ nene2 は計算コストの低い Fixed Window を採用している。
69
+
70
+ ドキュメントにこの制限を明記すべき。
71
+
72
+ ### 問題2: in-memory カウンターはマルチプロセス非対応
73
+
74
+ `_counts` は Python の `dict` で保持されており、
75
+ 複数の uvicorn ワーカー(`gunicorn -w 4`)や Docker Pod では共有されない。
76
+
77
+ 実効的な制限は `limit × worker_count` になる。
78
+ 本番で水平スケールさせると全くレート制限が機能しない。
79
+
80
+ **ドキュメントに警告はある**(コードの docstring)が、README や使用例には書かれていない。
81
+
82
+ ### 問題3: カウント状態の観察手段がない
83
+
84
+ 現在のカウント状態(「このIPが今 N/M 消費」)を外部から取得する手段がない。
85
+ デバッグ時に困る。
86
+
87
+ ```python
88
+ # これが欲しいが存在しない
89
+ info = middleware.get_rate_info(ip="192.168.1.1")
90
+ print(info.current_count, info.remaining)
91
+ ```
92
+
93
+ ### 問題4: exclude_paths はヘッダーも返さない
94
+
95
+ 除外パスにはレート制限ヘッダーが一切付かない。
96
+ クライアントが「このパスは制限外か?」を知る方法がない。
97
+
98
+ ---
99
+
100
+ ## テスト結果(全14件パス)
101
+
102
+ ```
103
+ test_requests_within_limit_are_allowed PASSED
104
+ test_exceeding_limit_returns_429 PASSED
105
+ test_429_response_is_problem_details PASSED
106
+ test_rate_limit_headers_present_on_200 PASSED
107
+ test_rate_limit_remaining_decrements PASSED
108
+ test_retry_after_header_present_on_429 PASSED
109
+ test_retry_after_reasonable_value PASSED
110
+ test_path_limits_independent_from_global PASSED
111
+ test_path_limits_headers_show_path_limit PASSED
112
+ test_exclude_paths_bypass_throttle PASSED
113
+ test_window_resets_after_elapsed PASSED
114
+ test_friction_no_global_ip_tracking_visible_to_user PASSED
115
+ test_friction_in_memory_state_not_shared_across_workers PASSED
116
+ test_friction_fixed_window_burst_at_boundary PASSED
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 摩擦ポイント一覧
122
+
123
+ | ID | 内容 | 深刻度 |
124
+ |---|---|---|
125
+ | F78-1 | Fixed Window バースト問題がドキュメントに明記されていない | 中 |
126
+ | F78-2 | マルチプロセス非対応(in-memory)がドキュメントに明記されていない | 高 |
127
+ | F78-3 | カウント状態の観察 API がない | 低 |
128
+ | F78-4 | exclude_paths のパスにレート制限ヘッダーが付かない | 低 |
129
+
130
+ ---
131
+
132
+ ## 使用感(主観評価)
133
+
134
+ ### 直感性 ★★★★☆
135
+
136
+ `setup_middlewares(app, throttle_limit=60, throttle_window=60)` で一発設定できる点は非常に快適。
137
+ `path_limits` / `exclude_paths` のパラメーター名も直感的。
138
+ `Retry-After` が自動で付くのはエレガント。
139
+
140
+ ### 実害の深刻さ ★★★★☆
141
+
142
+ マルチプロセス非対応は本番で深刻。
143
+ 「レート制限を設定したのに効いていない」というデバッグは非常に時間がかかる。
144
+ ただし docstring に警告が書かれており、見れば分かる(見逃しやすいが)。
145
+
146
+ ### 修正のしやすさ ★★★★★
147
+
148
+ ドキュメント追記のみで対応できる問題が多い。
149
+ 実装変更(Redis 対応)は将来的な機能拡張として Issues に記録する程度でよい。
150
+
151
+ ### 総合コメント
152
+
153
+ 基本機能は非常によくできている。
154
+ 問題は「制限の文書化」が不足している点。
155
+ Fixed Window の特性とマルチプロセス制約を README や使用例に追記するだけで
156
+ UX が大幅に改善する。実装は変えなくてよい。
157
+
158
+ ---
159
+
160
+ ## 推奨アクション
161
+
162
+ 1. **Issue**: ThrottleMiddleware のドキュメントに Fixed Window バースト特性を追記
163
+ 2. **Issue**: ThrottleMiddleware のドキュメントにマルチプロセス非対応の警告を目立つ位置に追記
164
+ 3. **将来**: Redis カウンターバックエンドの検討(外部依存なので慎重に)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.22"
3
+ version = "1.8.24"
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 "):
@@ -90,6 +90,12 @@ def setup_middlewares(
90
90
  throttle_window: Rate-limit window in seconds (default: 60).
91
91
  throttle_path_limits: Per-path overrides for throttle limits.
92
92
  throttle_exclude_paths: Paths excluded from throttling.
93
+
94
+ .. warning::
95
+ ``ThrottleMiddleware`` uses an in-memory counter that is **not
96
+ shared across workers or pods**. Multi-process deployments will
97
+ see an effective limit of ``throttle_limit × worker_count``.
98
+ See :class:`ThrottleMiddleware` for details.
93
99
  max_request_bytes: Maximum request body size in bytes (default: 1 MiB).
94
100
  request_size_path_limits: Per-path size limits.
95
101
  request_size_exclude_paths: Paths excluded from size limiting.
@@ -55,6 +55,20 @@ class ThrottleMiddleware(BaseHTTPMiddleware):
55
55
  Path-limited endpoints are tracked independently from the global counter
56
56
  (the key includes the path, so ``/api/expensive`` quota is separate from
57
57
  the default quota for other paths).
58
+
59
+ .. warning:: **Single-process only.**
60
+ Counters are stored in an in-memory dict. When running multiple
61
+ uvicorn workers (e.g. ``gunicorn -w 4``) or multiple containers,
62
+ each process maintains its own counter, so the effective rate limit
63
+ is ``limit × worker_count``. For multi-process deployments, enforce
64
+ rate limits at the reverse proxy (nginx, Caddy) or use a shared
65
+ store (Redis).
66
+
67
+ .. note:: **Fixed-window burst at boundaries.**
68
+ Fixed-window counting can pass up to ``2 × limit`` requests in a
69
+ short burst when requests arrive just before and just after a window
70
+ boundary. If you need protection against burst traffic, consider
71
+ sliding-window rate limiting at the proxy layer.
58
72
  """
59
73
 
60
74
  def __init__(
@@ -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