nene2-python 1.8.32__tar.gz → 1.8.33__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 (314) hide show
  1. {nene2_python-1.8.32 → nene2_python-1.8.33}/CHANGELOG.md +13 -0
  2. {nene2_python-1.8.32 → nene2_python-1.8.33}/PKG-INFO +1 -1
  3. nene2_python-1.8.33/docs/field-trials/2026-05-field-trial-100.md +62 -0
  4. nene2_python-1.8.33/docs/how-to/webhook.md +151 -0
  5. {nene2_python-1.8.32 → nene2_python-1.8.33}/pyproject.toml +1 -1
  6. nene2_python-1.8.33/src/nene2/cache/__init__.py +5 -0
  7. nene2_python-1.8.33/src/nene2/cache/ttl.py +57 -0
  8. nene2_python-1.8.33/tests/nene2/cache/test_ttl.py +76 -0
  9. nene2_python-1.8.33/tests/scripts/__init__.py +0 -0
  10. {nene2_python-1.8.32 → nene2_python-1.8.33}/uv.lock +1 -1
  11. {nene2_python-1.8.32 → nene2_python-1.8.33}/.env.example +0 -0
  12. {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/ci.yml +0 -0
  13. {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/docs.yml +0 -0
  14. {nene2_python-1.8.32 → nene2_python-1.8.33}/.github/workflows/publish.yml +0 -0
  15. {nene2_python-1.8.32 → nene2_python-1.8.33}/.gitignore +0 -0
  16. {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/config.mts +0 -0
  17. {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/theme/custom.css +0 -0
  18. {nene2_python-1.8.32 → nene2_python-1.8.33}/.vitepress/theme/index.ts +0 -0
  19. {nene2_python-1.8.32 → nene2_python-1.8.33}/AGENTS.md +0 -0
  20. {nene2_python-1.8.32 → nene2_python-1.8.33}/CLAUDE.md +0 -0
  21. {nene2_python-1.8.32 → nene2_python-1.8.33}/Dockerfile +0 -0
  22. {nene2_python-1.8.32 → nene2_python-1.8.33}/LICENSE +0 -0
  23. {nene2_python-1.8.32 → nene2_python-1.8.33}/README.md +0 -0
  24. {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/README +0 -0
  25. {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/env.py +0 -0
  26. {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/script.py.mako +0 -0
  27. {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  28. {nene2_python-1.8.32 → nene2_python-1.8.33}/alembic.ini +0 -0
  29. {nene2_python-1.8.32 → nene2_python-1.8.33}/compose.yaml +0 -0
  30. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0001-toolchain.md +0 -0
  31. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0002-clean-architecture.md +0 -0
  32. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0003-security-first.md +0 -0
  33. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0004-ai-first-design.md +0 -0
  34. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0005-logging.md +0 -0
  35. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0006-rate-limiting.md +0 -0
  36. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0009-mcp-design.md +0 -0
  37. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0010-async-use-case.md +0 -0
  38. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
  39. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/de/index.md +0 -0
  40. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/de/tutorials/getting-started.md +0 -0
  41. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/explanation/architecture.md +0 -0
  42. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/explanation/design-philosophy.md +0 -0
  43. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  44. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-10.md +0 -0
  45. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-11.md +0 -0
  46. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-12.md +0 -0
  47. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-13.md +0 -0
  48. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-14.md +0 -0
  49. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-15.md +0 -0
  50. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-16.md +0 -0
  51. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-17.md +0 -0
  52. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-18.md +0 -0
  53. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-19.md +0 -0
  54. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  55. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-20.md +0 -0
  56. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-21.md +0 -0
  57. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-22.md +0 -0
  58. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-23.md +0 -0
  59. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-24.md +0 -0
  60. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-25.md +0 -0
  61. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-26.md +0 -0
  62. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-27.md +0 -0
  63. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-28.md +0 -0
  64. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-29.md +0 -0
  65. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  66. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-30.md +0 -0
  67. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-31.md +0 -0
  68. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-32.md +0 -0
  69. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-33.md +0 -0
  70. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-34.md +0 -0
  71. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-35.md +0 -0
  72. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-36.md +0 -0
  73. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-37.md +0 -0
  74. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-38.md +0 -0
  75. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-39.md +0 -0
  76. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  77. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-40.md +0 -0
  78. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-41.md +0 -0
  79. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-42.md +0 -0
  80. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-43.md +0 -0
  81. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-44.md +0 -0
  82. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-45.md +0 -0
  83. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-46.md +0 -0
  84. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-47.md +0 -0
  85. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-48.md +0 -0
  86. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-49.md +0 -0
  87. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  88. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-50.md +0 -0
  89. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-51.md +0 -0
  90. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-52.md +0 -0
  91. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-53.md +0 -0
  92. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-54.md +0 -0
  93. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-55.md +0 -0
  94. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-56.md +0 -0
  95. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-57.md +0 -0
  96. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-58.md +0 -0
  97. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-59.md +0 -0
  98. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  99. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-60.md +0 -0
  100. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-61.md +0 -0
  101. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-62.md +0 -0
  102. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-63.md +0 -0
  103. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-64.md +0 -0
  104. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-65.md +0 -0
  105. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-66.md +0 -0
  106. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-67.md +0 -0
  107. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-68.md +0 -0
  108. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-69.md +0 -0
  109. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  110. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-70.md +0 -0
  111. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-71.md +0 -0
  112. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-72.md +0 -0
  113. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-73.md +0 -0
  114. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-74.md +0 -0
  115. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-75.md +0 -0
  116. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-76.md +0 -0
  117. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-77.md +0 -0
  118. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-78.md +0 -0
  119. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-79.md +0 -0
  120. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  121. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-80.md +0 -0
  122. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-81.md +0 -0
  123. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-82.md +0 -0
  124. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-83.md +0 -0
  125. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-84.md +0 -0
  126. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-85.md +0 -0
  127. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-86.md +0 -0
  128. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-87.md +0 -0
  129. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-88.md +0 -0
  130. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-89.md +0 -0
  131. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-9.md +0 -0
  132. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-90.md +0 -0
  133. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-91.md +0 -0
  134. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-92.md +0 -0
  135. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-93.md +0 -0
  136. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-94.md +0 -0
  137. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-95.md +0 -0
  138. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-96.md +0 -0
  139. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-97.md +0 -0
  140. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-98.md +0 -0
  141. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/field-trials/2026-05-field-trial-99.md +0 -0
  142. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/fr/index.md +0 -0
  143. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/fr/tutorials/getting-started.md +0 -0
  144. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/add-new-domain.md +0 -0
  145. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/async-use-case.md +0 -0
  146. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/background-tasks.md +0 -0
  147. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/configure-auth.md +0 -0
  148. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/cors.md +0 -0
  149. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/domain-events.md +0 -0
  150. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/file-upload.md +0 -0
  151. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/lifespan-and-app-state.md +0 -0
  152. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/middleware-stack.md +0 -0
  153. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/new-project.md +0 -0
  154. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/problem-details.md +0 -0
  155. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/response-patterns.md +0 -0
  156. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/run-tests.md +0 -0
  157. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/sqlalchemy-repository.md +0 -0
  158. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/streaming.md +0 -0
  159. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/how-to/validation.md +0 -0
  160. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/howto/mcp-setup.md +0 -0
  161. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/index.md +0 -0
  162. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/explanation/architecture.md +0 -0
  163. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/explanation/design-philosophy.md +0 -0
  164. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/add-new-domain.md +0 -0
  165. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/configure-auth.md +0 -0
  166. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/new-project.md +0 -0
  167. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/run-tests.md +0 -0
  168. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  169. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/howto/mcp-setup.md +0 -0
  170. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/index.md +0 -0
  171. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/api.md +0 -0
  172. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/configuration.md +0 -0
  173. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/reference/framework-modules.md +0 -0
  174. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/tutorials/first-domain.md +0 -0
  175. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/ja/tutorials/getting-started.md +0 -0
  176. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/pt-br/index.md +0 -0
  177. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/pt-br/tutorials/getting-started.md +0 -0
  178. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/api.md +0 -0
  179. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/configuration.md +0 -0
  180. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/reference/framework-modules.md +0 -0
  181. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/roadmap.md +0 -0
  182. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/todo/current.md +0 -0
  183. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/tutorials/first-domain.md +0 -0
  184. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/tutorials/getting-started.md +0 -0
  185. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/zh/index.md +0 -0
  186. {nene2_python-1.8.32 → nene2_python-1.8.33}/docs/zh/tutorials/getting-started.md +0 -0
  187. {nene2_python-1.8.32 → nene2_python-1.8.33}/package-lock.json +0 -0
  188. {nene2_python-1.8.32 → nene2_python-1.8.33}/package.json +0 -0
  189. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/__init__.py +0 -0
  190. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/__main__.py +0 -0
  191. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/app.py +0 -0
  192. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/__init__.py +0 -0
  193. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/entity.py +0 -0
  194. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/exceptions.py +0 -0
  195. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/handler.py +0 -0
  196. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/repository.py +0 -0
  197. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/sqlalchemy_repository.py +0 -0
  198. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/comment/use_case.py +0 -0
  199. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/mcp.py +0 -0
  200. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/__init__.py +0 -0
  201. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/async_use_case.py +0 -0
  202. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/entity.py +0 -0
  203. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/exceptions.py +0 -0
  204. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/handler.py +0 -0
  205. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/repository.py +0 -0
  206. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/sqlalchemy_repository.py +0 -0
  207. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/note/use_case.py +0 -0
  208. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/schema.py +0 -0
  209. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/__init__.py +0 -0
  210. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/entity.py +0 -0
  211. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/exceptions.py +0 -0
  212. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/handler.py +0 -0
  213. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/repository.py +0 -0
  214. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/sqlalchemy_repository.py +0 -0
  215. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/example/tag/use_case.py +0 -0
  216. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/__init__.py +0 -0
  217. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/__init__.py +0 -0
  218. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/api_key.py +0 -0
  219. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/bearer_token.py +0 -0
  220. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/deps.py +0 -0
  221. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/exceptions.py +0 -0
  222. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/interfaces.py +0 -0
  223. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/auth/local_verifier.py +0 -0
  224. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/config/__init__.py +0 -0
  225. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/config/settings.py +0 -0
  226. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/__init__.py +0 -0
  227. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/exceptions.py +0 -0
  228. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/health.py +0 -0
  229. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/interfaces.py +0 -0
  230. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/sqlalchemy_executor.py +0 -0
  231. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/database/utils.py +0 -0
  232. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/__init__.py +0 -0
  233. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/etag.py +0 -0
  234. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/health.py +0 -0
  235. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/pagination.py +0 -0
  236. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/http/problem_details.py +0 -0
  237. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/log/__init__.py +0 -0
  238. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/log/setup.py +0 -0
  239. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/__init__.py +0 -0
  240. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/http_client.py +0 -0
  241. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/mcp/server.py +0 -0
  242. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/__init__.py +0 -0
  243. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/domain_exception.py +0 -0
  244. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/error_handler.py +0 -0
  245. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_id.py +0 -0
  246. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_logging.py +0 -0
  247. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/request_size_limit.py +0 -0
  248. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/security_headers.py +0 -0
  249. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/setup.py +0 -0
  250. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/middleware/throttle.py +0 -0
  251. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/py.typed +0 -0
  252. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/security/__init__.py +0 -0
  253. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/security/webhook.py +0 -0
  254. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/use_case/__init__.py +0 -0
  255. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/use_case/protocols.py +0 -0
  256. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/validation/__init__.py +0 -0
  257. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/nene2/validation/exceptions.py +0 -0
  258. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/scripts/__init__.py +0 -0
  259. {nene2_python-1.8.32 → nene2_python-1.8.33}/src/scripts/export_openapi.py +0 -0
  260. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/__init__.py +0 -0
  261. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/__init__.py +0 -0
  262. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/__init__.py +0 -0
  263. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_http.py +0 -0
  264. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_repository.py +0 -0
  265. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/comment/test_comment_use_case.py +0 -0
  266. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/conftest.py +0 -0
  267. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/__init__.py +0 -0
  268. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_async_note_use_case.py +0 -0
  269. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_list_notes.py +0 -0
  270. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/note/test_note_repository.py +0 -0
  271. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/__init__.py +0 -0
  272. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/test_tag_repository.py +0 -0
  273. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/tag/test_tags.py +0 -0
  274. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/test_cors.py +0 -0
  275. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/example/test_mcp.py +0 -0
  276. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/__init__.py +0 -0
  277. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/__init__.py +0 -0
  278. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_api_key.py +0 -0
  279. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_bearer_token.py +0 -0
  280. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_make_require_auth.py +0 -0
  281. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/auth/test_token_issuer.py +0 -0
  282. {nene2_python-1.8.32/tests/nene2/config → nene2_python-1.8.33/tests/nene2/cache}/__init__.py +0 -0
  283. {nene2_python-1.8.32/tests/nene2/database → nene2_python-1.8.33/tests/nene2/config}/__init__.py +0 -0
  284. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/config/test_settings.py +0 -0
  285. {nene2_python-1.8.32/tests/nene2/http → nene2_python-1.8.33/tests/nene2/database}/__init__.py +0 -0
  286. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/database/test_transaction.py +0 -0
  287. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/database/test_utils.py +0 -0
  288. {nene2_python-1.8.32/tests/nene2/log → nene2_python-1.8.33/tests/nene2/http}/__init__.py +0 -0
  289. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_etag.py +0 -0
  290. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_health.py +0 -0
  291. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_pagination.py +0 -0
  292. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/http/test_problem_details.py +0 -0
  293. {nene2_python-1.8.32/tests/nene2/mcp → nene2_python-1.8.33/tests/nene2/log}/__init__.py +0 -0
  294. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/log/test_setup.py +0 -0
  295. {nene2_python-1.8.32/tests/nene2/middleware → nene2_python-1.8.33/tests/nene2/mcp}/__init__.py +0 -0
  296. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/mcp/test_http_client.py +0 -0
  297. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/mcp/test_server.py +0 -0
  298. {nene2_python-1.8.32/tests/nene2/security → nene2_python-1.8.33/tests/nene2/middleware}/__init__.py +0 -0
  299. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_error_handler.py +0 -0
  300. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_id.py +0 -0
  301. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_logging.py +0 -0
  302. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  303. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_security_headers.py +0 -0
  304. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
  305. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
  306. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/middleware/test_throttle.py +0 -0
  307. {nene2_python-1.8.32/tests/nene2/use_case → nene2_python-1.8.33/tests/nene2/security}/__init__.py +0 -0
  308. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/security/test_webhook.py +0 -0
  309. {nene2_python-1.8.32/tests/nene2/validation → nene2_python-1.8.33/tests/nene2/use_case}/__init__.py +0 -0
  310. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/use_case/test_protocols.py +0 -0
  311. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
  312. {nene2_python-1.8.32/tests/scripts → nene2_python-1.8.33/tests/nene2/validation}/__init__.py +0 -0
  313. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/nene2/validation/test_exceptions.py +0 -0
  314. {nene2_python-1.8.32 → nene2_python-1.8.33}/tests/scripts/test_export_openapi.py +0 -0
@@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [1.8.33] — 2026-05-20
9
+
10
+ FT100 フィールドトライアル — In-memory TTL レスポンスキャッシュパターン検証と nene2.cache モジュール追加。
11
+
12
+ ### Added
13
+ - `nene2.cache` モジュールを新設 (#409) (FT100)
14
+ - `TtlCache[V]` — TTL 付きインメモリキャッシュ(ジェネリック型)
15
+ - `time.monotonic()` ベースの TTL で NTP 調整の影響を受けない
16
+ - `get()`, `set()`, `delete()`, `clear()`, `size()` API
17
+ - Field trial report: `docs/field-trials/2026-05-field-trial-100.md` (FT100)
18
+
19
+ ---
20
+
8
21
  ## [1.8.32] — 2026-05-20
9
22
 
10
23
  FT99 フィールドトライアル — Webhook HMAC-SHA256 署名検証パターン検証と nene2.security モジュール追加。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nene2-python
3
- Version: 1.8.32
3
+ Version: 1.8.33
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,62 @@
1
+ # Field Trial 100: In-memory TTL レスポンスキャッシュ
2
+
3
+ ## テーマ
4
+
5
+ 重い処理の結果を TTL 付きインメモリキャッシュに格納し、重複リクエストにキャッシュから応答するパターンを nene2 上で実装する。
6
+
7
+ ## 実施内容
8
+
9
+ `/home/xi/docker/nene2-python-FT/ft100-response-cache/` に以下を実装:
10
+
11
+ - `TtlCache` データクラス(TTL 付き辞書)
12
+ - FastAPI lifespan でキャッシュを初期化・破棄
13
+ - `Depends(get_cache)` でハンドラーに注入
14
+ - キャッシュヒット/ミス判定、TTL 失効テスト
15
+
16
+ ## テスト結果
17
+
18
+ 全 7 テスト通過。
19
+
20
+ ## Friction Points
21
+
22
+ ### FP1: nene2 に TTL キャッシュユーティリティがない
23
+
24
+ **状況**: キャッシュは Web API の基本パターンだが、nene2 に `TtlCache` のようなユーティリティが存在しない。`TtlCache` を毎回自前実装する必要がある。
25
+
26
+ **影響**: 開発者がスレッドセーフでないキャッシュを実装してしまうリスク。また asyncio 環境での並行アクセスの考慮が漏れやすい(Python GIL により dict 操作自体はアトミックだが、get-then-set パターンは競合する)。
27
+
28
+ **期待する API**:
29
+ ```python
30
+ from nene2.cache import TtlCache
31
+
32
+ cache = TtlCache(ttl_seconds=60.0)
33
+ cache.set("key", value)
34
+ value = cache.get("key") # None if expired
35
+ ```
36
+
37
+ ### FP2: lifespan + グローバル変数パターンに型エラーが出る
38
+
39
+ **状況**: キャッシュを lifespan で初期化してグローバル変数に格納するパターンで、`async def lifespan(app: FastAPI)` の型注釈に `type: ignore[type-arg]` が必要。
40
+
41
+ ```python
42
+ _cache: TtlCache | None = None
43
+
44
+ @asynccontextmanager
45
+ async def lifespan(app: FastAPI): # type: ignore[type-arg] が必要
46
+ global _cache
47
+ _cache = TtlCache(ttl_seconds=60.0)
48
+ yield
49
+ ```
50
+
51
+ `app.state` を使うパターンの方がよりクリーンだが、`app.state` の型付けも `app.state.cache` のアクセスで `Any` になる。
52
+
53
+ ### FP3: `app.state` でのキャッシュ管理がドキュメント化されていない
54
+
55
+ **状況**: `lifespan-and-app-state.md` では `app.state.db` の例はあるが、キャッシュを `app.state` に格納するパターン(`request.app.state.cache`)の説明がない。
56
+
57
+ **影響**: グローバル変数を使う開発者が多く、テスト時のリセットが困難になる。
58
+
59
+ ## まとめ
60
+
61
+ キャッシュユーティリティ追加は中程度の価値あり(FP1)。FP2・FP3 はドキュメント摩擦。
62
+ 今回は FP1 を `nene2.cache` モジュールとして実装し、Issue を起票する。
@@ -0,0 +1,151 @@
1
+ # How-to: Webhook 受信と HMAC-SHA256 署名検証
2
+
3
+ GitHub や Stripe などの外部サービスから Webhook を受信し、HMAC-SHA256 署名を検証するパターンを説明する。
4
+
5
+ ---
6
+
7
+ ## 1. 基本パターン(GitHub 方式)
8
+
9
+ GitHub は `X-Hub-Signature-256: sha256=<hex>` ヘッダーで署名を送る。
10
+
11
+ ```python
12
+ from fastapi import FastAPI, Request
13
+ from fastapi.responses import JSONResponse
14
+
15
+ from nene2.security import verify_hmac_signature
16
+
17
+ WEBHOOK_SECRET = "your-secret-key"
18
+
19
+ app = FastAPI()
20
+
21
+ @app.post("/webhooks/github")
22
+ async def github_webhook(request: Request) -> JSONResponse:
23
+ signature = request.headers.get("X-Hub-Signature-256", "")
24
+ body = await request.body()
25
+
26
+ if not signature:
27
+ return JSONResponse({"error": "Missing signature"}, status_code=400)
28
+
29
+ if not verify_hmac_signature(body, WEBHOOK_SECRET, signature, prefix="sha256="):
30
+ return JSONResponse({"error": "Invalid signature"}, status_code=401)
31
+
32
+ payload = await request.json()
33
+ event = request.headers.get("X-GitHub-Event", "unknown")
34
+ # ... イベント処理
35
+ return JSONResponse({"status": "received", "event": event})
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 2. Stripe 方式(timestamp 付き署名)
41
+
42
+ Stripe は `Stripe-Signature: t=<timestamp>,v1=<hex>` 形式で送る。timestamp + body を HMAC する。
43
+
44
+ ```python
45
+ import hashlib
46
+ import hmac
47
+ import time
48
+
49
+ @app.post("/webhooks/stripe")
50
+ async def stripe_webhook(request: Request) -> JSONResponse:
51
+ stripe_sig = request.headers.get("Stripe-Signature", "")
52
+ body = await request.body()
53
+
54
+ if not stripe_sig:
55
+ return JSONResponse({"error": "Missing Stripe-Signature"}, status_code=400)
56
+
57
+ parts = dict(item.split("=", 1) for item in stripe_sig.split(",") if "=" in item)
58
+ timestamp = parts.get("t", "")
59
+ v1_sig = parts.get("v1", "")
60
+
61
+ # Stripe 方式: "timestamp." + body を HMAC する
62
+ signed_payload = f"{timestamp}.".encode() + body
63
+ if not verify_hmac_signature(signed_payload, WEBHOOK_SECRET, v1_sig):
64
+ return JSONResponse({"error": "Invalid signature"}, status_code=401)
65
+
66
+ payload = await request.json()
67
+ return JSONResponse({"status": "received", "type": payload.get("type")})
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 3. `await request.body()` → `await request.json()` の二重読み取り
73
+
74
+ 署名検証では生バイト(`body()`)が必要だが、その後 JSON としてもパースしたい。
75
+ FastAPI はボディを内部でキャッシュするので、両方呼び出せる。
76
+
77
+ ```python
78
+ @app.post("/webhooks/example")
79
+ async def handler(request: Request) -> JSONResponse:
80
+ # ✅ body() を先に呼んでも json() は正常に動く
81
+ body = await request.body() # 生バイト取得(署名検証用)
82
+ payload = await request.json() # JSON パース(内部キャッシュを使う)
83
+ return JSONResponse({"size": len(body), "action": payload.get("action")})
84
+ ```
85
+
86
+ `json.loads(body)` でも動作するが、`await request.json()` の方が Pydantic モデル変換と統一感がある。
87
+
88
+ ---
89
+
90
+ ## 4. `verify_hmac_signature()` の API
91
+
92
+ ```python
93
+ from nene2.security import verify_hmac_signature
94
+
95
+ verify_hmac_signature(
96
+ body: bytes, # 検証するバイト列
97
+ secret: str, # 共有シークレット
98
+ signature: str, # 検証対象の署名文字列(prefix 込み可)
99
+ *,
100
+ prefix: str = "", # 署名の prefix(例: "sha256=")
101
+ ) -> bool
102
+ ```
103
+
104
+ `hmac.compare_digest()` で timing attack 対策済み。署名の比較に `==` を使わないこと。
105
+
106
+ ---
107
+
108
+ ## 5. BearerTokenMiddleware との使い分け
109
+
110
+ | パターン | 認証方法 | nene2 サポート |
111
+ |---|---|---|
112
+ | API クライアント認証 | `Authorization: Bearer <token>` | `BearerTokenMiddleware` |
113
+ | Webhook 署名検証 | リクエストボディ + シークレット | `verify_hmac_signature()` |
114
+
115
+ Webhook エンドポイントは `BearerTokenMiddleware` の `exclude_paths` に加えて、
116
+ 自前の署名検証を行う。ミドルウェアでは raw body を読む関係で `BearerTokenMiddleware` は使用できない。
117
+
118
+ ```python
119
+ from nene2.middleware import BearerTokenMiddleware
120
+
121
+ app.add_middleware(
122
+ BearerTokenMiddleware,
123
+ verifier=token_verifier,
124
+ exclude_paths=["/webhooks/"], # Webhook エンドポイントを除外
125
+ )
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 6. テスト
131
+
132
+ ```python
133
+ import hashlib
134
+ import hmac
135
+
136
+ def make_github_sig(body: bytes, secret: str) -> str:
137
+ return "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
138
+
139
+ def test_webhook_valid() -> None:
140
+ payload = b'{"action": "opened"}'
141
+ r = client.post(
142
+ "/webhooks/github",
143
+ content=payload,
144
+ headers={
145
+ "Content-Type": "application/json",
146
+ "X-Hub-Signature-256": make_github_sig(payload, "your-secret-key"),
147
+ "X-GitHub-Event": "issues",
148
+ },
149
+ )
150
+ assert r.status_code == 200
151
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.8.32"
3
+ version = "1.8.33"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,5 @@
1
+ """In-memory TTL キャッシュユーティリティ."""
2
+
3
+ from .ttl import TtlCache
4
+
5
+ __all__ = ["TtlCache"]
@@ -0,0 +1,57 @@
1
+ """TTL 付きインメモリキャッシュ."""
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class _Entry[V]:
9
+ value: V
10
+ expires_at: float
11
+
12
+
13
+ @dataclass
14
+ class TtlCache[V]:
15
+ """TTL 付きインメモリキャッシュ。
16
+
17
+ asyncio コンテキストでの利用を想定。Python GIL により dict 操作は
18
+ アトミックで安全だが、get-then-set のような複合操作は排他しない。
19
+
20
+ Args:
21
+ ttl_seconds: キャッシュエントリの生存時間(秒)。
22
+
23
+ Example:
24
+ cache: TtlCache[dict[str, object]] = TtlCache(ttl_seconds=60.0)
25
+ cache.set("key", {"data": 42})
26
+ value = cache.get("key")
27
+ """
28
+
29
+ ttl_seconds: float
30
+ _store: dict[str, _Entry[V]] = field(default_factory=dict, init=False, repr=False)
31
+
32
+ def get(self, key: str) -> V | None:
33
+ """キーに対応する値を返す。TTL 切れの場合は None を返してエントリを削除する。"""
34
+ entry = self._store.get(key)
35
+ if entry is None:
36
+ return None
37
+ if time.monotonic() > entry.expires_at:
38
+ del self._store[key]
39
+ return None
40
+ return entry.value
41
+
42
+ def set(self, key: str, value: V) -> None:
43
+ """キーと値を TTL 付きで格納する。"""
44
+ self._store[key] = _Entry(value=value, expires_at=time.monotonic() + self.ttl_seconds)
45
+
46
+ def delete(self, key: str) -> None:
47
+ """キーに対応するエントリを削除する。存在しなくても例外は発生しない。"""
48
+ self._store.pop(key, None)
49
+
50
+ def clear(self) -> None:
51
+ """すべてのエントリを削除する。"""
52
+ self._store.clear()
53
+
54
+ def size(self) -> int:
55
+ """TTL 切れを除いた有効なエントリ数を返す。"""
56
+ now = time.monotonic()
57
+ return sum(1 for e in self._store.values() if e.expires_at > now)
@@ -0,0 +1,76 @@
1
+ """Tests for nene2.cache.TtlCache."""
2
+
3
+ import time
4
+
5
+ import pytest
6
+
7
+ from nene2.cache import TtlCache
8
+
9
+
10
+ def test_set_and_get_returns_value() -> None:
11
+ cache = TtlCache(ttl_seconds=60.0)
12
+ cache.set("key", {"data": 42})
13
+ assert cache.get("key") == {"data": 42}
14
+
15
+
16
+ def test_get_missing_key_returns_none() -> None:
17
+ cache = TtlCache(ttl_seconds=60.0)
18
+ assert cache.get("nonexistent") is None
19
+
20
+
21
+ def test_expired_entry_returns_none() -> None:
22
+ cache = TtlCache(ttl_seconds=0.01)
23
+ cache.set("key", "value")
24
+ time.sleep(0.02)
25
+ assert cache.get("key") is None
26
+
27
+
28
+ def test_expired_entry_is_removed_from_store() -> None:
29
+ cache = TtlCache(ttl_seconds=0.01)
30
+ cache.set("key", "value")
31
+ time.sleep(0.02)
32
+ cache.get("key") # TTL 切れでエントリ削除
33
+ assert cache.size() == 0
34
+
35
+
36
+ def test_delete_removes_entry() -> None:
37
+ cache = TtlCache(ttl_seconds=60.0)
38
+ cache.set("key", "value")
39
+ cache.delete("key")
40
+ assert cache.get("key") is None
41
+
42
+
43
+ def test_delete_missing_key_does_not_raise() -> None:
44
+ cache = TtlCache(ttl_seconds=60.0)
45
+ cache.delete("nonexistent") # should not raise
46
+
47
+
48
+ def test_clear_removes_all_entries() -> None:
49
+ cache = TtlCache(ttl_seconds=60.0)
50
+ cache.set("a", 1)
51
+ cache.set("b", 2)
52
+ cache.clear()
53
+ assert cache.size() == 0
54
+
55
+
56
+ def test_size_excludes_expired_entries() -> None:
57
+ cache = TtlCache(ttl_seconds=0.01)
58
+ cache.set("expired", "value")
59
+ time.sleep(0.02)
60
+ cache.set("fresh", "value")
61
+ assert cache.size() == 1
62
+
63
+
64
+ def test_overwrite_resets_ttl() -> None:
65
+ cache = TtlCache(ttl_seconds=60.0)
66
+ cache.set("key", "old")
67
+ cache.set("key", "new")
68
+ assert cache.get("key") == "new"
69
+
70
+
71
+ @pytest.mark.parametrize("value", [None, 0, "", [], {}, False])
72
+ def test_falsy_values_are_stored_correctly(value: object) -> None:
73
+ cache = TtlCache(ttl_seconds=60.0)
74
+ cache.set("key", value)
75
+ assert cache.get("key") == value
76
+ assert cache.size() == 1
File without changes
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "nene2-python"
928
- version = "1.8.32"
928
+ version = "1.8.33"
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