nene2-python 1.8.23__tar.gz → 1.8.25__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 (272) hide show
  1. {nene2_python-1.8.23 → nene2_python-1.8.25}/CHANGELOG.md +22 -0
  2. {nene2_python-1.8.23 → nene2_python-1.8.25}/PKG-INFO +1 -1
  3. nene2_python-1.8.25/docs/field-trials/2026-05-field-trial-78.md +164 -0
  4. nene2_python-1.8.25/docs/field-trials/2026-05-field-trial-79.md +186 -0
  5. {nene2_python-1.8.23 → nene2_python-1.8.25}/pyproject.toml +1 -1
  6. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/request_logging.py +24 -0
  7. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/setup.py +6 -0
  8. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/throttle.py +14 -0
  9. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_request_logging.py +78 -0
  10. {nene2_python-1.8.23 → nene2_python-1.8.25}/uv.lock +1 -1
  11. {nene2_python-1.8.23 → nene2_python-1.8.25}/.env.example +0 -0
  12. {nene2_python-1.8.23 → nene2_python-1.8.25}/.github/workflows/ci.yml +0 -0
  13. {nene2_python-1.8.23 → nene2_python-1.8.25}/.github/workflows/docs.yml +0 -0
  14. {nene2_python-1.8.23 → nene2_python-1.8.25}/.github/workflows/publish.yml +0 -0
  15. {nene2_python-1.8.23 → nene2_python-1.8.25}/.gitignore +0 -0
  16. {nene2_python-1.8.23 → nene2_python-1.8.25}/.vitepress/config.mts +0 -0
  17. {nene2_python-1.8.23 → nene2_python-1.8.25}/.vitepress/theme/custom.css +0 -0
  18. {nene2_python-1.8.23 → nene2_python-1.8.25}/.vitepress/theme/index.ts +0 -0
  19. {nene2_python-1.8.23 → nene2_python-1.8.25}/AGENTS.md +0 -0
  20. {nene2_python-1.8.23 → nene2_python-1.8.25}/CLAUDE.md +0 -0
  21. {nene2_python-1.8.23 → nene2_python-1.8.25}/Dockerfile +0 -0
  22. {nene2_python-1.8.23 → nene2_python-1.8.25}/LICENSE +0 -0
  23. {nene2_python-1.8.23 → nene2_python-1.8.25}/README.md +0 -0
  24. {nene2_python-1.8.23 → nene2_python-1.8.25}/alembic/README +0 -0
  25. {nene2_python-1.8.23 → nene2_python-1.8.25}/alembic/env.py +0 -0
  26. {nene2_python-1.8.23 → nene2_python-1.8.25}/alembic/script.py.mako +0 -0
  27. {nene2_python-1.8.23 → nene2_python-1.8.25}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  28. {nene2_python-1.8.23 → nene2_python-1.8.25}/alembic.ini +0 -0
  29. {nene2_python-1.8.23 → nene2_python-1.8.25}/compose.yaml +0 -0
  30. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0001-toolchain.md +0 -0
  31. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0002-clean-architecture.md +0 -0
  32. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0003-security-first.md +0 -0
  33. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0004-ai-first-design.md +0 -0
  34. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0005-logging.md +0 -0
  35. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0006-rate-limiting.md +0 -0
  36. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0009-mcp-design.md +0 -0
  37. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0010-async-use-case.md +0 -0
  38. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  39. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/de/index.md +0 -0
  40. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/de/tutorials/getting-started.md +0 -0
  41. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/explanation/architecture.md +0 -0
  42. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/explanation/design-philosophy.md +0 -0
  43. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  44. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  45. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  46. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  47. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  48. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  49. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  50. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  51. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  52. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  53. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  54. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  55. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  56. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  57. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  58. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  59. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  60. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  61. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  62. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  63. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  64. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  65. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  66. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  67. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  68. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  69. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  70. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  71. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  72. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  73. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  74. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  75. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  76. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  77. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  78. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  79. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  80. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  81. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  82. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  83. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  84. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  85. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  86. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  87. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  88. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  89. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  90. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  91. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  92. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  93. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  94. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  95. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  96. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  97. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  98. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  99. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  100. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  101. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  102. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  103. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  104. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  105. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  106. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  107. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  108. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  109. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  110. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  111. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  112. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  113. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  114. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  115. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  116. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  117. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  118. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  119. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  120. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/fr/index.md +0 -0
  121. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/fr/tutorials/getting-started.md +0 -0
  122. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/add-new-domain.md +0 -0
  123. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/async-use-case.md +0 -0
  124. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/configure-auth.md +0 -0
  125. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/middleware-stack.md +0 -0
  126. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/new-project.md +0 -0
  127. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/problem-details.md +0 -0
  128. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/run-tests.md +0 -0
  129. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/sqlalchemy-repository.md +0 -0
  130. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/how-to/validation.md +0 -0
  131. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/howto/mcp-setup.md +0 -0
  132. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/index.md +0 -0
  133. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/explanation/architecture.md +0 -0
  134. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/explanation/design-philosophy.md +0 -0
  135. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/how-to/add-new-domain.md +0 -0
  136. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/how-to/configure-auth.md +0 -0
  137. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/how-to/new-project.md +0 -0
  138. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/how-to/run-tests.md +0 -0
  139. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  140. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/howto/mcp-setup.md +0 -0
  141. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/index.md +0 -0
  142. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/reference/api.md +0 -0
  143. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/reference/configuration.md +0 -0
  144. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/reference/framework-modules.md +0 -0
  145. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/tutorials/first-domain.md +0 -0
  146. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/ja/tutorials/getting-started.md +0 -0
  147. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/pt-br/index.md +0 -0
  148. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/pt-br/tutorials/getting-started.md +0 -0
  149. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/reference/api.md +0 -0
  150. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/reference/configuration.md +0 -0
  151. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/reference/framework-modules.md +0 -0
  152. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/roadmap.md +0 -0
  153. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/todo/current.md +0 -0
  154. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/tutorials/first-domain.md +0 -0
  155. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/tutorials/getting-started.md +0 -0
  156. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/zh/index.md +0 -0
  157. {nene2_python-1.8.23 → nene2_python-1.8.25}/docs/zh/tutorials/getting-started.md +0 -0
  158. {nene2_python-1.8.23 → nene2_python-1.8.25}/package-lock.json +0 -0
  159. {nene2_python-1.8.23 → nene2_python-1.8.25}/package.json +0 -0
  160. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/__init__.py +0 -0
  161. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/__main__.py +0 -0
  162. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/app.py +0 -0
  163. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/__init__.py +0 -0
  164. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/entity.py +0 -0
  165. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/exceptions.py +0 -0
  166. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/handler.py +0 -0
  167. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/repository.py +0 -0
  168. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/sqlalchemy_repository.py +0 -0
  169. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/comment/use_case.py +0 -0
  170. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/mcp.py +0 -0
  171. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/__init__.py +0 -0
  172. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/async_use_case.py +0 -0
  173. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/entity.py +0 -0
  174. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/exceptions.py +0 -0
  175. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/handler.py +0 -0
  176. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/repository.py +0 -0
  177. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/sqlalchemy_repository.py +0 -0
  178. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/note/use_case.py +0 -0
  179. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/schema.py +0 -0
  180. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/__init__.py +0 -0
  181. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/entity.py +0 -0
  182. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/exceptions.py +0 -0
  183. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/handler.py +0 -0
  184. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/repository.py +0 -0
  185. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/sqlalchemy_repository.py +0 -0
  186. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/example/tag/use_case.py +0 -0
  187. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/__init__.py +0 -0
  188. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/__init__.py +0 -0
  189. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/api_key.py +0 -0
  190. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/bearer_token.py +0 -0
  191. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/exceptions.py +0 -0
  192. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/interfaces.py +0 -0
  193. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/auth/local_verifier.py +0 -0
  194. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/config/__init__.py +0 -0
  195. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/config/settings.py +0 -0
  196. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/__init__.py +0 -0
  197. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/exceptions.py +0 -0
  198. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/health.py +0 -0
  199. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/interfaces.py +0 -0
  200. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/sqlalchemy_executor.py +0 -0
  201. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/database/utils.py +0 -0
  202. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/http/__init__.py +0 -0
  203. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/http/health.py +0 -0
  204. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/http/pagination.py +0 -0
  205. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/http/problem_details.py +0 -0
  206. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/log/__init__.py +0 -0
  207. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/log/setup.py +0 -0
  208. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/mcp/__init__.py +0 -0
  209. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/mcp/http_client.py +0 -0
  210. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/mcp/server.py +0 -0
  211. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/__init__.py +0 -0
  212. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/domain_exception.py +0 -0
  213. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/error_handler.py +0 -0
  214. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/request_id.py +0 -0
  215. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/request_size_limit.py +0 -0
  216. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/middleware/security_headers.py +0 -0
  217. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/py.typed +0 -0
  218. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/use_case/__init__.py +0 -0
  219. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/use_case/protocols.py +0 -0
  220. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/validation/__init__.py +0 -0
  221. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/nene2/validation/exceptions.py +0 -0
  222. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/scripts/__init__.py +0 -0
  223. {nene2_python-1.8.23 → nene2_python-1.8.25}/src/scripts/export_openapi.py +0 -0
  224. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/__init__.py +0 -0
  225. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/__init__.py +0 -0
  226. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/comment/__init__.py +0 -0
  227. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/comment/test_comment_http.py +0 -0
  228. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/comment/test_comment_repository.py +0 -0
  229. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/comment/test_comment_use_case.py +0 -0
  230. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/conftest.py +0 -0
  231. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/note/__init__.py +0 -0
  232. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/note/test_async_note_use_case.py +0 -0
  233. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/note/test_list_notes.py +0 -0
  234. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/note/test_note_repository.py +0 -0
  235. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/tag/__init__.py +0 -0
  236. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/tag/test_tag_repository.py +0 -0
  237. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/tag/test_tags.py +0 -0
  238. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/test_cors.py +0 -0
  239. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/example/test_mcp.py +0 -0
  240. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/__init__.py +0 -0
  241. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/auth/__init__.py +0 -0
  242. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/auth/test_api_key.py +0 -0
  243. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/auth/test_bearer_token.py +0 -0
  244. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/auth/test_token_issuer.py +0 -0
  245. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/config/__init__.py +0 -0
  246. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/config/test_settings.py +0 -0
  247. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/database/__init__.py +0 -0
  248. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/database/test_transaction.py +0 -0
  249. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/database/test_utils.py +0 -0
  250. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/http/__init__.py +0 -0
  251. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/http/test_health.py +0 -0
  252. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/http/test_pagination.py +0 -0
  253. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/http/test_problem_details.py +0 -0
  254. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/log/__init__.py +0 -0
  255. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/log/test_setup.py +0 -0
  256. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/mcp/__init__.py +0 -0
  257. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/mcp/test_http_client.py +0 -0
  258. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/__init__.py +0 -0
  259. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_error_handler.py +0 -0
  260. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_request_id.py +0 -0
  261. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  262. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_security_headers.py +0 -0
  263. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  264. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  265. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/middleware/test_throttle.py +0 -0
  266. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/use_case/__init__.py +0 -0
  267. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/use_case/test_protocols.py +0 -0
  268. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  269. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/validation/__init__.py +0 -0
  270. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/nene2/validation/test_exceptions.py +0 -0
  271. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/scripts/__init__.py +0 -0
  272. {nene2_python-1.8.23 → nene2_python-1.8.25}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.24] — 2026-05-20
9
+
10
+ FT78 フィールドトライアル — ThrottleMiddleware の境界動作検証とドキュメント強化。
11
+
12
+ ### Changed
13
+ - `ThrottleMiddleware` クラス docstring に Fixed Window バースト特性とマルチプロセス非対応の警告を追記 (#335) (FT78)
14
+ - `setup_middlewares()` の `throttle_limit` docstring にマルチプロセス警告を追記 (#335) (FT78)
15
+ - Field trial report: `docs/field-trials/2026-05-field-trial-78.md` (FT78)
16
+
17
+ ---
18
+
19
+ ## [1.8.23] — 2026-05-20
20
+
21
+ FT77 フィールドトライアル — BearerToken + ApiKey 混在認証と include_paths 追加。
22
+
23
+ ### Added
24
+ - `BearerTokenMiddleware` / `ApiKeyAuthMiddleware` に `include_paths` パラメーターを追加 (#331) (FT77)
25
+ — プレフィックスマッチで「守りたいパス」を直接指定でき、混在認証の `exclude_paths` 二重管理を解消
26
+ - Field trial report: `docs/field-trials/2026-05-field-trial-77.md` (FT77)
27
+
28
+ ---
29
+
8
30
  ## [1.8.22] — 2026-05-20
9
31
 
10
32
  FT76 フィールドトライアル — async def + sync DB ブロッキング問題と run_in_threadpool 追加。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.23
3
+ Version: 1.8.25
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,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 カウンターバックエンドの検討(外部依存なので慎重に)
@@ -0,0 +1,186 @@
1
+ # FT79: RequestLoggingMiddleware と構造化ログ
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: リクエストログに何が含まれるか — 機密情報漏洩リスクとデバッグ適性の確認
5
+ **バージョン**: v1.8.24
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft79-request-logging/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `RequestLoggingMiddleware` と `structlog` の組み合わせを検証した。
13
+ 基本機能は期待通り動作し、セキュリティ設計(ボディをログしない)も適切。
14
+ 一方で、リクエストボディの選択的ログ記録やリクエストごとの動的コンテキスト追加といった
15
+ デバッグ用途での柔軟性の欠如が摩擦点として判明した。
16
+
17
+ ---
18
+
19
+ ## ログ出力の内容(確認済み)
20
+
21
+ ### request.received
22
+
23
+ ```json
24
+ {
25
+ "event": "request.received",
26
+ "level": "info",
27
+ "logger": "nene2.middleware.request_logging",
28
+ "method": "GET",
29
+ "path": "/api/users",
30
+ "request_id": "abc-123",
31
+ "service": "ft79-app",
32
+ "version": "1.0.0"
33
+ }
34
+ ```
35
+
36
+ ### request.completed
37
+
38
+ ```json
39
+ {
40
+ "event": "request.completed",
41
+ "level": "info",
42
+ "logger": "nene2.middleware.request_logging",
43
+ "method": "GET",
44
+ "path": "/api/users",
45
+ "status_code": 200,
46
+ "duration_ms": 1.4,
47
+ "request_id": "abc-123",
48
+ "service": "ft79-app",
49
+ "version": "1.0.0"
50
+ }
51
+ ```
52
+
53
+ ---
54
+
55
+ ## セキュリティ設計(良い点)
56
+
57
+ ### リクエストボディをログしない ✅
58
+
59
+ ```python
60
+ # POST /api/users {"name": "alice", "password": "super-secret-pw"}
61
+ # → ログには password が含まれない
62
+ ```
63
+
64
+ nene2 は `url.path` のみをログし、ボディも Cookie も Authorization ヘッダーも記録しない。
65
+ Django の `request_finished` シグナルや Flask-Login のデフォルト設定で
66
+ 機密情報がログに漏洩するケースと対照的に、nene2 はデフォルトで安全。
67
+
68
+ ### クエリパラメーターをログしない ✅
69
+
70
+ ```
71
+ GET /api/users?secret_token=hidden
72
+ ```
73
+
74
+ `url.path` は `/api/users` のみを返すため、クエリパラメーターはログに含まれない。
75
+ URLにAPIキーを含める(推奨されないが)パターンでも機密情報が漏洩しない。
76
+
77
+ ---
78
+
79
+ ## 発見した問題
80
+
81
+ ### 問題1: extra_context が静的のみ — リクエストごとの動的コンテキストが追加できない
82
+
83
+ ```python
84
+ # 現状: アプリ起動時の静的な値のみ
85
+ app.add_middleware(
86
+ RequestLoggingMiddleware,
87
+ extra_context={"service": "my-api", "version": "1.0.0"},
88
+ )
89
+ ```
90
+
91
+ ```python
92
+ # 欲しいが実現できない: リクエストごとの動的な値
93
+ # → JWT から取り出した user_id をログに含めたい
94
+ # → テナントIDをログに含めたい
95
+ app.add_middleware(
96
+ RequestLoggingMiddleware,
97
+ context_extractor=lambda request: {"user_id": get_user_id(request)}, # 非対応
98
+ )
99
+ ```
100
+
101
+ `structlog.contextvars.bind_contextvars()` を直接使えば可能だが、
102
+ その場合はミドルウェアを自作する必要がある。
103
+
104
+ ### 問題2: デバッグ時のリクエストボディログ方法が不明
105
+
106
+ 本番では不要だが、開発時にリクエストボディをログしたいケースがある。
107
+ 現状では nene2 でこれを行う方法がなく、
108
+ カスタムミドルウェアを追加するか `add_route_logging` のようなデコレーターを自作する必要がある。
109
+
110
+ ### 問題3: configure_for_testing() を使わないと caplog でキャプチャできない
111
+
112
+ ```python
113
+ # conftest.py に必須
114
+ from nene2.log import configure_for_testing
115
+ configure_for_testing()
116
+ ```
117
+
118
+ この設定を忘れると `caplog` で nene2 のログがキャプチャできず、
119
+ 「テストしてるのにログが出ない」という混乱を招く。
120
+ ドキュメントへの明示が必要。
121
+
122
+ ---
123
+
124
+ ## テスト結果(全14件パス)
125
+
126
+ ```
127
+ test_request_received_logged PASSED
128
+ test_request_completed_logged PASSED
129
+ test_method_and_path_in_log PASSED
130
+ test_status_code_in_completed_log PASSED
131
+ test_duration_in_completed_log PASSED
132
+ test_extra_context_in_log PASSED
133
+ test_health_not_logged PASSED
134
+ test_request_body_not_in_log PASSED
135
+ test_request_query_params_in_nene2_log PASSED # nene2はクエリを含まない ✅
136
+ test_error_request_still_logged PASSED
137
+ test_error_request_has_status_500 PASSED
138
+ test_friction_no_request_body_logging_api PASSED
139
+ test_friction_no_response_body_logging PASSED
140
+ test_friction_no_user_id_in_log_without_custom_context PASSED
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 摩擦ポイント一覧
146
+
147
+ | ID | 内容 | 深刻度 |
148
+ |---|---|---|
149
+ | F79-1 | extra_context が静的のみ — リクエストごとの動的コンテキスト(user_id等)が追加できない | 中 |
150
+ | F79-2 | デバッグ用のリクエストボディログ方法がない | 低 |
151
+ | F79-3 | configure_for_testing() 未設定だと caplog でログがキャプチャできない | 中 |
152
+
153
+ ---
154
+
155
+ ## 使用感(主観評価)
156
+
157
+ ### 直感性 ★★★★★
158
+
159
+ `setup_middlewares(app)` で自動的にリクエストログが有効になるのは非常に快適。
160
+ `extra_context` で追加フィールドを注入できる設計もわかりやすい。
161
+ `exclude_paths` でヘルスチェックを除外できるのも実務的。
162
+
163
+ ### 実害の深刻さ ★★☆☆☆
164
+
165
+ 機密情報をログしないデフォルト設計は本番環境で非常に重要で、これは大きな強み。
166
+ `configure_for_testing()` の存在を知らないと pytest で詰まるが、
167
+ ドキュメントに追記すれば解決する軽い問題。
168
+
169
+ ### 修正のしやすさ ★★★★☆
170
+
171
+ 動的コンテキストの追加は `context_extractor` コールバックを追加すれば実現できる。
172
+ 実装は `dispatch()` 内で `context_extractor(request)` を呼んで結果を `bind_contextvars()` に渡すだけ。
173
+
174
+ ### 総合コメント
175
+
176
+ RequestLoggingMiddleware は実用的でセキュリティ設計も適切。
177
+ `configure_for_testing()` のドキュメント強化と、
178
+ `context_extractor` パラメーターの追加でさらに実用性が高まる。
179
+ 全体的に「使っていて気持ちいい」ミドルウェア。
180
+
181
+ ---
182
+
183
+ ## 推奨アクション
184
+
185
+ 1. **Issue**: `RequestLoggingMiddleware` に `context_extractor` コールバックパラメーターを追加
186
+ 2. **Issue**: `configure_for_testing()` の使い方を README のテストセクションに追記
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.23"
3
+ version = "1.8.25"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,8 @@
1
1
  """Request logging middleware using structlog."""
2
2
 
3
+ import contextlib
3
4
  import time
5
+ from collections.abc import Callable
4
6
 
5
7
  import structlog
6
8
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
@@ -20,6 +22,21 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
20
22
  Useful for high-frequency health-check endpoints where log noise is unwanted.
21
23
  extra_context: Additional key-value pairs bound to every request log entry
22
24
  (e.g. ``{"service": "my-api", "version": "1.0.0"}``).
25
+ context_extractor: Optional callable ``(request) -> dict[str, str]`` that
26
+ returns per-request dynamic context (e.g. user ID from JWT, tenant ID).
27
+ The returned dict is merged into the log context for each request::
28
+
29
+ def get_log_context(request: Request) -> dict[str, str]:
30
+ return {"user_id": request.headers.get("X-User-Id", "anonymous")}
31
+
32
+
33
+ app.add_middleware(
34
+ RequestLoggingMiddleware,
35
+ context_extractor=get_log_context,
36
+ )
37
+
38
+ If the extractor raises an exception, it is silently skipped so that
39
+ logging failures never break request handling.
23
40
  """
24
41
 
25
42
  def __init__(
@@ -27,10 +44,12 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
27
44
  app: object,
28
45
  exclude_paths: list[str] | None = None,
29
46
  extra_context: dict[str, str] | None = None,
47
+ context_extractor: Callable[[Request], dict[str, str]] | None = None,
30
48
  ) -> None:
31
49
  super().__init__(app) # type: ignore[arg-type]
32
50
  self._exclude_paths = set(exclude_paths or [])
33
51
  self._extra_context: dict[str, str] = extra_context or {}
52
+ self._context_extractor = context_extractor
34
53
 
35
54
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
36
55
  if request.url.path in self._exclude_paths:
@@ -38,11 +57,16 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
38
57
 
39
58
  start = time.perf_counter()
40
59
  structlog.contextvars.clear_contextvars()
60
+ dynamic_context: dict[str, str] = {}
61
+ if self._context_extractor is not None:
62
+ with contextlib.suppress(Exception):
63
+ dynamic_context = self._context_extractor(request)
41
64
  structlog.contextvars.bind_contextvars(
42
65
  request_id=request_id_var.get(),
43
66
  method=request.method,
44
67
  path=request.url.path,
45
68
  **self._extra_context,
69
+ **dynamic_context,
46
70
  )
47
71
  logger.info("request.received")
48
72
  response = await call_next(request)
@@ -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__(
@@ -95,3 +95,81 @@ def test_exclude_paths_passes_requests_through() -> None:
95
95
  client = TestClient(app)
96
96
  assert client.get("/health").status_code == 200
97
97
  assert client.get("/items").status_code == 200
98
+
99
+
100
+ def test_context_extractor_adds_dynamic_context() -> None:
101
+ """context_extractor が返した値がログコンテキストに含まれる。"""
102
+ captured: list[dict] = []
103
+ app = FastAPI()
104
+ app.add_middleware(
105
+ RequestLoggingMiddleware,
106
+ context_extractor=lambda req: {"user_id": req.headers.get("X-User-Id", "anon")},
107
+ )
108
+ app.add_middleware(RequestIdMiddleware)
109
+
110
+ @app.get("/ping")
111
+ async def ping() -> JSONResponse:
112
+ captured.append(dict(structlog.contextvars.get_contextvars()))
113
+ return JSONResponse({"ok": True})
114
+
115
+ client = TestClient(app)
116
+ client.get("/ping", headers={"X-User-Id": "user-42"})
117
+ assert captured[0]["user_id"] == "user-42"
118
+
119
+
120
+ def test_context_extractor_default_is_none() -> None:
121
+ """context_extractor を省略した場合は従来通り動作する。"""
122
+ captured: list[dict] = []
123
+ app = FastAPI()
124
+ app.add_middleware(RequestLoggingMiddleware)
125
+ app.add_middleware(RequestIdMiddleware)
126
+
127
+ @app.get("/ping")
128
+ async def ping() -> JSONResponse:
129
+ captured.append(dict(structlog.contextvars.get_contextvars()))
130
+ return JSONResponse({"ok": True})
131
+
132
+ client = TestClient(app)
133
+ client.get("/ping")
134
+ assert "user_id" not in captured[0]
135
+
136
+
137
+ def test_context_extractor_exception_is_silently_skipped() -> None:
138
+ """context_extractor が例外を投げてもリクエスト処理が続行される。"""
139
+ app = FastAPI()
140
+
141
+ def _exploding_extractor(req: object) -> dict[str, str]:
142
+ raise RuntimeError("extractor failure")
143
+
144
+ app.add_middleware(RequestLoggingMiddleware, context_extractor=_exploding_extractor)
145
+ app.add_middleware(RequestIdMiddleware)
146
+
147
+ @app.get("/ping")
148
+ async def ping() -> JSONResponse:
149
+ return JSONResponse({"ok": True})
150
+
151
+ client = TestClient(app, raise_server_exceptions=False)
152
+ r = client.get("/ping")
153
+ assert r.status_code == 200 # ログの失敗がリクエスト処理を壊さない
154
+
155
+
156
+ def test_context_extractor_merges_with_extra_context() -> None:
157
+ """context_extractor と extra_context が両方指定されても正しくマージされる。"""
158
+ captured: list[dict] = []
159
+ app = FastAPI()
160
+ app.add_middleware(
161
+ RequestLoggingMiddleware,
162
+ extra_context={"service": "my-api"},
163
+ context_extractor=lambda req: {"user_id": "user-1"},
164
+ )
165
+ app.add_middleware(RequestIdMiddleware)
166
+
167
+ @app.get("/ping")
168
+ async def ping() -> JSONResponse:
169
+ captured.append(dict(structlog.contextvars.get_contextvars()))
170
+ return JSONResponse({"ok": True})
171
+
172
+ client = TestClient(app)
173
+ client.get("/ping")
174
+ assert captured[0]["service"] == "my-api"
175
+ assert captured[0]["user_id"] == "user-1"
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.8.23"
928
+ version = "1.8.25"
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