nene2-python 1.8.1__tar.gz → 1.8.3__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 (220) hide show
  1. {nene2_python-1.8.1 → nene2_python-1.8.3}/CHANGELOG.md +23 -0
  2. {nene2_python-1.8.1 → nene2_python-1.8.3}/PKG-INFO +1 -1
  3. nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-29.md +74 -0
  4. nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-30.md +66 -0
  5. nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-31.md +66 -0
  6. nene2_python-1.8.3/docs/field-trials/2026-05-field-trial-32.md +78 -0
  7. nene2_python-1.8.3/docs/how-to/async-use-case.md +121 -0
  8. {nene2_python-1.8.1 → nene2_python-1.8.3}/pyproject.toml +1 -1
  9. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/health.py +4 -0
  10. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_logging.py +10 -1
  11. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/security_headers.py +17 -3
  12. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_health.py +8 -0
  13. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_logging.py +37 -0
  14. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_security_headers.py +38 -0
  15. {nene2_python-1.8.1 → nene2_python-1.8.3}/uv.lock +1 -1
  16. {nene2_python-1.8.1 → nene2_python-1.8.3}/.env.example +0 -0
  17. {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/ci.yml +0 -0
  18. {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/docs.yml +0 -0
  19. {nene2_python-1.8.1 → nene2_python-1.8.3}/.github/workflows/publish.yml +0 -0
  20. {nene2_python-1.8.1 → nene2_python-1.8.3}/.gitignore +0 -0
  21. {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/config.mts +0 -0
  22. {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/theme/custom.css +0 -0
  23. {nene2_python-1.8.1 → nene2_python-1.8.3}/.vitepress/theme/index.ts +0 -0
  24. {nene2_python-1.8.1 → nene2_python-1.8.3}/AGENTS.md +0 -0
  25. {nene2_python-1.8.1 → nene2_python-1.8.3}/CLAUDE.md +0 -0
  26. {nene2_python-1.8.1 → nene2_python-1.8.3}/Dockerfile +0 -0
  27. {nene2_python-1.8.1 → nene2_python-1.8.3}/LICENSE +0 -0
  28. {nene2_python-1.8.1 → nene2_python-1.8.3}/README.md +0 -0
  29. {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/README +0 -0
  30. {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/env.py +0 -0
  31. {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/script.py.mako +0 -0
  32. {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  33. {nene2_python-1.8.1 → nene2_python-1.8.3}/alembic.ini +0 -0
  34. {nene2_python-1.8.1 → nene2_python-1.8.3}/compose.yaml +0 -0
  35. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0001-toolchain.md +0 -0
  36. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0002-clean-architecture.md +0 -0
  37. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0003-security-first.md +0 -0
  38. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0004-ai-first-design.md +0 -0
  39. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0005-logging.md +0 -0
  40. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0006-rate-limiting.md +0 -0
  41. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0009-mcp-design.md +0 -0
  42. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0010-async-use-case.md +0 -0
  43. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  44. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/de/index.md +0 -0
  45. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/de/tutorials/getting-started.md +0 -0
  46. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/explanation/architecture.md +0 -0
  47. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/explanation/design-philosophy.md +0 -0
  48. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  49. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  50. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  51. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  52. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  53. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  54. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  55. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  56. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  57. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  58. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  59. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  60. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  61. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  62. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  63. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  64. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  65. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  66. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  67. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  68. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  69. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  70. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  71. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  72. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  73. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  74. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  75. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  76. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/fr/index.md +0 -0
  77. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/fr/tutorials/getting-started.md +0 -0
  78. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/add-new-domain.md +0 -0
  79. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/configure-auth.md +0 -0
  80. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/new-project.md +0 -0
  81. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/problem-details.md +0 -0
  82. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/run-tests.md +0 -0
  83. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/how-to/sqlalchemy-repository.md +0 -0
  84. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/howto/mcp-setup.md +0 -0
  85. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/index.md +0 -0
  86. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/explanation/architecture.md +0 -0
  87. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/explanation/design-philosophy.md +0 -0
  88. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/add-new-domain.md +0 -0
  89. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/configure-auth.md +0 -0
  90. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/new-project.md +0 -0
  91. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/run-tests.md +0 -0
  92. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  93. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/howto/mcp-setup.md +0 -0
  94. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/index.md +0 -0
  95. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/api.md +0 -0
  96. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/configuration.md +0 -0
  97. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/reference/framework-modules.md +0 -0
  98. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/tutorials/first-domain.md +0 -0
  99. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/ja/tutorials/getting-started.md +0 -0
  100. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/pt-br/index.md +0 -0
  101. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/pt-br/tutorials/getting-started.md +0 -0
  102. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/api.md +0 -0
  103. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/configuration.md +0 -0
  104. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/reference/framework-modules.md +0 -0
  105. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/roadmap.md +0 -0
  106. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/todo/current.md +0 -0
  107. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/tutorials/first-domain.md +0 -0
  108. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/tutorials/getting-started.md +0 -0
  109. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/zh/index.md +0 -0
  110. {nene2_python-1.8.1 → nene2_python-1.8.3}/docs/zh/tutorials/getting-started.md +0 -0
  111. {nene2_python-1.8.1 → nene2_python-1.8.3}/package-lock.json +0 -0
  112. {nene2_python-1.8.1 → nene2_python-1.8.3}/package.json +0 -0
  113. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/__init__.py +0 -0
  114. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/__main__.py +0 -0
  115. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/app.py +0 -0
  116. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/__init__.py +0 -0
  117. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/entity.py +0 -0
  118. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/exceptions.py +0 -0
  119. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/handler.py +0 -0
  120. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/repository.py +0 -0
  121. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/sqlalchemy_repository.py +0 -0
  122. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/comment/use_case.py +0 -0
  123. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/mcp.py +0 -0
  124. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/__init__.py +0 -0
  125. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/async_use_case.py +0 -0
  126. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/entity.py +0 -0
  127. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/exceptions.py +0 -0
  128. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/handler.py +0 -0
  129. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/repository.py +0 -0
  130. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/sqlalchemy_repository.py +0 -0
  131. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/note/use_case.py +0 -0
  132. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/schema.py +0 -0
  133. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/__init__.py +0 -0
  134. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/entity.py +0 -0
  135. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/exceptions.py +0 -0
  136. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/handler.py +0 -0
  137. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/repository.py +0 -0
  138. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/sqlalchemy_repository.py +0 -0
  139. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/example/tag/use_case.py +0 -0
  140. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/__init__.py +0 -0
  141. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/__init__.py +0 -0
  142. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/api_key.py +0 -0
  143. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/bearer_token.py +0 -0
  144. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/exceptions.py +0 -0
  145. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/interfaces.py +0 -0
  146. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/auth/local_verifier.py +0 -0
  147. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/config/__init__.py +0 -0
  148. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/config/settings.py +0 -0
  149. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/__init__.py +0 -0
  150. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/exceptions.py +0 -0
  151. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/health.py +0 -0
  152. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/interfaces.py +0 -0
  153. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/sqlalchemy_executor.py +0 -0
  154. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/database/utils.py +0 -0
  155. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/__init__.py +0 -0
  156. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/pagination.py +0 -0
  157. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/http/problem_details.py +0 -0
  158. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/log/__init__.py +0 -0
  159. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/log/setup.py +0 -0
  160. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/__init__.py +0 -0
  161. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/http_client.py +0 -0
  162. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/mcp/server.py +0 -0
  163. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/__init__.py +0 -0
  164. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/domain_exception.py +0 -0
  165. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/error_handler.py +0 -0
  166. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_id.py +0 -0
  167. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/request_size_limit.py +0 -0
  168. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/middleware/throttle.py +0 -0
  169. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/py.typed +0 -0
  170. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/use_case/__init__.py +0 -0
  171. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/use_case/protocols.py +0 -0
  172. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/validation/__init__.py +0 -0
  173. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/nene2/validation/exceptions.py +0 -0
  174. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/scripts/__init__.py +0 -0
  175. {nene2_python-1.8.1 → nene2_python-1.8.3}/src/scripts/export_openapi.py +0 -0
  176. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/__init__.py +0 -0
  177. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/__init__.py +0 -0
  178. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/__init__.py +0 -0
  179. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_http.py +0 -0
  180. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_repository.py +0 -0
  181. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/comment/test_comment_use_case.py +0 -0
  182. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/conftest.py +0 -0
  183. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/__init__.py +0 -0
  184. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_async_note_use_case.py +0 -0
  185. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_list_notes.py +0 -0
  186. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/note/test_note_repository.py +0 -0
  187. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/__init__.py +0 -0
  188. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/test_tag_repository.py +0 -0
  189. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/tag/test_tags.py +0 -0
  190. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/test_cors.py +0 -0
  191. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/example/test_mcp.py +0 -0
  192. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/__init__.py +0 -0
  193. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/__init__.py +0 -0
  194. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_api_key.py +0 -0
  195. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_bearer_token.py +0 -0
  196. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/auth/test_token_issuer.py +0 -0
  197. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/config/__init__.py +0 -0
  198. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/config/test_settings.py +0 -0
  199. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/__init__.py +0 -0
  200. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/test_transaction.py +0 -0
  201. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/database/test_utils.py +0 -0
  202. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/__init__.py +0 -0
  203. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_pagination.py +0 -0
  204. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/http/test_problem_details.py +0 -0
  205. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/log/__init__.py +0 -0
  206. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/log/test_setup.py +0 -0
  207. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/mcp/__init__.py +0 -0
  208. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/mcp/test_http_client.py +0 -0
  209. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/__init__.py +0 -0
  210. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_error_handler.py +0 -0
  211. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_id.py +0 -0
  212. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  213. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  214. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/middleware/test_throttle.py +0 -0
  215. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/use_case/__init__.py +0 -0
  216. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/use_case/test_protocols.py +0 -0
  217. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/validation/__init__.py +0 -0
  218. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/nene2/validation/test_exceptions.py +0 -0
  219. {nene2_python-1.8.1 → nene2_python-1.8.3}/tests/scripts/__init__.py +0 -0
  220. {nene2_python-1.8.1 → nene2_python-1.8.3}/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.3] — 2026-05-20
9
+
10
+ FT31〜FT32 フィールドトライアル — HealthCheck・SecurityHeaders 改善。
11
+
12
+ ### Added
13
+ - `HealthStatus.http_status_code` プロパティ — `is_healthy` → 200、それ以外 → 503 のマッピングを提供 (FT31)
14
+ - `SecurityHeadersMiddleware` に `permissions_policy: str | None = None` パラメータを追加 (FT32)
15
+ - `SecurityHeadersMiddleware` に `hsts: str | None = None` パラメータを追加 (FT32)
16
+ - Field trial reports: `docs/field-trials/2026-05-field-trial-31.md`、`docs/field-trials/2026-05-field-trial-32.md`
17
+
18
+ ---
19
+
20
+ ## [1.8.2] — 2026-05-20
21
+
22
+ FT29〜FT30 フィールドトライアル — AsyncUseCase ドキュメント・RequestLoggingMiddleware 改善。
23
+
24
+ ### Added
25
+ - `docs/how-to/async-use-case.md` — `AsyncUseCaseProtocol` + FastAPI `Depends` の DI パターンガイドを追加 (FT29)
26
+ - `RequestLoggingMiddleware` に `extra_context: dict[str, str] | None = None` パラメータを追加し、全ログに静的フィールドを付加できるように (FT30)
27
+ - Field trial reports: `docs/field-trials/2026-05-field-trial-29.md`、`docs/field-trials/2026-05-field-trial-30.md`
28
+
29
+ ---
30
+
8
31
  ## [1.8.1] — 2026-05-20
9
32
 
10
33
  FT25〜FT28 フィールドトライアル — RequestId ヘルパー・structlog ログレベル・ThrottleMiddleware 改善。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.1
3
+ Version: 1.8.3
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,74 @@
1
+ # FT29: AsyncUseCaseProtocol 実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: `AsyncUseCaseProtocol` を使った非同期 UseCase パターンの実運用検証
5
+ **FT アプリ**: `/home/xi/docker/nene2-python-FT/ft29-async-usecase/`
6
+
7
+ ---
8
+
9
+ ## 目的
10
+
11
+ `AsyncUseCaseProtocol` の実装・FastAPI ハンドラーへの統合・並行処理の動作を検証する。
12
+
13
+ ---
14
+
15
+ ## 実施内容
16
+
17
+ - 外部 API 呼び出しを模した `FetchDataUseCase` を実装
18
+ - `asyncio.gather()` で並行実行を確認
19
+ - Protocol 適合性と isinstance() の既知制限を検証
20
+
21
+ ---
22
+
23
+ ## テスト結果
24
+
25
+ ### test_app.py(正常系・機能確認)
26
+ | テスト | 結果 |
27
+ |---|---|
28
+ | test_get_item_returns_200 | PASS |
29
+ | test_slow_endpoint_returns_200 | PASS |
30
+ | test_async_use_case_executes_correctly | PASS |
31
+ | test_async_use_case_satisfies_protocol | PASS |
32
+ | test_multiple_async_calls_are_independent | PASS |
33
+
34
+ ### test_friction.py(摩擦点確認)
35
+ | テスト | 結果 | 摩擦 |
36
+ |---|---|---|
37
+ | test_isinstance_cannot_distinguish_sync_vs_async_protocol | PASS | 既知(ADR-0010) |
38
+ | test_no_async_usecase_base_class_provided | PASS | 軽微(設計通り) |
39
+ | test_no_generic_di_container_for_async_use_cases | PASS | あり(ドキュメント不備) |
40
+
41
+ ---
42
+
43
+ ## 発見した摩擦点
44
+
45
+ ### FT29-F1: FastAPI Depends を使った AsyncUseCase DI パターンがドキュメント化されていない
46
+
47
+ **概要**: `AsyncUseCaseProtocol` を FastAPI の依存性注入と統合する標準的なパターンがない。
48
+ ユーザーは毎回自分でパターンを決める必要がある。
49
+
50
+ ```python
51
+ # ユーザーが毎回書く必要があるボイラープレート
52
+ def get_fetch_use_case() -> FetchDataUseCase:
53
+ return FetchDataUseCase()
54
+
55
+ @app.get("/items/{item_id}")
56
+ async def get_item(
57
+ item_id: int,
58
+ use_case: FetchDataUseCase = Depends(get_fetch_use_case),
59
+ ) -> JSONResponse:
60
+ result = await use_case.execute(FetchDataInput(item_id=item_id))
61
+ ...
62
+ ```
63
+
64
+ **判断**: how-to ドキュメントに DI パターンを追記する(Issue 化)。
65
+
66
+ ---
67
+
68
+ ## まとめ
69
+
70
+ `AsyncUseCaseProtocol` の基本機能(実装・FastAPI 統合・並行処理)は問題なく動作する。
71
+
72
+ 摩擦点:
73
+ 1. **AsyncUseCase + FastAPI DI パターンがドキュメント化されていない** → Issue 化・how-to に追記
74
+ 2. **isinstance() の sync/async 区別不可** → ADR-0010 記載の既知制限、修正不要
@@ -0,0 +1,66 @@
1
+ # FT30: RequestLoggingMiddleware + structlog バインディング実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: `RequestLoggingMiddleware` と structlog contextvars の実運用パターン検証
5
+ **FT アプリ**: `/home/xi/docker/nene2-python-FT/ft30-request-logging/`
6
+
7
+ ---
8
+
9
+ ## 目的
10
+
11
+ `RequestLoggingMiddleware` と structlog の contextvars 統合を実際のアプリで検証し、
12
+ カスタムフィールドの付加方法を確認する。
13
+
14
+ ---
15
+
16
+ ## 実施内容
17
+
18
+ - `RequestLoggingMiddleware` が自動バインドするコンテキスト(request_id, method, path)を確認
19
+ - 追加のコンテキストフィールドを渡す方法を検証
20
+ - `clear_contextvars()` の挙動確認
21
+
22
+ ---
23
+
24
+ ## テスト結果
25
+
26
+ ### test_app.py(正常系・機能確認)
27
+ | テスト | 結果 |
28
+ |---|---|
29
+ | test_log_test_endpoint_returns_200 | PASS |
30
+ | test_with_user_context_returns_200 | PASS |
31
+ | test_request_id_is_in_response_header | PASS |
32
+
33
+ ### test_friction.py(摩擦点確認)
34
+ | テスト | 結果 | 摩擦 |
35
+ |---|---|---|
36
+ | test_clear_contextvars_wipes_pre_bound_context | PASS | 軽微(設計上の制限) |
37
+ | test_no_way_to_add_extra_fields_to_request_log | PASS | あり |
38
+
39
+ ---
40
+
41
+ ## 発見した摩擦点
42
+
43
+ ### FT30-F1: RequestLoggingMiddleware に extra_context パラメータがない
44
+
45
+ **概要**: `service_name` や `version` などの静的フィールドを全リクエストログに含めたい場合、
46
+ `RequestLoggingMiddleware` にパラメータとして渡す方法がない。
47
+
48
+ ```python
49
+ # 期待する使い方
50
+ app.add_middleware(
51
+ RequestLoggingMiddleware,
52
+ extra_context={"service": "my-api", "version": "1.0.0"},
53
+ )
54
+ ```
55
+
56
+ **期待する解決策**: `extra_context: dict[str, str] | None = None` パラメータを追加し、
57
+ `bind_contextvars()` に追加フィールドとして渡す。
58
+
59
+ ---
60
+
61
+ ## まとめ
62
+
63
+ `RequestLoggingMiddleware` の基本機能は問題なく動作する。
64
+
65
+ 摩擦点:
66
+ 1. **`extra_context` パラメータがない** → Issue 化・修正対象
@@ -0,0 +1,66 @@
1
+ # FT31: DatabaseHealthCheck + ヘルスエンドポイント統合検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: `DatabaseHealthCheck` と `/health` エンドポイントの統合パターン検証
5
+ **FT アプリ**: `/home/xi/docker/nene2-python-FT/ft31-health-check/`
6
+
7
+ ---
8
+
9
+ ## 目的
10
+
11
+ `DatabaseHealthCheck` + `CompositeHealthCheck` を使った `/health` エンドポイントの実装パターンを検証する。
12
+
13
+ ---
14
+
15
+ ## 実施内容
16
+
17
+ - `DatabaseHealthCheck` + `CompositeHealthCheck` で `/health` を実装
18
+ - DB 接続成功時に 200、失敗時に 503 を返すパターンを確認
19
+ - フレームワークが提供すべき機能の不足を記録
20
+
21
+ ---
22
+
23
+ ## テスト結果
24
+
25
+ ### test_app.py(正常系・機能確認)
26
+ | テスト | 結果 |
27
+ |---|---|
28
+ | test_health_endpoint_returns_200_when_db_healthy | PASS |
29
+ | test_health_includes_all_checks | PASS |
30
+ | test_ping_returns_200 | PASS |
31
+ | test_health_returns_503_when_db_fails | PASS |
32
+
33
+ ### test_friction.py(摩擦点確認)
34
+ | テスト | 結果 | 摩擦 |
35
+ |---|---|---|
36
+ | test_health_endpoint_has_no_built_in_route | PASS | 軽微(how-to 推奨パターン記載で対応) |
37
+ | test_health_status_lacks_http_status_code_mapping | PASS | あり |
38
+
39
+ ---
40
+
41
+ ## 発見した摩擦点
42
+
43
+ ### FT31-F1: HealthStatus が HTTP ステータスコードのマッピングを持たない
44
+
45
+ **概要**: `/health` エンドポイントを実装するとき、
46
+ `status="ok"` → 200、`status="error"` → 503 のマッピングを毎回手動で書く必要がある。
47
+
48
+ ```python
49
+ # 毎回このボイラープレートが必要
50
+ http_status = 200 if status.is_healthy else 503
51
+ return JSONResponse({"status": status.status}, status_code=http_status)
52
+
53
+ # 期待する使い方
54
+ return JSONResponse({"status": status.status}, status_code=status.http_status_code)
55
+ ```
56
+
57
+ **期待する解決策**: `HealthStatus` に `http_status_code: int` プロパティを追加する。
58
+
59
+ ---
60
+
61
+ ## まとめ
62
+
63
+ `DatabaseHealthCheck` + `CompositeHealthCheck` の基本機能は問題なく動作する。
64
+
65
+ 摩擦点:
66
+ 1. **`HealthStatus.http_status_code` プロパティがない** → Issue 化・修正対象
@@ -0,0 +1,78 @@
1
+ # FT32: SecurityHeadersMiddleware CSP カスタマイズ実運用検証
2
+
3
+ **日付**: 2026-05-20
4
+ **テーマ**: `SecurityHeadersMiddleware` のカスタマイズ可能性検証
5
+ **FT アプリ**: `/home/xi/docker/nene2-python-FT/ft32-security-headers/`
6
+
7
+ ---
8
+
9
+ ## 目的
10
+
11
+ `SecurityHeadersMiddleware` の CSP カスタマイズ機能を実際のアプリで検証し、
12
+ ハードコードされたヘッダーの問題を記録する。
13
+
14
+ ---
15
+
16
+ ## 実施内容
17
+
18
+ - デフォルトセキュリティヘッダーの確認
19
+ - カスタム CSP の適用確認
20
+ - `extra_no_csp_paths` の動作確認
21
+ - ハードコードされたヘッダーの制限を確認
22
+
23
+ ---
24
+
25
+ ## テスト結果
26
+
27
+ ### test_app.py(正常系・機能確認)
28
+ | テスト | 結果 |
29
+ |---|---|
30
+ | test_default_security_headers_present | PASS |
31
+ | test_default_csp_is_default_src_self | PASS |
32
+ | test_custom_csp_is_applied | PASS |
33
+ | test_docs_path_has_no_csp | PASS |
34
+ | test_extra_no_csp_paths_skip_csp | PASS |
35
+
36
+ ### test_friction.py(摩擦点確認)
37
+ | テスト | 結果 | 摩擦 |
38
+ |---|---|---|
39
+ | test_permissions_policy_is_hardcoded | PASS | あり |
40
+ | test_no_hsts_header | PASS | あり |
41
+ | test_x_frame_options_is_hardcoded_to_deny | PASS | あり(軽微) |
42
+
43
+ ---
44
+
45
+ ## 発見した摩擦点
46
+
47
+ ### FT32-F1: Permissions-Policy がハードコードされている
48
+
49
+ **概要**: `geolocation=(), microphone=()` が固定値でカスタマイズできない。
50
+ 位置情報 API を使うアプリでは `geolocation=(self)` に変更できない。
51
+
52
+ **期待する解決策**: `permissions_policy: str | None = None` パラメータを追加。
53
+
54
+ ### FT32-F2: HSTS ヘッダーがない
55
+
56
+ **概要**: production 環境では `Strict-Transport-Security` を設定すべきだが、
57
+ `SecurityHeadersMiddleware` は HSTS を付与しない。
58
+ 開発環境では不要なため、オプションとして設定できるべき。
59
+
60
+ **期待する解決策**: `hsts: str | None = None` パラメータを追加。
61
+
62
+ ### FT32-F3: X-Frame-Options が DENY にハードコード(軽微)
63
+
64
+ **概要**: iframe 内表示が必要なケースで SAMEORIGIN に変更できない。
65
+ ただし DENY がセキュリティ上より安全なデフォルトであり、一般ユース向け。
66
+
67
+ **判断**: ニッチなケースのため低優先度。Issue 化はするが即座に修正しない。
68
+
69
+ ---
70
+
71
+ ## まとめ
72
+
73
+ CSP カスタマイズ機能は問題なく動作する。
74
+
75
+ 摩擦点:
76
+ 1. **Permissions-Policy ハードコード** → Issue 化・修正対象
77
+ 2. **HSTS ヘッダーなし** → Issue 化・修正対象
78
+ 3. **X-Frame-Options ハードコード** → 低優先度 Issue
@@ -0,0 +1,121 @@
1
+ # How-to: AsyncUseCase と FastAPI の統合
2
+
3
+ ## AsyncUseCaseProtocol の基本実装
4
+
5
+ `AsyncUseCaseProtocol` は Protocol(構造的部分型)なので継承不要です。
6
+ `async def execute(self, input_: I) -> O` を実装するだけで適合します。
7
+
8
+ ```python
9
+ from dataclasses import dataclass
10
+ from nene2.use_case import AsyncUseCaseProtocol
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class FetchUserInput:
15
+ user_id: int
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class FetchUserOutput:
20
+ user_id: int
21
+ name: str
22
+
23
+
24
+ class FetchUserUseCase:
25
+ async def execute(self, input_: FetchUserInput) -> FetchUserOutput:
26
+ # 外部 API 呼び出し・DB アクセスなど非同期処理
27
+ return FetchUserOutput(user_id=input_.user_id, name="Alice")
28
+ ```
29
+
30
+ ---
31
+
32
+ ## FastAPI Depends との統合
33
+
34
+ ファクトリ関数を `Depends()` に渡すのが標準パターンです。
35
+
36
+ ```python
37
+ from fastapi import Depends, FastAPI
38
+ from fastapi.responses import JSONResponse
39
+
40
+ app = FastAPI()
41
+
42
+
43
+ def get_fetch_user_use_case() -> FetchUserUseCase:
44
+ return FetchUserUseCase()
45
+
46
+
47
+ @app.get("/users/{user_id}")
48
+ async def get_user(
49
+ user_id: int,
50
+ use_case: FetchUserUseCase = Depends(get_fetch_user_use_case),
51
+ ) -> JSONResponse:
52
+ result = await use_case.execute(FetchUserInput(user_id=user_id))
53
+ return JSONResponse({"user_id": result.user_id, "name": result.name})
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 外部依存を持つ UseCase の DI
59
+
60
+ リポジトリや外部クライアントを受け取る UseCase は、依存も Depends で注入します。
61
+
62
+ ```python
63
+ class FetchUserUseCase:
64
+ def __init__(self, repository: UserRepositoryInterface) -> None:
65
+ self._repository = repository
66
+
67
+ async def execute(self, input_: FetchUserInput) -> FetchUserOutput:
68
+ user = await self._repository.find_by_id(input_.user_id)
69
+ return FetchUserOutput(user_id=user.id, name=user.name)
70
+
71
+
72
+ def get_user_repository() -> UserRepositoryInterface:
73
+ return InMemoryUserRepository()
74
+
75
+
76
+ def get_fetch_user_use_case(
77
+ repository: UserRepositoryInterface = Depends(get_user_repository),
78
+ ) -> FetchUserUseCase:
79
+ return FetchUserUseCase(repository)
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 並行実行
85
+
86
+ 複数の AsyncUseCase を並行実行するには `asyncio.gather()` を使います。
87
+
88
+ ```python
89
+ import asyncio
90
+
91
+
92
+ @app.get("/dashboard")
93
+ async def dashboard(
94
+ user_id: int,
95
+ fetch_user: FetchUserUseCase = Depends(get_fetch_user_use_case),
96
+ fetch_stats: FetchStatsUseCase = Depends(get_fetch_stats_use_case),
97
+ ) -> JSONResponse:
98
+ user, stats = await asyncio.gather(
99
+ fetch_user.execute(FetchUserInput(user_id=user_id)),
100
+ fetch_stats.execute(FetchStatsInput(user_id=user_id)),
101
+ )
102
+ return JSONResponse({"user": user.name, "stats": stats.count})
103
+ ```
104
+
105
+ ---
106
+
107
+ ## isinstance() の注意点
108
+
109
+ `AsyncUseCaseProtocol` は `@runtime_checkable` ですが、`isinstance()` は
110
+ `execute` 属性の存在のみを確認します(sync/async の区別はしません)。
111
+
112
+ ```python
113
+ # isinstance() は sync UseCase も True を返す(false positive)
114
+ isinstance(sync_use_case, AsyncUseCaseProtocol) # → True
115
+
116
+ # 正しい非同期確認方法
117
+ import inspect
118
+ inspect.iscoroutinefunction(use_case.execute) # → True/False
119
+ ```
120
+
121
+ 型安全性は `mypy --strict` の静的解析で保証します。詳細は ADR-0010 を参照してください。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.1"
3
+ version = "1.8.3"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -13,6 +13,10 @@ class HealthStatus:
13
13
  def is_healthy(self) -> bool:
14
14
  return self.status == "ok"
15
15
 
16
+ @property
17
+ def http_status_code(self) -> int:
18
+ return 200 if self.is_healthy else 503
19
+
16
20
 
17
21
  @runtime_checkable
18
22
  class HealthCheckProtocol(Protocol):
@@ -18,11 +18,19 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
18
18
  Args:
19
19
  exclude_paths: Paths to skip logging for (e.g. ``["/health"]``).
20
20
  Useful for high-frequency health-check endpoints where log noise is unwanted.
21
+ extra_context: Additional key-value pairs bound to every request log entry
22
+ (e.g. ``{"service": "my-api", "version": "1.0.0"}``).
21
23
  """
22
24
 
23
- def __init__(self, app: object, exclude_paths: list[str] | None = None) -> None:
25
+ def __init__(
26
+ self,
27
+ app: object,
28
+ exclude_paths: list[str] | None = None,
29
+ extra_context: dict[str, str] | None = None,
30
+ ) -> None:
24
31
  super().__init__(app) # type: ignore[arg-type]
25
32
  self._exclude_paths = set(exclude_paths or [])
33
+ self._extra_context: dict[str, str] = extra_context or {}
26
34
 
27
35
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
28
36
  if request.url.path in self._exclude_paths:
@@ -34,6 +42,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
34
42
  request_id=request_id_var.get(),
35
43
  method=request.method,
36
44
  path=request.url.path,
45
+ **self._extra_context,
37
46
  )
38
47
  logger.info("request.received")
39
48
  response = await call_next(request)
@@ -10,12 +10,12 @@ from starlette.requests import Request
10
10
  from starlette.responses import Response
11
11
 
12
12
  _DEFAULT_CSP = "default-src 'self'"
13
+ _DEFAULT_PERMISSIONS_POLICY = "geolocation=(), microphone=()"
13
14
 
14
- _NON_CSP_HEADERS: dict[str, str] = {
15
+ _STATIC_HEADERS: dict[str, str] = {
15
16
  "X-Content-Type-Options": "nosniff",
16
17
  "X-Frame-Options": "DENY",
17
18
  "Referrer-Policy": "strict-origin-when-cross-origin",
18
- "Permissions-Policy": "geolocation=(), microphone=()",
19
19
  }
20
20
 
21
21
  _DEFAULT_NO_CSP_PATHS: frozenset[str] = frozenset({"/docs", "/redoc", "/openapi.json"})
@@ -30,6 +30,11 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
30
30
  Args:
31
31
  csp: Custom Content-Security-Policy header value.
32
32
  Defaults to ``"default-src 'self'"`` when not specified.
33
+ permissions_policy: Custom Permissions-Policy header value.
34
+ Defaults to ``"geolocation=(), microphone=()"`` when not specified.
35
+ hsts: Strict-Transport-Security header value (e.g.
36
+ ``"max-age=31536000; includeSubDomains"``). Not set by default.
37
+ Enable only in production environments serving HTTPS.
33
38
  extra_no_csp_paths: Additional paths to skip the CSP header for.
34
39
  Useful when FastAPI is configured with custom ``docs_url`` / ``redoc_url``.
35
40
  The built-in paths ``/docs``, ``/redoc``, and ``/openapi.json`` are always included.
@@ -39,16 +44,25 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
39
44
  self,
40
45
  app: object,
41
46
  csp: str | None = None,
47
+ permissions_policy: str | None = None,
48
+ hsts: str | None = None,
42
49
  extra_no_csp_paths: list[str] | None = None,
43
50
  ) -> None:
44
51
  super().__init__(app) # type: ignore[arg-type]
45
52
  self._csp = csp if csp is not None else _DEFAULT_CSP
53
+ self._permissions_policy = (
54
+ permissions_policy if permissions_policy is not None else _DEFAULT_PERMISSIONS_POLICY
55
+ )
56
+ self._hsts = hsts
46
57
  self._no_csp_paths = _DEFAULT_NO_CSP_PATHS | frozenset(extra_no_csp_paths or [])
47
58
 
48
59
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
49
60
  response = await call_next(request)
50
- for header, value in _NON_CSP_HEADERS.items():
61
+ for header, value in _STATIC_HEADERS.items():
51
62
  response.headers[header] = value
63
+ response.headers["Permissions-Policy"] = self._permissions_policy
64
+ if self._hsts:
65
+ response.headers["Strict-Transport-Security"] = self._hsts
52
66
  if request.url.path not in self._no_csp_paths:
53
67
  response.headers["Content-Security-Policy"] = self._csp
54
68
  return response
@@ -56,3 +56,11 @@ def test_composite_with_empty_checks_returns_ok() -> None:
56
56
  result = composite.check()
57
57
  assert result.is_healthy is True
58
58
  assert result.checks == {}
59
+
60
+
61
+ def test_health_status_http_status_code_ok() -> None:
62
+ assert HealthStatus(status="ok").http_status_code == 200
63
+
64
+
65
+ def test_health_status_http_status_code_error() -> None:
66
+ assert HealthStatus(status="error").http_status_code == 503
@@ -1,5 +1,6 @@
1
1
  """Tests for RequestLoggingMiddleware."""
2
2
 
3
+ import structlog
3
4
  from fastapi import FastAPI
4
5
  from fastapi.responses import JSONResponse
5
6
  from fastapi.testclient import TestClient
@@ -41,6 +42,42 @@ def test_logging_does_not_remove_headers() -> None:
41
42
  assert "X-Request-Id" in response.headers
42
43
 
43
44
 
45
+ def test_extra_context_is_bound_to_structlog() -> None:
46
+ captured: list[dict] = []
47
+ app = FastAPI()
48
+ app.add_middleware(
49
+ RequestLoggingMiddleware,
50
+ extra_context={"service": "my-api", "version": "1.0"},
51
+ )
52
+ app.add_middleware(RequestIdMiddleware)
53
+
54
+ @app.get("/ping")
55
+ async def ping() -> JSONResponse:
56
+ captured.append(dict(structlog.contextvars.get_contextvars()))
57
+ return JSONResponse({"ok": True})
58
+
59
+ client = TestClient(app)
60
+ client.get("/ping")
61
+ assert captured[0]["service"] == "my-api"
62
+ assert captured[0]["version"] == "1.0"
63
+
64
+
65
+ def test_extra_context_default_is_empty() -> None:
66
+ captured: list[dict] = []
67
+ app = FastAPI()
68
+ app.add_middleware(RequestLoggingMiddleware) # extra_context なし
69
+ app.add_middleware(RequestIdMiddleware)
70
+
71
+ @app.get("/ping")
72
+ async def ping() -> JSONResponse:
73
+ captured.append(dict(structlog.contextvars.get_contextvars()))
74
+ return JSONResponse({"ok": True})
75
+
76
+ client = TestClient(app)
77
+ client.get("/ping")
78
+ assert "service" not in captured[0]
79
+
80
+
44
81
  def test_exclude_paths_passes_requests_through() -> None:
45
82
  """exclude_paths に指定したパスへのリクエストがミドルウェアを通過すること"""
46
83
  app = FastAPI()
@@ -80,6 +80,44 @@ def test_extra_no_csp_paths() -> None:
80
80
  assert "Content-Security-Policy" in client.get("/ping").headers
81
81
 
82
82
 
83
+ def test_custom_permissions_policy() -> None:
84
+ app = FastAPI()
85
+ app.add_middleware(
86
+ SecurityHeadersMiddleware,
87
+ permissions_policy="geolocation=(self), microphone=()",
88
+ )
89
+
90
+ @app.get("/ping")
91
+ async def ping() -> JSONResponse:
92
+ return JSONResponse({"ok": True})
93
+
94
+ client = TestClient(app)
95
+ r = client.get("/ping")
96
+ assert r.headers["Permissions-Policy"] == "geolocation=(self), microphone=()"
97
+
98
+
99
+ def test_hsts_header_when_specified() -> None:
100
+ app = FastAPI()
101
+ app.add_middleware(
102
+ SecurityHeadersMiddleware,
103
+ hsts="max-age=31536000; includeSubDomains",
104
+ )
105
+
106
+ @app.get("/ping")
107
+ async def ping() -> JSONResponse:
108
+ return JSONResponse({"ok": True})
109
+
110
+ client = TestClient(app)
111
+ r = client.get("/ping")
112
+ assert r.headers["Strict-Transport-Security"] == "max-age=31536000; includeSubDomains"
113
+
114
+
115
+ def test_no_hsts_by_default() -> None:
116
+ client = TestClient(_make_app())
117
+ r = client.get("/ping")
118
+ assert "Strict-Transport-Security" not in r.headers
119
+
120
+
83
121
  def test_default_no_csp_paths_still_work_with_extra_paths() -> None:
84
122
  app = FastAPI()
85
123
  app.add_middleware(SecurityHeadersMiddleware, extra_no_csp_paths=["/custom"])
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.8.1"
928
+ version = "1.8.3"
929
929
  source = { editable = "." }
930
930
  dependencies = [
931
931
  { name = "alembic" },
File without changes
File without changes