nene2-python 1.8.20__tar.gz → 1.8.21__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 (267) hide show
  1. {nene2_python-1.8.20 → nene2_python-1.8.21}/CHANGELOG.md +14 -0
  2. {nene2_python-1.8.20 → nene2_python-1.8.21}/CLAUDE.md +29 -0
  3. {nene2_python-1.8.20 → nene2_python-1.8.21}/PKG-INFO +1 -1
  4. nene2_python-1.8.21/docs/field-trials/2026-05-field-trial-73.md +69 -0
  5. nene2_python-1.8.21/docs/field-trials/2026-05-field-trial-74.md +76 -0
  6. nene2_python-1.8.21/docs/field-trials/2026-05-field-trial-75.md +128 -0
  7. nene2_python-1.8.21/docs/how-to/middleware-stack.md +111 -0
  8. {nene2_python-1.8.20 → nene2_python-1.8.21}/pyproject.toml +1 -1
  9. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/__init__.py +2 -0
  10. nene2_python-1.8.21/src/nene2/middleware/setup.py +142 -0
  11. nene2_python-1.8.21/tests/nene2/middleware/test_setup_middlewares.py +124 -0
  12. {nene2_python-1.8.20 → nene2_python-1.8.21}/uv.lock +1 -1
  13. {nene2_python-1.8.20 → nene2_python-1.8.21}/.env.example +0 -0
  14. {nene2_python-1.8.20 → nene2_python-1.8.21}/.github/workflows/ci.yml +0 -0
  15. {nene2_python-1.8.20 → nene2_python-1.8.21}/.github/workflows/docs.yml +0 -0
  16. {nene2_python-1.8.20 → nene2_python-1.8.21}/.github/workflows/publish.yml +0 -0
  17. {nene2_python-1.8.20 → nene2_python-1.8.21}/.gitignore +0 -0
  18. {nene2_python-1.8.20 → nene2_python-1.8.21}/.vitepress/config.mts +0 -0
  19. {nene2_python-1.8.20 → nene2_python-1.8.21}/.vitepress/theme/custom.css +0 -0
  20. {nene2_python-1.8.20 → nene2_python-1.8.21}/.vitepress/theme/index.ts +0 -0
  21. {nene2_python-1.8.20 → nene2_python-1.8.21}/AGENTS.md +0 -0
  22. {nene2_python-1.8.20 → nene2_python-1.8.21}/Dockerfile +0 -0
  23. {nene2_python-1.8.20 → nene2_python-1.8.21}/LICENSE +0 -0
  24. {nene2_python-1.8.20 → nene2_python-1.8.21}/README.md +0 -0
  25. {nene2_python-1.8.20 → nene2_python-1.8.21}/alembic/README +0 -0
  26. {nene2_python-1.8.20 → nene2_python-1.8.21}/alembic/env.py +0 -0
  27. {nene2_python-1.8.20 → nene2_python-1.8.21}/alembic/script.py.mako +0 -0
  28. {nene2_python-1.8.20 → nene2_python-1.8.21}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  29. {nene2_python-1.8.20 → nene2_python-1.8.21}/alembic.ini +0 -0
  30. {nene2_python-1.8.20 → nene2_python-1.8.21}/compose.yaml +0 -0
  31. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0001-toolchain.md +0 -0
  32. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0002-clean-architecture.md +0 -0
  33. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0003-security-first.md +0 -0
  34. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0004-ai-first-design.md +0 -0
  35. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0005-logging.md +0 -0
  36. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0006-rate-limiting.md +0 -0
  37. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0009-mcp-design.md +0 -0
  38. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0010-async-use-case.md +0 -0
  39. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  40. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/de/index.md +0 -0
  41. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/de/tutorials/getting-started.md +0 -0
  42. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/explanation/architecture.md +0 -0
  43. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/explanation/design-philosophy.md +0 -0
  44. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  45. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  46. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  47. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  48. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  49. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  50. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  51. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  52. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  53. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  54. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  55. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  56. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  57. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  58. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  59. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  60. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  61. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  62. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  63. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  64. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  65. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  66. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  67. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  68. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  69. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  70. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  71. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  72. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  73. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  74. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  75. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  76. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  77. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  78. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  79. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  80. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  81. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  82. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  83. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  84. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  85. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  86. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  87. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  88. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  89. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  90. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  91. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  92. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  93. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  94. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  95. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  96. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  97. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  98. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  99. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  100. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  101. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  102. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  103. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  104. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  105. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  106. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  107. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  108. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  109. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  110. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  111. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  112. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  113. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  114. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  115. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  116. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/fr/index.md +0 -0
  117. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/fr/tutorials/getting-started.md +0 -0
  118. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/add-new-domain.md +0 -0
  119. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/async-use-case.md +0 -0
  120. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/configure-auth.md +0 -0
  121. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/new-project.md +0 -0
  122. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/problem-details.md +0 -0
  123. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/run-tests.md +0 -0
  124. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/sqlalchemy-repository.md +0 -0
  125. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/how-to/validation.md +0 -0
  126. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/howto/mcp-setup.md +0 -0
  127. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/index.md +0 -0
  128. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/explanation/architecture.md +0 -0
  129. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/explanation/design-philosophy.md +0 -0
  130. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/how-to/add-new-domain.md +0 -0
  131. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/how-to/configure-auth.md +0 -0
  132. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/how-to/new-project.md +0 -0
  133. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/how-to/run-tests.md +0 -0
  134. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  135. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/howto/mcp-setup.md +0 -0
  136. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/index.md +0 -0
  137. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/reference/api.md +0 -0
  138. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/reference/configuration.md +0 -0
  139. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/reference/framework-modules.md +0 -0
  140. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/tutorials/first-domain.md +0 -0
  141. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/ja/tutorials/getting-started.md +0 -0
  142. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/pt-br/index.md +0 -0
  143. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/pt-br/tutorials/getting-started.md +0 -0
  144. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/reference/api.md +0 -0
  145. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/reference/configuration.md +0 -0
  146. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/reference/framework-modules.md +0 -0
  147. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/roadmap.md +0 -0
  148. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/todo/current.md +0 -0
  149. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/tutorials/first-domain.md +0 -0
  150. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/tutorials/getting-started.md +0 -0
  151. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/zh/index.md +0 -0
  152. {nene2_python-1.8.20 → nene2_python-1.8.21}/docs/zh/tutorials/getting-started.md +0 -0
  153. {nene2_python-1.8.20 → nene2_python-1.8.21}/package-lock.json +0 -0
  154. {nene2_python-1.8.20 → nene2_python-1.8.21}/package.json +0 -0
  155. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/__init__.py +0 -0
  156. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/__main__.py +0 -0
  157. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/app.py +0 -0
  158. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/__init__.py +0 -0
  159. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/entity.py +0 -0
  160. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/exceptions.py +0 -0
  161. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/handler.py +0 -0
  162. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/repository.py +0 -0
  163. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/sqlalchemy_repository.py +0 -0
  164. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/comment/use_case.py +0 -0
  165. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/mcp.py +0 -0
  166. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/__init__.py +0 -0
  167. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/async_use_case.py +0 -0
  168. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/entity.py +0 -0
  169. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/exceptions.py +0 -0
  170. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/handler.py +0 -0
  171. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/repository.py +0 -0
  172. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/sqlalchemy_repository.py +0 -0
  173. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/note/use_case.py +0 -0
  174. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/schema.py +0 -0
  175. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/__init__.py +0 -0
  176. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/entity.py +0 -0
  177. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/exceptions.py +0 -0
  178. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/handler.py +0 -0
  179. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/repository.py +0 -0
  180. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/sqlalchemy_repository.py +0 -0
  181. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/example/tag/use_case.py +0 -0
  182. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/__init__.py +0 -0
  183. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/__init__.py +0 -0
  184. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/api_key.py +0 -0
  185. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/bearer_token.py +0 -0
  186. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/exceptions.py +0 -0
  187. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/interfaces.py +0 -0
  188. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/auth/local_verifier.py +0 -0
  189. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/config/__init__.py +0 -0
  190. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/config/settings.py +0 -0
  191. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/__init__.py +0 -0
  192. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/exceptions.py +0 -0
  193. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/health.py +0 -0
  194. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/interfaces.py +0 -0
  195. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/sqlalchemy_executor.py +0 -0
  196. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/database/utils.py +0 -0
  197. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/http/__init__.py +0 -0
  198. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/http/health.py +0 -0
  199. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/http/pagination.py +0 -0
  200. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/http/problem_details.py +0 -0
  201. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/log/__init__.py +0 -0
  202. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/log/setup.py +0 -0
  203. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/mcp/__init__.py +0 -0
  204. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/mcp/http_client.py +0 -0
  205. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/mcp/server.py +0 -0
  206. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/domain_exception.py +0 -0
  207. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/error_handler.py +0 -0
  208. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/request_id.py +0 -0
  209. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/request_logging.py +0 -0
  210. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/request_size_limit.py +0 -0
  211. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/security_headers.py +0 -0
  212. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/middleware/throttle.py +0 -0
  213. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/py.typed +0 -0
  214. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/use_case/__init__.py +0 -0
  215. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/use_case/protocols.py +0 -0
  216. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/validation/__init__.py +0 -0
  217. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/nene2/validation/exceptions.py +0 -0
  218. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/scripts/__init__.py +0 -0
  219. {nene2_python-1.8.20 → nene2_python-1.8.21}/src/scripts/export_openapi.py +0 -0
  220. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/__init__.py +0 -0
  221. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/__init__.py +0 -0
  222. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/comment/__init__.py +0 -0
  223. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/comment/test_comment_http.py +0 -0
  224. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/comment/test_comment_repository.py +0 -0
  225. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/comment/test_comment_use_case.py +0 -0
  226. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/conftest.py +0 -0
  227. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/note/__init__.py +0 -0
  228. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/note/test_async_note_use_case.py +0 -0
  229. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/note/test_list_notes.py +0 -0
  230. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/note/test_note_repository.py +0 -0
  231. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/tag/__init__.py +0 -0
  232. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/tag/test_tag_repository.py +0 -0
  233. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/tag/test_tags.py +0 -0
  234. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/test_cors.py +0 -0
  235. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/example/test_mcp.py +0 -0
  236. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/__init__.py +0 -0
  237. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/auth/__init__.py +0 -0
  238. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/auth/test_api_key.py +0 -0
  239. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/auth/test_bearer_token.py +0 -0
  240. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/auth/test_token_issuer.py +0 -0
  241. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/config/__init__.py +0 -0
  242. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/config/test_settings.py +0 -0
  243. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/database/__init__.py +0 -0
  244. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/database/test_transaction.py +0 -0
  245. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/database/test_utils.py +0 -0
  246. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/http/__init__.py +0 -0
  247. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/http/test_health.py +0 -0
  248. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/http/test_pagination.py +0 -0
  249. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/http/test_problem_details.py +0 -0
  250. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/log/__init__.py +0 -0
  251. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/log/test_setup.py +0 -0
  252. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/mcp/__init__.py +0 -0
  253. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/mcp/test_http_client.py +0 -0
  254. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/__init__.py +0 -0
  255. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_error_handler.py +0 -0
  256. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_request_id.py +0 -0
  257. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_request_logging.py +0 -0
  258. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  259. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_security_headers.py +0 -0
  260. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  261. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/middleware/test_throttle.py +0 -0
  262. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/use_case/__init__.py +0 -0
  263. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/use_case/test_protocols.py +0 -0
  264. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/validation/__init__.py +0 -0
  265. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/nene2/validation/test_exceptions.py +0 -0
  266. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/scripts/__init__.py +0 -0
  267. {nene2_python-1.8.20 → nene2_python-1.8.21}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.21] — 2026-05-20
9
+
10
+ FT75 フィールドトライアル — ミドルウェアスタック順序問題の発見と根本解決。
11
+
12
+ ### Added
13
+ - `setup_middlewares(app, ...)` ユーティリティ関数を追加 (#320 #321) (FT75)
14
+ — 全ミドルウェアを正しい順序(RequestId 最外側・ErrorHandler 最内側)で一括登録し、
15
+ エラーレスポンスにも X-Request-Id とセキュリティヘッダーが確実に付与される
16
+ - `docs/how-to/middleware-stack.md` — ミドルウェア順序の解説ガイドを追加
17
+ - CLAUDE.md セクション 8 に推奨 `add_middleware` 順序を追記
18
+ - Field trial report: `docs/field-trials/2026-05-field-trial-75.md` (FT75)
19
+
20
+ ---
21
+
8
22
  ## [1.8.20] — 2026-05-20
9
23
 
10
24
  FT72 フィールドトライアル — DatabaseIntegrityException + ErrorHandlerMiddleware.install() 改善。
@@ -233,6 +233,35 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
233
233
  - `nene2.http.problem_details_response()` で RFC 9457 エラー応答
234
234
  - `nene2.http.PaginationQueryParser` でページネーション
235
235
 
236
+ ### ミドルウェアスタック順序(重要)
237
+
238
+ `app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
239
+ 直感と逆なので注意 — 「外側に置きたいものを後から追加する」。
240
+
241
+ **推奨 `add_middleware` 呼び出し順**(最初が最内側・最後が最外側):
242
+
243
+ ```python
244
+ # ✅ 正しい順序
245
+ app.add_middleware(ErrorHandlerMiddleware) # 最内側: ハンドラー例外を捕捉
246
+ app.add_middleware(RequestLoggingMiddleware) # ↑
247
+ app.add_middleware(ThrottleMiddleware, ...) # |
248
+ app.add_middleware(RequestSizeLimitMiddleware, ...) # |
249
+ app.add_middleware(SecurityHeadersMiddleware) # ↓ 全レスポンスにヘッダー付与
250
+ app.add_middleware(RequestIdMiddleware) # 最外側: 全レスポンスに X-Request-Id 付与
251
+ ```
252
+
253
+ ```python
254
+ # ❌ よくある間違い — ErrorHandler を最後(最外側)に追加
255
+ app.add_middleware(RequestIdMiddleware)
256
+ app.add_middleware(ErrorHandlerMiddleware) # 最外側にすると...
257
+ # → 500 エラーに X-Request-Id が付かない(内側のミドルウェアをバイパスするため)
258
+ # → 500 エラーにセキュリティヘッダーが付かない
259
+ ```
260
+
261
+ `ErrorHandlerMiddleware` が例外を捕捉して新しい Response を返すとき、
262
+ それより内側のミドルウェアはバイパスされる(Starlette の `BaseHTTPMiddleware` の仕様)。
263
+ `RequestIdMiddleware` と `SecurityHeadersMiddleware` は必ず **ErrorHandler より外側** に置くこと。
264
+
236
265
  ### REST 規約
237
266
  - リソース名は複数形: `/notes`, `/tags`
238
267
  - ID はパスパラメータ: `/notes/{note_id}`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.20
3
+ Version: 1.8.21
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,69 @@
1
+ # FT73: PaginationQueryParser.parse() 静的メソッド実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: `PaginationQueryParser.parse(Request)` レガシーパターンと `ErrorHandlerMiddleware.install()` の連携確認
5
+ **バージョン**: v1.8.20
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft73-pagination-parse/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ `PaginationQueryParser.parse(request)` 静的メソッド(`Depends()` を使わないパターン)を検証した。
13
+ カスタム `default_limit` / `max_limit` の動作と、
14
+ v1.8.20 で追加した `ErrorHandlerMiddleware.install()` との連携も確認した。
15
+
16
+ ---
17
+
18
+ ## 実装内容
19
+
20
+ - `GET /items` — `PaginationQueryParser.parse(request)` でデフォルト (limit=20, max=100)
21
+ - `GET /items/custom` — `parse(request, default_limit=5, max_limit=10)` でカスタム値
22
+ - `ErrorHandlerMiddleware.install(app)` を使用(v1.8.20 の新機能)
23
+ - `ValidationException` による 422 を nene2 Problem Details 形式で返す
24
+
25
+ ---
26
+
27
+ ## テスト結果
28
+
29
+ **11/11 passed**
30
+
31
+ | テスト | 結果 |
32
+ |---|---|
33
+ | `test_default_pagination_returns_20_items` | PASSED |
34
+ | `test_custom_limit_and_offset` | PASSED |
35
+ | `test_limit_too_large_returns_422` | PASSED |
36
+ | `test_limit_zero_returns_422` | PASSED |
37
+ | `test_negative_offset_returns_422` | PASSED |
38
+ | `test_non_integer_limit_returns_422` | PASSED |
39
+ | `test_non_integer_offset_returns_422` | PASSED |
40
+ | `test_custom_default_limit_applied` | PASSED |
41
+ | `test_custom_max_limit_enforced` | PASSED |
42
+ | `test_custom_max_limit_at_boundary` | PASSED |
43
+ | `test_last_page_returns_remaining_items` | PASSED |
44
+
45
+ ---
46
+
47
+ ## Friction Points
48
+
49
+ なし。
50
+
51
+ **特筆点**:
52
+ - `PaginationQueryParser.parse()` は `ValidationException` を raise するため、
53
+ `ErrorHandlerMiddleware.install(app)` と組み合わせると
54
+ 非整数値・範囲外の入力が自動的に 422 nene2 Problem Details で返る。
55
+ - `parse()` が返す `PaginationQuery` (named dataclass) は `limit` / `offset` を保持し、
56
+ `Depends()` パターンの `PaginationQueryParser` インスタンスと同じインターフェースで使える。
57
+ - `default_limit` / `max_limit` のカスタマイズが `parse()` の引数で完結するため、
58
+ ルートごとに異なるページネーション制限を設定しやすい。
59
+ - `ErrorHandlerMiddleware.install(app)` が v1.8.20 で正式追加され、
60
+ `ValidationException` の 422 フォーマット統一が一行で完了するようになった。
61
+
62
+ ---
63
+
64
+ ## 結論
65
+
66
+ `PaginationQueryParser.parse()` は `Depends()` パターンが使えないシナリオ
67
+ (例: ミドルウェアや `WebSocket` ハンドラー内)で有効な代替手段。
68
+ `default_limit` / `max_limit` のカスタマイズと `ValidationException` の自動 422 変換が
69
+ `ErrorHandlerMiddleware.install()` との組み合わせで自然に機能する。
@@ -0,0 +1,76 @@
1
+ # FT74: カスタム HealthCheckProtocol 実装の実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: ユーザー定義 `HealthCheckProtocol` / `AsyncHealthCheckProtocol` 実装と `CompositeHealthCheck` の連携
5
+ **バージョン**: v1.8.20
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft74-custom-health/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ ユーザーが `HealthCheckProtocol` を実装する実際のシナリオ(メモリ・DB・外部サービス)を
13
+ `CompositeHealthCheck` / `AsyncCompositeHealthCheck` に組み合わせて動作を検証した。
14
+
15
+ ---
16
+
17
+ ## 実装内容
18
+
19
+ ### カスタム同期ヘルスチェック
20
+ - `AlwaysOkCheck` — 常に ok を返すベースライン
21
+ - `ToggleableCheck` — テストで on/off できるトグル可能なチェック
22
+ - `MemoryCheck` — 閾値ベースのメモリ使用量チェック(psutil なしでシミュレート)
23
+
24
+ ### カスタム非同期ヘルスチェック
25
+ - `AsyncAlwaysOkCheck` — 常に ok を返す非同期版
26
+ - `AsyncToggleableCheck` — 非同期版トグルチェック
27
+
28
+ ### エンドポイント
29
+ - `GET /health` — `CompositeHealthCheck` (同期)
30
+ - `GET /health/async` — `AsyncCompositeHealthCheck` (非同期)
31
+ - `create_app(db_healthy, cache_healthy, memory_usage_pct)` でテストシナリオを切り替え
32
+
33
+ ---
34
+
35
+ ## テスト結果
36
+
37
+ **13/13 passed**
38
+
39
+ | テスト | 結果 | 種別 |
40
+ |---|---|---|
41
+ | `test_always_ok_check_returns_ok` | PASSED | 単体 |
42
+ | `test_toggleable_check_returns_error_when_unhealthy` | PASSED | 単体 |
43
+ | `test_memory_check_ok_under_threshold` | PASSED | 単体 |
44
+ | `test_memory_check_error_over_threshold` | PASSED | 単体 |
45
+ | `test_composite_ok_when_all_checks_pass` | PASSED | 単体 |
46
+ | `test_composite_error_when_any_check_fails` | PASSED | 単体 |
47
+ | `test_health_status_http_code_200_when_ok` | PASSED | 単体 |
48
+ | `test_health_status_http_code_503_when_error` | PASSED | 単体 |
49
+ | `test_sync_health_endpoint_returns_200_when_all_ok` | PASSED | HTTP 統合 |
50
+ | `test_sync_health_endpoint_returns_503_when_db_down` | PASSED | HTTP 統合 |
51
+ | `test_sync_health_endpoint_returns_503_when_memory_high` | PASSED | HTTP 統合 |
52
+ | `test_async_health_endpoint_returns_200_when_all_ok` | PASSED | HTTP 統合 |
53
+ | `test_async_health_endpoint_returns_503_when_cache_down` | PASSED | HTTP 統合 |
54
+
55
+ ---
56
+
57
+ ## Friction Points
58
+
59
+ なし。
60
+
61
+ **特筆点**:
62
+ - `HealthCheckProtocol` / `AsyncHealthCheckProtocol` はどちらも `@runtime_checkable Protocol` なので、
63
+ 特定の基底クラスを継承せずとも `check()` メソッドを実装するだけで準拠できる。
64
+ - `CompositeHealthCheck` は各チェックの `checks` dict をフラットマージするため、
65
+ 複数チェックが同一キーを持つと後のチェックが上書きする。キーの命名が重要。
66
+ - `HealthStatus.http_status_code` は 200 / 503 を自動で返すため、
67
+ HTTP ハンドラーで `status_code=status.http_status_code` とするだけで正しいステータスコードになる。
68
+ - `create_app(db_healthy=False)` の DI パターンで HTTP テストにおける「障害シミュレーション」が簡潔に書ける。
69
+
70
+ ---
71
+
72
+ ## 結論
73
+
74
+ `HealthCheckProtocol` の実装は `check() -> HealthStatus` を持つだけで完了する。
75
+ `CompositeHealthCheck` がフラットマージすることを把握した上で、
76
+ 各チェックのキー命名を一意にすれば、複数チェックの組み合わせは完全に直感的に動作する。
@@ -0,0 +1,128 @@
1
+ # FT75: ミドルウェアスタック順序依存性の実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: Starlette LIFO ミドルウェア順序の落とし穴 — エラーレスポンスにヘッダーが付かない問題
5
+ **バージョン**: v1.8.20
6
+ **FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft75-middleware-order/`
7
+
8
+ ---
9
+
10
+ ## 概要
11
+
12
+ nene2 が提供する 6 つのミドルウェアを組み合わせた際の順序依存バグを検証した。
13
+ 「ErrorHandler を最外側に」という直感が **間違い** であることを実際のテストで実証し、
14
+ 全レスポンスに X-Request-Id とセキュリティヘッダーを付与するための正しい順序を確認した。
15
+
16
+ ---
17
+
18
+ ## 発見した問題
19
+
20
+ ### Starlette BaseHTTPMiddleware の動作原理
21
+
22
+ `app.add_middleware(X)` は **LIFO**(後から追加したものが外側になる)で積まれる。
23
+
24
+ ```python
25
+ app.add_middleware(A) # 内側
26
+ app.add_middleware(B) # 外側
27
+ # スタック: B(A(Router))
28
+ # リクエスト: B → A → Router
29
+ # レスポンス: Router → A → B
30
+ ```
31
+
32
+ ### 落とし穴: ErrorHandler を最外側にすると何が起きるか
33
+
34
+ ```python
35
+ # 「直感」による書き方
36
+ app.add_middleware(RequestIdMiddleware) # 内側
37
+ app.add_middleware(SecurityHeadersMiddleware) # 内側
38
+ app.add_middleware(ErrorHandlerMiddleware) # 最外側(最後に追加)
39
+
40
+ # スタック: ErrorHandler(SecurityHeaders(RequestId(Router)))
41
+ ```
42
+
43
+ ハンドラーが例外を raise すると:
44
+ 1. `ErrorHandlerMiddleware.dispatch` が例外を捕捉
45
+ 2. `problem_details_response(...)` で **新しい Response を直接 return**
46
+ 3. この Response は内側の `SecurityHeaders` も `RequestId` も **通過しない**
47
+ 4. 結果: **500 エラーに X-Request-Id もセキュリティヘッダーも付かない**
48
+
49
+ ### 正しい順序
50
+
51
+ ErrorHandler を **最内側** に置き、RequestId と SecurityHeaders を **外側** に置く。
52
+
53
+ ```python
54
+ # 正しい書き方(最初に add するものが最内側)
55
+ app.add_middleware(ErrorHandlerMiddleware) # 最内側
56
+ app.add_middleware(RequestLoggingMiddleware)
57
+ app.add_middleware(ThrottleMiddleware, ...)
58
+ app.add_middleware(RequestSizeLimitMiddleware, ...)
59
+ app.add_middleware(SecurityHeadersMiddleware)
60
+ app.add_middleware(RequestIdMiddleware) # 最外側
61
+
62
+ # スタック: RequestId(SecurityHeaders(SizeLimit(Throttle(RequestLogging(ErrorHandler(Router))))))
63
+ # 全レスポンス(エラー含む)が SecurityHeaders と RequestId を通過する ✓
64
+ ```
65
+
66
+ ---
67
+
68
+ ## テスト結果
69
+
70
+ **10/10 passed**
71
+
72
+ | テスト | 結果 | 観察内容 |
73
+ |---|---|---|
74
+ | `test_correct_order_500_is_problem_details` | PASSED | ErrorHandler は内側でも 500 を捕捉できる |
75
+ | `test_correct_order_500_has_request_id` | PASSED | RequestId が外側 → 500 にも付く |
76
+ | `test_correct_order_500_has_security_headers` | PASSED | SecurityHeaders が外側 → 500 にも付く |
77
+ | `test_correct_order_413_is_problem_details` | PASSED | SizeLimitMiddleware は内部で直接 problem_details_response を返す |
78
+ | `test_correct_order_413_has_request_id` | PASSED | 413 にも X-Request-Id が付く |
79
+ | `test_correct_order_413_has_security_headers` | PASSED | 413 にもセキュリティヘッダーが付く |
80
+ | `test_naive_order_500_missing_request_id` | PASSED | 直感的順序だと 500 に X-Request-Id が**付かない**ことを実証 |
81
+ | `test_naive_order_500_missing_security_headers` | PASSED | 直感的順序だと 500 にセキュリティヘッダーが**付かない**ことを実証 |
82
+
83
+ ---
84
+
85
+ ## Friction Points
86
+
87
+ ### 🔴 重大: ミドルウェア推奨順序がドキュメント化されていない
88
+
89
+ `add_middleware` の呼び出し順序について nene2 のドキュメントに推奨順序が存在しない。
90
+ Starlette の LIFO 動作は非直感的であり、「ErrorHandler が最外側にあるべき」という誤解を招きやすい。
91
+
92
+ 実際の影響:
93
+ - **ErrorHandler を最外側に置く(最後に add する)と** 500 エラーに X-Request-Id が付かない
94
+ - **Security audit で「エラーレスポンスにセキュリティヘッダーがない」と指摘される**
95
+ - 本番環境で気づかずに運用してしまう可能性が高い
96
+
97
+ **推奨順序(コメント付き)をドキュメントに追加すべき。**
98
+
99
+ ---
100
+
101
+ ## 使用感(主観評価)
102
+
103
+ **直感性: ★★☆☆☆**
104
+ LIFO の仕組みを知っていても、「外側に置きたいものを後から add する」という逆転発想は
105
+ 毎回確認しないと間違える。Express.js でも同じ罠があり、FastAPI/Starlette ユーザーが
106
+ 最も頻繁にハマる問題のひとつ。
107
+
108
+ **実害の深刻さ: ★★★★★**
109
+ 単に動かない(すぐ気づく)ではなく、**動くが一部の非機能要件が欠落する**パターン。
110
+ Security headers がエラーページだけ欠落していても CI は通るし、
111
+ X-Request-Id がエラーレスポンスにないことはログ追跡するまで気づかない。
112
+
113
+ **修正のしやすさ: ★★★★★**
114
+ 順序を正しくするだけなので、原因がわかれば修正は 1 分。
115
+ 問題は「原因に気づく」のが遅いこと。
116
+
117
+ **フレームワーク側で改善できること**:
118
+ CLAUDE.md や how-to ガイドに推奨スタック順序を明記するだけで解決できる。
119
+ ミドルウェアを `ErrorHandlerMiddleware.install()` のように一括登録する
120
+ `setup_middlewares(app)` ユーティリティがあると事故を防げる。
121
+
122
+ ---
123
+
124
+ ## 結論
125
+
126
+ ミドルウェア順序のバグは **動作するが静かに壊れている** 類の問題で、
127
+ 実際の本番事故に直結しやすい。正しい順序(ErrorHandler 最内側・RequestId 最外側)を
128
+ nene2 の公式ドキュメントと CLAUDE.md に追記することを強く推奨する。
@@ -0,0 +1,111 @@
1
+ # How-To: ミドルウェアスタックの正しい設定
2
+
3
+ ## 結論(TL;DR)
4
+
5
+ ```python
6
+ # この順序で add_middleware を呼ぶ
7
+ app.add_middleware(ErrorHandlerMiddleware) # 最内側
8
+ app.add_middleware(RequestLoggingMiddleware)
9
+ app.add_middleware(ThrottleMiddleware, limit=100, window=60)
10
+ app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
11
+ app.add_middleware(SecurityHeadersMiddleware)
12
+ app.add_middleware(RequestIdMiddleware) # 最外側
13
+ ```
14
+
15
+ ---
16
+
17
+ ## なぜこの順序なのか
18
+
19
+ ### Starlette の LIFO ルール
20
+
21
+ `app.add_middleware()` は **後から追加したものが外側**(LIFO)になる。
22
+
23
+ ```
24
+ add_middleware(A) → B(A(Router))
25
+ add_middleware(B)
26
+ ```
27
+
28
+ リクエストは外側から内側へ(B → A → Router)、
29
+ レスポンスは内側から外側へ(Router → A → B)流れる。
30
+
31
+ ### ErrorHandler を最外側にすると何が壊れるか
32
+
33
+ ```python
34
+ # ❌ 間違い
35
+ app.add_middleware(RequestIdMiddleware)
36
+ app.add_middleware(ErrorHandlerMiddleware) # 最外側
37
+ # スタック: ErrorHandler(RequestId(Router))
38
+ ```
39
+
40
+ ハンドラーが例外を raise したとき:
41
+
42
+ 1. `ErrorHandlerMiddleware.dispatch` が例外を捕捉
43
+ 2. `problem_details_response(...)` で **新しい Response を直接 return**
44
+ 3. この Response は内側の `RequestId` ミドルウェアを **通過しない**
45
+ 4. 結果: **500 エラーに `X-Request-Id` が付かない**
46
+
47
+ 同じ理由で `SecurityHeadersMiddleware` が内側にあると、
48
+ エラーレスポンスにセキュリティヘッダーが付かない。
49
+
50
+ ### 正しい順序のスタック図
51
+
52
+ ```
53
+ RequestIdMiddleware ← 全レスポンス(200〜5xx)に X-Request-Id を付与
54
+ └─ SecurityHeadersMiddleware ← 全レスポンスにセキュリティヘッダーを付与
55
+ └─ RequestSizeLimitMiddleware ← 413 を直接返す(ErrorHandler 不要)
56
+ └─ ThrottleMiddleware ← 429 を直接返す(ErrorHandler 不要)
57
+ └─ RequestLoggingMiddleware
58
+ └─ ErrorHandlerMiddleware ← ハンドラー例外を 500 に変換
59
+ └─ Router (FastAPI handlers)
60
+ ```
61
+
62
+ `RequestSizeLimitMiddleware` と `ThrottleMiddleware` は自身で `problem_details_response()` を
63
+ 返すため、ErrorHandler の内外に置いても 413/429 の形式は変わらない。
64
+ ただし `X-Request-Id` が付くかどうかは `RequestIdMiddleware` の位置次第。
65
+
66
+ ---
67
+
68
+ ## 使用しないミドルウェアがある場合
69
+
70
+ 一部のミドルウェアを省略しても、残りの順序は同じルールに従う:
71
+
72
+ ```python
73
+ # ThrottleMiddleware と RequestLoggingMiddleware を省略した場合
74
+ app.add_middleware(ErrorHandlerMiddleware)
75
+ app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
76
+ app.add_middleware(SecurityHeadersMiddleware)
77
+ app.add_middleware(RequestIdMiddleware)
78
+ ```
79
+
80
+ ---
81
+
82
+ ## ErrorHandlerMiddleware.install() を使う場合
83
+
84
+ `install()` は `add_middleware` と `add_exception_handler` をまとめて行うが、
85
+ 他のミドルウェアとの順序設定は手動で行う必要がある:
86
+
87
+ ```python
88
+ # install() は最初に呼ぶ(最内側になる)
89
+ ErrorHandlerMiddleware.install(app) # 内側
90
+
91
+ # その後に他のミドルウェアを追加
92
+ app.add_middleware(RequestSizeLimitMiddleware, max_bytes=1_048_576)
93
+ app.add_middleware(SecurityHeadersMiddleware)
94
+ app.add_middleware(RequestIdMiddleware) # 外側
95
+ ```
96
+
97
+ ---
98
+
99
+ ## よくある質問
100
+
101
+ **Q: `RequestSizeLimitMiddleware` は ErrorHandler の内外どちらに置くべきか?**
102
+
103
+ A: 内外どちらでも機能するが、`RequestIdMiddleware` より内側にすることで
104
+ 413 レスポンスにも `X-Request-Id` が付く。上記の推奨順序に従えばよい。
105
+
106
+ **Q: カスタムミドルウェアはどこに追加するか?**
107
+
108
+ A: そのミドルウェアの性質による:
109
+ - 全レスポンスに何かを追加したい → `RequestIdMiddleware` の直前(外側)
110
+ - ハンドラー例外をキャッチしたい → `ErrorHandlerMiddleware` の直後(内側)
111
+ - リクエストを早期拒否したい → `RequestSizeLimitMiddleware` や `ThrottleMiddleware` の近く
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.20"
3
+ version = "1.8.21"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -6,6 +6,7 @@ from .request_id import RequestIdMiddleware, get_request_id, request_id_var
6
6
  from .request_logging import RequestLoggingMiddleware
7
7
  from .request_size_limit import RequestSizeLimitMiddleware
8
8
  from .security_headers import SecurityHeadersMiddleware
9
+ from .setup import setup_middlewares
9
10
  from .throttle import ThrottleMiddleware
10
11
 
11
12
  __all__ = [
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "RequestLoggingMiddleware",
19
20
  "RequestSizeLimitMiddleware",
20
21
  "SecurityHeadersMiddleware",
22
+ "setup_middlewares",
21
23
  "ThrottleMiddleware",
22
24
  "request_id_var",
23
25
  ]
@@ -0,0 +1,142 @@
1
+ """setup_middlewares() — register all nene2 middlewares in the correct order."""
2
+
3
+ from typing import Any
4
+
5
+ from starlette.applications import Starlette
6
+
7
+ from .domain_exception import DomainExceptionHandlerProtocol
8
+ from .error_handler import ErrorHandlerMiddleware
9
+ from .request_id import RequestIdMiddleware
10
+ from .request_logging import RequestLoggingMiddleware
11
+ from .request_size_limit import RequestSizeLimitMiddleware
12
+ from .security_headers import SecurityHeadersMiddleware
13
+ from .throttle import ThrottleMiddleware
14
+
15
+ _DEFAULT_MAX_BYTES = 1_048_576 # 1 MiB
16
+
17
+
18
+ def setup_middlewares(
19
+ app: object,
20
+ *,
21
+ debug: bool = False,
22
+ domain_handlers: list[DomainExceptionHandlerProtocol] | None = None,
23
+ enable_request_logging: bool = True,
24
+ throttle_limit: int | None = None,
25
+ throttle_window: int = 60,
26
+ throttle_path_limits: dict[str, int] | None = None,
27
+ throttle_exclude_paths: list[str] | None = None,
28
+ max_request_bytes: int = _DEFAULT_MAX_BYTES,
29
+ request_size_path_limits: dict[str, int] | None = None,
30
+ request_size_exclude_paths: list[str] | None = None,
31
+ security_headers: bool = True,
32
+ hsts: bool = False,
33
+ csp: str | None = None,
34
+ security_extra_no_csp_paths: list[str] | None = None,
35
+ ) -> None:
36
+ """Register all nene2 middlewares in the correct order.
37
+
38
+ Starlette applies ``add_middleware`` in LIFO order (last added = outermost).
39
+ This function adds middlewares in the correct sequence so that **all responses**
40
+ — including 500 errors — receive ``X-Request-Id`` and security headers.
41
+
42
+ Effective stack (outermost → innermost)::
43
+
44
+ RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
45
+
46
+ **Minimal usage** — all options have sensible defaults::
47
+
48
+ from nene2.middleware import setup_middlewares
49
+
50
+ app = FastAPI()
51
+ setup_middlewares(app)
52
+
53
+ **With customisation**::
54
+
55
+ setup_middlewares(
56
+ app,
57
+ debug=True,
58
+ throttle_limit=60, # 60 req/min
59
+ max_request_bytes=524_288, # 512 KiB
60
+ hsts=True,
61
+ )
62
+
63
+ **Without throttle** (pass ``throttle_limit=None``, the default)::
64
+
65
+ setup_middlewares(app, throttle_limit=None) # ThrottleMiddleware omitted
66
+
67
+ **Custom domain handlers**::
68
+
69
+ from nene2.middleware import SimpleDomainHandler
70
+
71
+ setup_middlewares(
72
+ app,
73
+ domain_handlers=[
74
+ SimpleDomainHandler(MyDomainError, "my-error", "My Error", 400),
75
+ ],
76
+ )
77
+
78
+ .. note::
79
+ This function calls :meth:`ErrorHandlerMiddleware.install` internally, which also
80
+ registers ``request_validation_error_handler`` so Pydantic 422 errors are formatted
81
+ as nene2 Problem Details.
82
+
83
+ Args:
84
+ app: The FastAPI (or Starlette) application instance.
85
+ debug: Expose exception messages in 500 responses (development only).
86
+ domain_handlers: Custom domain exception handlers for :class:`ErrorHandlerMiddleware`.
87
+ enable_request_logging: Whether to include :class:`RequestLoggingMiddleware`.
88
+ throttle_limit: Max requests per ``throttle_window`` seconds.
89
+ Pass ``None`` (default) to skip :class:`ThrottleMiddleware`.
90
+ throttle_window: Rate-limit window in seconds (default: 60).
91
+ throttle_path_limits: Per-path overrides for throttle limits.
92
+ throttle_exclude_paths: Paths excluded from throttling.
93
+ max_request_bytes: Maximum request body size in bytes (default: 1 MiB).
94
+ request_size_path_limits: Per-path size limits.
95
+ request_size_exclude_paths: Paths excluded from size limiting.
96
+ security_headers: Whether to include :class:`SecurityHeadersMiddleware` (default: True).
97
+ hsts: Enable Strict-Transport-Security header (default: False).
98
+ csp: Custom Content-Security-Policy value. Defaults to nene2's built-in policy.
99
+ security_extra_no_csp_paths: Additional paths to skip CSP (on top of /docs, /redoc).
100
+ """
101
+ if not isinstance(app, Starlette):
102
+ raise TypeError(f"app must be a Starlette/FastAPI instance, got {type(app)!r}")
103
+
104
+ # Add in reverse order — first added = innermost, last added = outermost.
105
+ # Desired outermost → innermost:
106
+ # RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
107
+
108
+ # 1. Innermost: ErrorHandlerMiddleware (also registers RequestValidationError handler)
109
+ ErrorHandlerMiddleware.install(app, debug=debug, domain_handlers=domain_handlers)
110
+
111
+ # 2. RequestLoggingMiddleware (optional)
112
+ if enable_request_logging:
113
+ app.add_middleware(RequestLoggingMiddleware)
114
+
115
+ # 3. ThrottleMiddleware (optional)
116
+ if throttle_limit is not None:
117
+ throttle_kwargs: dict[str, Any] = {"limit": throttle_limit, "window": throttle_window}
118
+ if throttle_path_limits:
119
+ throttle_kwargs["path_limits"] = throttle_path_limits
120
+ if throttle_exclude_paths:
121
+ throttle_kwargs["exclude_paths"] = throttle_exclude_paths
122
+ app.add_middleware(ThrottleMiddleware, **throttle_kwargs)
123
+
124
+ # 4. RequestSizeLimitMiddleware
125
+ size_kwargs: dict[str, Any] = {"max_bytes": max_request_bytes}
126
+ if request_size_path_limits:
127
+ size_kwargs["path_limits"] = request_size_path_limits
128
+ if request_size_exclude_paths:
129
+ size_kwargs["exclude_paths"] = request_size_exclude_paths
130
+ app.add_middleware(RequestSizeLimitMiddleware, **size_kwargs)
131
+
132
+ # 5. SecurityHeadersMiddleware (optional)
133
+ if security_headers:
134
+ sec_kwargs: dict[str, Any] = {"hsts": hsts}
135
+ if csp is not None:
136
+ sec_kwargs["csp"] = csp
137
+ if security_extra_no_csp_paths:
138
+ sec_kwargs["extra_no_csp_paths"] = security_extra_no_csp_paths
139
+ app.add_middleware(SecurityHeadersMiddleware, **sec_kwargs)
140
+
141
+ # 6. Outermost: RequestIdMiddleware
142
+ app.add_middleware(RequestIdMiddleware)