tina4-python 3.13.76__tar.gz → 3.13.78__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 (168) hide show
  1. {tina4_python-3.13.76 → tina4_python-3.13.78}/PKG-INFO +1 -1
  2. {tina4_python-3.13.76 → tina4_python-3.13.78}/pyproject.toml +4 -1
  3. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/CLAUDE.md +13 -13
  4. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/server.py +60 -26
  5. {tina4_python-3.13.76 → tina4_python-3.13.78}/.gitignore +0 -0
  6. {tina4_python-3.13.76 → tina4_python-3.13.78}/README.md +0 -0
  7. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/HtmlElement.py +0 -0
  8. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/Testing.py +0 -0
  9. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/__init__.py +0 -0
  10. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/context/__init__.py +0 -0
  17. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/context/chunker.py +0 -0
  18. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/mongodb.py +0 -0
  33. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/postgres.py +0 -0
  37. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/docstore/__init__.py +0 -0
  46. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/env.py +0 -0
  48. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/frond/FROND.md +0 -0
  49. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/frond/__init__.py +0 -0
  50. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/frond/engine.py +0 -0
  51. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/auth/meta.json +0 -0
  52. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  53. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/database/meta.json +0 -0
  54. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  55. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/error-overlay/meta.json +0 -0
  56. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  57. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/orm/meta.json +0 -0
  58. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  59. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  60. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/queue/meta.json +0 -0
  61. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  62. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/rest-api/meta.json +0 -0
  63. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  64. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/templates/meta.json +0 -0
  65. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  66. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  67. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/graphql/__init__.py +0 -0
  68. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/i18n/__init__.py +0 -0
  69. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/mcp/__init__.py +0 -0
  70. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/mcp/protocol.py +0 -0
  71. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/mcp/tools.py +0 -0
  72. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/messenger/__init__.py +0 -0
  73. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/migration/__init__.py +0 -0
  74. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/migration/runner.py +0 -0
  75. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/__feedback/widget.js +0 -0
  79. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  87. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  88. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.76 → tina4_python-3.13.78}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.76 → tina4_python-3.13.78}/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.76
3
+ Version: 3.13.78
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.76"
3
+ version = "3.13.78"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -64,6 +64,9 @@ tina4python = "tina4_python.cli:main"
64
64
  [tool.pytest.ini_options]
65
65
  asyncio_mode = "auto"
66
66
  testpaths = ["tests"]
67
+ markers = [
68
+ "slow: a test that needs real wall-clock time to reproduce a timing bug (deselect with -m 'not slow')",
69
+ ]
67
70
 
68
71
  [tool.ruff]
69
72
  target-version = "py312"
@@ -514,23 +514,23 @@ TINA4_TOKEN_LIMIT is used to set the session time, default 60 minutes
514
514
 
515
515
  Set `TINA4_SESSION_BACKEND` to choose a backend:
516
516
 
517
- | Handler | Backend | Required package |
517
+ The value is the backend NAME, not a class name. An unknown value falls back to
518
+ the file backend without raising.
519
+
520
+ | Value | Backend | Required package |
518
521
  |---------|---------|-----------------|
519
- | `SessionFileHandler` (default) | File system | |
520
- | `SessionRedisHandler` | Redis | `redis` |
521
- | `SessionValkeyHandler` | Valkey | `valkey` |
522
- | `SessionMongoHandler` | MongoDB | `pymongo` |
522
+ | `file` (default) | File system | none |
523
+ | `redis` | Redis | `redis` |
524
+ | `valkey` | Valkey | `valkey` |
525
+ | `mongodb` | MongoDB | `pymongo` |
526
+ | `database` | Your ORM DB | none |
523
527
 
524
528
  #### MongoDB session env vars
525
529
 
526
530
  ```bash
527
- TINA4_SESSION_BACKEND=SessionMongoHandler
528
- TINA4_SESSION_MONGO_HOST=localhost # default
529
- TINA4_SESSION_MONGO_PORT=27017 # default
530
- TINA4_SESSION_MONGO_URI= # full URI (overrides host/port)
531
- TINA4_SESSION_MONGO_USERNAME= # optional
532
- TINA4_SESSION_MONGO_PASSWORD= # optional
533
- TINA4_SESSION_MONGO_DB=tina4_sessions # default database
531
+ TINA4_SESSION_BACKEND=mongodb # backend NAME, not a class name
532
+ TINA4_SESSION_MONGO_URI=mongodb://localhost:27017 # connection URI
533
+ TINA4_SESSION_MONGO_DB=tina4_sessions # default database
534
534
  TINA4_SESSION_MONGO_COLLECTION=sessions # default collection
535
535
  TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
536
536
  ```
@@ -1644,7 +1644,7 @@ TINA4_OVERRIDE_CLIENT=false # Set to true to allow running without tina4 C
1644
1644
  HOST_NAME=localhost:7145
1645
1645
 
1646
1646
  # Sessions
1647
- TINA4_SESSION_BACKEND=SessionFileHandler # SessionFileHandler, SessionRedisHandler, SessionValkeyHandler, SessionMongoHandler
1647
+ TINA4_SESSION_BACKEND=file # file, redis, valkey, mongodb, database (unknown value falls back to file)
1648
1648
  TINA4_SESSION_SAMESITE=Lax # SameSite attribute for session cookies (default: Lax)
1649
1649
 
1650
1650
  # Swagger/OpenAPI
@@ -49,6 +49,58 @@ def background(callback, interval: float = 1.0):
49
49
  _background_tasks.append({"callback": callback, "interval": interval})
50
50
 
51
51
 
52
+ async def background_tick_loop(callback, interval: float, executor, shutdown):
53
+ """Run one registered background task on its interval until shutdown.
54
+
55
+ A task NEVER overlaps itself: each run is awaited to completion before the
56
+ interval sleep and the next tick. That is the only sound reading of an
57
+ interval scheduler, and it is what makes the one-worker-per-task pool sizing
58
+ in `_serve` safe.
59
+
60
+ Args:
61
+ callback: The registered callable (sync or async, no arguments).
62
+ interval: Seconds to sleep between runs.
63
+ executor: ThreadPoolExecutor used for sync callbacks.
64
+ shutdown: asyncio.Event that ends the loop when set.
65
+ """
66
+ timeout = max(interval * 2, 5.0)
67
+ loop = asyncio.get_running_loop()
68
+ while not shutdown.is_set():
69
+ try:
70
+ if asyncio.iscoroutinefunction(callback):
71
+ # A coroutine IS cancellable, so the timeout really interrupts it.
72
+ await asyncio.wait_for(callback(), timeout=timeout)
73
+ else:
74
+ # Run sync callback in a thread pool — never blocks the event loop.
75
+ #
76
+ # A callable already running in a ThreadPoolExecutor CANNOT be
77
+ # cancelled: Future.cancel() returns False once it has started and
78
+ # there is no way to interrupt the thread. So this must NOT use
79
+ # wait_for() — that abandons the wrapper future while the thread
80
+ # keeps running, and the next tick then starts a SECOND copy
81
+ # alongside the first (silent double-execution of a slow sweep,
82
+ # with every later tick piling on another). Wait for the run to
83
+ # finish and only WARN about the overrun.
84
+ fut = loop.run_in_executor(executor, callback)
85
+ done, _pending = await asyncio.wait({fut}, timeout=timeout)
86
+ if not done:
87
+ Log.warning(
88
+ f"Background task is still running after {timeout:.1f}s. "
89
+ f"The next run is deferred until it finishes (a running sync "
90
+ f"task cannot be interrupted). Use non-blocking calls "
91
+ f"(e.g. queue.pop() instead of queue.consume())."
92
+ )
93
+ await fut # never overlap; re-raises the callback's error
94
+ except asyncio.TimeoutError:
95
+ Log.warning(
96
+ f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
97
+ f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
98
+ )
99
+ except Exception as e:
100
+ Log.error(f"Background task error: {e}")
101
+ await asyncio.sleep(interval)
102
+
103
+
52
104
  # module_name → source-file mtime at the last (re)import. Drives the
53
105
  # changed-file detection in ``_auto_discover``: a file whose mtime is newer
54
106
  # than the recorded value is re-executed in place, so editing an existing
@@ -2743,7 +2795,9 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
2743
2795
 
2744
2796
  # Start registered background tasks as asyncio tasks.
2745
2797
  # Sync callbacks run in a thread pool so they CANNOT block the event loop.
2746
- # A timeout (2x interval, min 5s) cancels runaway tasks.
2798
+ # max_workers is one per registered task sound only because a task never
2799
+ # overlaps itself (see background_tick_loop), so tasks cannot starve each
2800
+ # other of workers.
2747
2801
  import concurrent.futures
2748
2802
  _executor = concurrent.futures.ThreadPoolExecutor(
2749
2803
  max_workers=max(len(_background_tasks), 2),
@@ -2751,31 +2805,11 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
2751
2805
  )
2752
2806
  bg_tasks = []
2753
2807
  for task_def in _background_tasks:
2754
- cb = task_def["callback"]
2755
- iv = task_def["interval"]
2756
-
2757
- async def _tick_loop(_cb=cb, _iv=iv):
2758
- timeout = max(_iv * 2, 5.0)
2759
- while not shutdown.is_set():
2760
- try:
2761
- if asyncio.iscoroutinefunction(_cb):
2762
- await asyncio.wait_for(_cb(), timeout=timeout)
2763
- else:
2764
- # Run sync callback in thread pool — never blocks the event loop
2765
- await asyncio.wait_for(
2766
- loop.run_in_executor(_executor, _cb),
2767
- timeout=timeout,
2768
- )
2769
- except asyncio.TimeoutError:
2770
- Log.warning(
2771
- f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
2772
- f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
2773
- )
2774
- except Exception as e:
2775
- Log.error(f"Background task error: {e}")
2776
- await asyncio.sleep(_iv)
2777
-
2778
- bg_tasks.append(asyncio.create_task(_tick_loop()))
2808
+ bg_tasks.append(asyncio.create_task(
2809
+ background_tick_loop(
2810
+ task_def["callback"], task_def["interval"], _executor, shutdown,
2811
+ )
2812
+ ))
2779
2813
 
2780
2814
  await shutdown.wait()
2781
2815
 
File without changes