tina4-python 3.13.21__tar.gz → 3.13.23__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 (157) hide show
  1. {tina4_python-3.13.21 → tina4_python-3.13.23}/PKG-INFO +1 -1
  2. {tina4_python-3.13.21 → tina4_python-3.13.23}/pyproject.toml +1 -1
  3. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/CLAUDE.md +2 -2
  4. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/server.py +9 -0
  6. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/connection.py +47 -3
  7. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/session/__init__.py +1 -1
  8. {tina4_python-3.13.21 → tina4_python-3.13.23}/.gitignore +0 -0
  9. {tina4_python-3.13.21 → tina4_python-3.13.23}/README.md +0 -0
  10. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/HtmlElement.py +0 -0
  11. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/Testing.py +0 -0
  12. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/env.py +0 -0
  46. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/frond/FROND.md +0 -0
  47. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/frond/__init__.py +0 -0
  48. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/frond/engine.py +0 -0
  49. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/auth/meta.json +0 -0
  50. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  51. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/database/meta.json +0 -0
  52. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  53. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/error-overlay/meta.json +0 -0
  54. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  55. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/orm/meta.json +0 -0
  56. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  57. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  58. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/queue/meta.json +0 -0
  59. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  60. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/rest-api/meta.json +0 -0
  61. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  62. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/templates/meta.json +0 -0
  63. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  64. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  65. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/graphql/__init__.py +0 -0
  66. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/i18n/__init__.py +0 -0
  67. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/mcp/__init__.py +0 -0
  68. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/mcp/protocol.py +0 -0
  69. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/mcp/tools.py +0 -0
  70. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/messenger/__init__.py +0 -0
  71. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/migration/__init__.py +0 -0
  72. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/migration/runner.py +0 -0
  73. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/orm/model.py +0 -0
  76. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/__feedback/widget.js +0 -0
  77. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/css/tina4.css +0 -0
  78. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/css/tina4.min.css +0 -0
  79. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/favicon.ico +0 -0
  80. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/images/logo.svg +0 -0
  81. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  82. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/frond.js +0 -0
  83. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  85. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  86. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/__init__.py +0 -0
  102. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  103. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_badges.scss +0 -0
  104. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  105. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_cards.scss +0 -0
  106. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_forms.scss +0 -0
  107. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_grid.scss +0 -0
  108. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_modals.scss +0 -0
  109. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_nav.scss +0 -0
  110. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_reset.scss +0 -0
  111. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_tables.scss +0 -0
  112. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_typography.scss +0 -0
  113. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  114. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/_variables.scss +0 -0
  115. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/base.scss +0 -0
  116. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/colors.scss +0 -0
  117. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/scss/tina4css/tina4.scss +0 -0
  118. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/seeder/__init__.py +0 -0
  119. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/service/__init__.py +0 -0
  120. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.21 → tina4_python-3.13.23}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.21
3
+ Version: 3.13.23
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.21"
3
+ version = "3.13.23"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -1836,10 +1836,10 @@ async def dashboard(request, response):
1836
1836
  ## v3 Features Summary
1837
1837
 
1838
1838
  - **55 built-in features**, zero third-party dependencies
1839
- - **2,857 tests** passing across all modules
1839
+ - **2,866 tests** passing across all modules
1840
1840
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1841
1841
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
1842
- - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
1842
+ - **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), DB query caching — request-scoped auto cache **on by default** (`TINA4_AUTO_CACHING=true`, TTL `TINA4_AUTO_CACHING_TTL=5`s) dedupes identical reads within a request and flushes on writes; persistent cross-request cache opt-in via `TINA4_DB_CACHE=true` (TTL `TINA4_DB_CACHE_TTL=30`s); `cache_stats()` reports `mode` (request/persistent/off), `cache_clear()`
1843
1843
  - **Sessions**: 4 backends (file, Redis/Valkey, MongoDB, database)
1844
1844
  - **Queue**: file/RabbitMQ/Kafka/MongoDB backends, configured via env vars
1845
1845
  - **Cache**: memory/Redis/file backends
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.21"
11
+ __version__ = "3.13.23"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -1563,6 +1563,15 @@ async def handle(request: Request) -> Response:
1563
1563
  if swagger_resp is not None:
1564
1564
  return swagger_resp
1565
1565
 
1566
+ # Reset the request-scoped query cache so identical SELECTs are deduped
1567
+ # within this request but never served across requests (zero cross-request
1568
+ # staleness). No-op when TINA4_DB_CACHE=true (persistent mode) or disabled.
1569
+ try:
1570
+ from tina4_python.database.connection import Database as _Db
1571
+ _Db.reset_request_caches()
1572
+ except Exception:
1573
+ pass
1574
+
1566
1575
  # Route matching and dispatch
1567
1576
  route, params = Router.match(request.method, request.path)
1568
1577
 
@@ -13,6 +13,7 @@ import hashlib
13
13
  import os
14
14
  import threading
15
15
  import time
16
+ import weakref
16
17
  from urllib.parse import urlparse
17
18
  from tina4_python.database.adapter import DatabaseAdapter, DatabaseResult
18
19
 
@@ -139,6 +140,10 @@ class Database:
139
140
  operations to the adapter. This is what the rest of the framework uses.
140
141
  """
141
142
 
143
+ #: Live Database instances, so the request dispatcher can reset the
144
+ #: request-scoped query cache on every connection at the start of a request.
145
+ _instances: "weakref.WeakSet[Database]" = weakref.WeakSet()
146
+
142
147
  @classmethod
143
148
  def get_connection(cls, url: str = None, username: str = "", password: str = "", pool: int = 0, **kwargs) -> "Database":
144
149
  """Open a database connection — convention name matching SQLAlchemy
@@ -205,14 +210,27 @@ class Database:
205
210
  # pool can't rotate mid-transaction and silently break atomicity.
206
211
  self._tx_local = threading.local()
207
212
 
208
- # Query cache off by default, opt-in via TINA4_DB_CACHE=true
213
+ # Query cache. One store, two layers:
214
+ # • request-scoped (DEFAULT ON) — dedupes identical SELECTs to protect
215
+ # the DB from rapid repeat reads. Cleared at the start of every HTTP
216
+ # request (so it never serves data across requests) and on any write,
217
+ # with a short safety TTL for non-request contexts (scripts/workers).
218
+ # Off-switch: TINA4_AUTO_CACHING=false.
219
+ # • persistent (opt-in, TINA4_DB_CACHE=true) — cross-request TTL cache
220
+ # that is NOT cleared per request; entries expire by TINA4_DB_CACHE_TTL.
209
221
  from tina4_python.dotenv import is_truthy
210
- self._cache_enabled: bool = is_truthy(os.environ.get("TINA4_DB_CACHE", "false"))
211
- self._cache_ttl: int = int(os.environ.get("TINA4_DB_CACHE_TTL", "30"))
222
+ self._cache_persistent: bool = is_truthy(os.environ.get("TINA4_DB_CACHE", "false"))
223
+ self._cache_request_scoped: bool = is_truthy(os.environ.get("TINA4_AUTO_CACHING", "true"))
224
+ self._cache_enabled: bool = self._cache_persistent or self._cache_request_scoped
225
+ if self._cache_persistent:
226
+ self._cache_ttl: int = int(os.environ.get("TINA4_DB_CACHE_TTL", "30"))
227
+ else:
228
+ self._cache_ttl = int(os.environ.get("TINA4_AUTO_CACHING_TTL", "5"))
212
229
  self._query_cache: dict[str, tuple[float, object]] = {} # key -> (expires_at, result)
213
230
  self._cache_hits: int = 0
214
231
  self._cache_misses: int = 0
215
232
  self._cache_lock = threading.Lock()
233
+ Database._instances.add(self)
216
234
 
217
235
  def _create_adapter(self) -> DatabaseAdapter:
218
236
  """Select adapter based on URL scheme."""
@@ -326,11 +344,37 @@ class Database:
326
344
  with self._cache_lock:
327
345
  self._query_cache.clear()
328
346
 
347
+ def cache_new_request(self):
348
+ """Clear the request-scoped query cache at the start of an HTTP request.
349
+
350
+ No-op in persistent mode (TINA4_DB_CACHE=true) so cross-request entries
351
+ survive up to their TTL. Cumulative hit/miss counters are preserved.
352
+ """
353
+ if self._cache_request_scoped and not self._cache_persistent:
354
+ with self._cache_lock:
355
+ self._query_cache.clear()
356
+
357
+ @classmethod
358
+ def reset_request_caches(cls):
359
+ """Clear the request-scoped query cache on every live Database instance.
360
+
361
+ The request dispatcher calls this at the start of each HTTP request so
362
+ request-scoped caching never serves rows across requests (zero
363
+ cross-request staleness). Persistent-mode connections are left alone.
364
+ """
365
+ for inst in list(cls._instances):
366
+ try:
367
+ inst.cache_new_request()
368
+ except Exception:
369
+ pass
370
+
329
371
  def cache_stats(self) -> dict:
330
372
  """Return query cache statistics."""
331
373
  with self._cache_lock:
332
374
  return {
333
375
  "enabled": self._cache_enabled,
376
+ "mode": ("persistent" if self._cache_persistent
377
+ else "request" if self._cache_request_scoped else "off"),
334
378
  "hits": self._cache_hits,
335
379
  "misses": self._cache_misses,
336
380
  "size": len(self._query_cache),
@@ -163,7 +163,7 @@ class Session:
163
163
 
164
164
  def __init__(self, handler: SessionHandler = None, ttl: int = None):
165
165
  self._handler = handler or self._resolve_handler()
166
- self._ttl = ttl or int(os.environ.get("TINA4_SESSION_TTL", "1800")) # 30 min
166
+ self._ttl = ttl or int(os.environ.get("TINA4_SESSION_TTL", "3600")) # 60 min
167
167
  self._session_id: str | None = None
168
168
  self._data: dict = {}
169
169
  self._dirty: bool = False
File without changes