tina4-python 3.13.79__tar.gz → 3.13.82__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 (173) hide show
  1. {tina4_python-3.13.79 → tina4_python-3.13.82}/.gitignore +13 -9
  2. {tina4_python-3.13.79 → tina4_python-3.13.82}/PKG-INFO +5 -5
  3. {tina4_python-3.13.79 → tina4_python-3.13.82}/README.md +4 -4
  4. {tina4_python-3.13.79 → tina4_python-3.13.82}/pyproject.toml +1 -1
  5. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/CLAUDE.md +2 -2
  6. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/__init__.py +9 -1
  7. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/cli/__init__.py +133 -11
  8. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/server.py +110 -11
  9. tina4_python-3.13.82/tina4_python/mqtt/__init__.py +742 -0
  10. tina4_python-3.13.82/tina4_python/mqtt/message.py +93 -0
  11. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/css/tina4.css +56 -130
  12. tina4_python-3.13.82/tina4_python/public/css/tina4.min.css +1 -0
  13. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/__init__.py +47 -2
  14. tina4_python-3.13.82/tina4_python/scss/tina4css/_pagination.scss +63 -0
  15. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_utilities.scss +11 -0
  16. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/tina4.scss +1 -0
  17. tina4_python-3.13.82/tina4_python/test_client/__init__.py +209 -0
  18. tina4_python-3.13.79/tina4_python/public/css/tina4.min.css +0 -1
  19. tina4_python-3.13.79/tina4_python/test_client/__init__.py +0 -188
  20. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/HtmlElement.py +0 -0
  21. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/Testing.py +0 -0
  22. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/ai/__init__.py +0 -0
  23. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/api/__init__.py +0 -0
  24. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/auth/__init__.py +0 -0
  25. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/cache/__init__.py +0 -0
  26. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/container/__init__.py +0 -0
  27. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/context/__init__.py +0 -0
  28. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/context/chunker.py +0 -0
  29. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/__init__.py +0 -0
  30. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/cache.py +0 -0
  31. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/constants.py +0 -0
  32. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/events.py +0 -0
  33. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/middleware.py +0 -0
  34. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/rate_limiter.py +0 -0
  35. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/request.py +0 -0
  36. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/response.py +0 -0
  37. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/core/router.py +0 -0
  38. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/crud/__init__.py +0 -0
  39. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/__init__.py +0 -0
  40. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/adapter.py +0 -0
  41. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/connection.py +0 -0
  42. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/firebird.py +0 -0
  43. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/mongodb.py +0 -0
  44. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/mssql.py +0 -0
  45. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/mysql.py +0 -0
  46. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/odbc.py +0 -0
  47. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/postgres.py +0 -0
  48. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/database/sqlite.py +0 -0
  49. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/debug/__init__.py +0 -0
  50. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/debug/error_overlay.py +0 -0
  51. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/dev_admin/__init__.py +0 -0
  52. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/dev_admin/metrics.py +0 -0
  53. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/dev_admin/plan.py +0 -0
  54. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/dev_admin/project_index.py +0 -0
  55. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/docs.py +0 -0
  56. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/docstore/__init__.py +0 -0
  57. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/dotenv/__init__.py +0 -0
  58. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/env.py +0 -0
  59. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/frond/FROND.md +0 -0
  60. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/frond/__init__.py +0 -0
  61. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/frond/engine.py +0 -0
  62. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/auth/meta.json +0 -0
  63. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  64. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/database/meta.json +0 -0
  65. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  66. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/error-overlay/meta.json +0 -0
  67. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  68. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/orm/meta.json +0 -0
  69. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  70. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  71. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/queue/meta.json +0 -0
  72. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  73. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/rest-api/meta.json +0 -0
  74. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  75. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/templates/meta.json +0 -0
  76. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  77. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  78. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/graphql/__init__.py +0 -0
  79. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/i18n/__init__.py +0 -0
  80. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/mcp/__init__.py +0 -0
  81. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/mcp/protocol.py +0 -0
  82. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/mcp/tools.py +0 -0
  83. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/messenger/__init__.py +0 -0
  84. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/migration/__init__.py +0 -0
  85. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/migration/runner.py +0 -0
  86. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/orm/__init__.py +0 -0
  87. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/orm/fields.py +0 -0
  88. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/orm/model.py +0 -0
  89. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/__feedback/widget.js +0 -0
  90. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/favicon.ico +0 -0
  91. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/images/logo.svg +0 -0
  92. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  93. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/frond.js +0 -0
  94. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/frond.min.js +0 -0
  95. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  96. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  97. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/tina4.min.js +0 -0
  98. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/js/tina4js.min.js +0 -0
  99. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/swagger/index.html +0 -0
  100. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  101. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/query_builder/__init__.py +0 -0
  102. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/__init__.py +0 -0
  103. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/job.py +0 -0
  104. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/kafka_backend.py +0 -0
  105. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/lite_backend.py +0 -0
  106. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/mongo_backend.py +0 -0
  107. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue/rabbitmq_backend.py +0 -0
  108. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue_backends/__init__.py +0 -0
  109. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue_backends/kafka_backend.py +0 -0
  110. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue_backends/mongo_backend.py +0 -0
  111. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  112. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/__init__.py +0 -0
  113. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/Attachment.py +0 -0
  114. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/Channel.py +0 -0
  115. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/ChannelMember.py +0 -0
  116. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/Message.py +0 -0
  117. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/Workspace.py +0 -0
  118. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/models/__init__.py +0 -0
  119. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/realtime/storage.py +0 -0
  120. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  121. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_badges.scss +0 -0
  122. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  123. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_cards.scss +0 -0
  124. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_forms.scss +0 -0
  125. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_grid.scss +0 -0
  126. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_modals.scss +0 -0
  127. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_nav.scss +0 -0
  128. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_reset.scss +0 -0
  129. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_tables.scss +0 -0
  130. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_typography.scss +0 -0
  131. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/_variables.scss +0 -0
  132. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/base.scss +0 -0
  133. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/scss/tina4css/colors.scss +0 -0
  134. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/seeder/__init__.py +0 -0
  135. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/service/__init__.py +0 -0
  136. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/session/__init__.py +0 -0
  137. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/session_handlers/__init__.py +0 -0
  138. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  139. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/session_handlers/redis_handler.py +0 -0
  140. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/session_handlers/valkey_handler.py +0 -0
  141. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/swagger/__init__.py +0 -0
  142. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/components/crud.twig +0 -0
  143. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  144. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  145. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/docker/python/Dockerfile +0 -0
  146. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  147. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/302.twig +0 -0
  148. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/401.twig +0 -0
  149. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/403.twig +0 -0
  150. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/404.twig +0 -0
  151. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/500.twig +0 -0
  152. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/502.twig +0 -0
  153. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/503.twig +0 -0
  154. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/errors/base.twig +0 -0
  155. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/frontend/README.md +0 -0
  156. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/templates/readme.md +0 -0
  157. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/test/__init__.py +0 -0
  158. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  159. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  160. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  161. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  162. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  163. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  164. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  165. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  166. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  167. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  168. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  169. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  170. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/validator/__init__.py +0 -0
  171. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/websocket/__init__.py +0 -0
  172. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/websocket/backplane.py +0 -0
  173. {tina4_python-3.13.79 → tina4_python-3.13.82}/tina4_python/wsdl/__init__.py +0 -0
@@ -74,15 +74,19 @@ build/
74
74
  example/.env
75
75
  example/data/*.db
76
76
  example/data/*.db-*
77
- example/store/data/*.db
78
- example/store/data/*.db-*
79
- example/store/data/broken/
80
- example/store/data/.broken/
81
- example/store/data/mailbox/
82
- example/store/data/sessions/
83
- example/store/.tina4/
84
- example/store/__pycache__/
85
- example/store/src/**/__pycache__/
77
+ # The example app runs from example/, not example/store/ (which no longer
78
+ # exists), so the old example/store/... rules below matched nothing and every
79
+ # runtime artifact the example writes leaked into git status - 466 of them, plus
80
+ # 6 that got committed. Anchored /logs/ missed example/logs/ for the same reason.
81
+ example/data/broken/
82
+ example/data/.broken/
83
+ example/data/mailbox/
84
+ example/data/queue/
85
+ example/data/sessions/
86
+ example/logs/
87
+ example/.tina4/
88
+ example/__pycache__/
89
+ example/src/**/__pycache__/
86
90
  .claude/settings.local.json
87
91
  .claude/worktrees/
88
92
  /nonexistent_path/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.79
3
+ Version: 3.13.82
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
@@ -37,13 +37,13 @@ Description-Content-Type: text/markdown
37
37
  <h1 align="center">Tina4 Python</h1>
38
38
 
39
39
  <p align="center">
40
- 55 built-in features. Zero dependencies. One import, everything works.
40
+ 98 built-in features. Zero dependencies. One import, everything works.
41
41
  </p>
42
42
 
43
43
  <p align="center">
44
44
  <a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
45
45
  <img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
46
- <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
46
+ <img src="https://img.shields.io/badge/features-98-blue" alt="Features">
47
47
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
48
48
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
49
49
  </p>
@@ -726,7 +726,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
726
726
  | Bottle | 4,355 | 0 | ~5 |
727
727
  | Django | 4,050 | 20+ | ~22 |
728
728
 
729
- Tina4 Python delivers competitive throughput with **zero dependencies and 55 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
729
+ Tina4 Python delivers competitive throughput with **zero dependencies and 98 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
730
730
 
731
731
  **Across all 4 Tina4 implementations:**
732
732
 
@@ -742,7 +742,7 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
742
742
 
743
743
  ## Cross-Framework Parity
744
744
 
745
- Tina4 ships identical features across four languages: same architecture, same conventions, same 55 features:
745
+ Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
746
746
 
747
747
  | | Python | PHP | Ruby | Node.js |
748
748
  |---|--------|-----|------|---------|
@@ -5,13 +5,13 @@
5
5
  <h1 align="center">Tina4 Python</h1>
6
6
 
7
7
  <p align="center">
8
- 55 built-in features. Zero dependencies. One import, everything works.
8
+ 98 built-in features. Zero dependencies. One import, everything works.
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
13
13
  <img src="https://img.shields.io/badge/tests-2%2C281%20passing-brightgreen" alt="Tests">
14
- <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
14
+ <img src="https://img.shields.io/badge/features-98-blue" alt="Features">
15
15
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
16
16
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
17
17
  </p>
@@ -694,7 +694,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
694
694
  | Bottle | 4,355 | 0 | ~5 |
695
695
  | Django | 4,050 | 20+ | ~22 |
696
696
 
697
- Tina4 Python delivers competitive throughput with **zero dependencies and 55 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
697
+ Tina4 Python delivers competitive throughput with **zero dependencies and 98 features**. Frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
698
698
 
699
699
  **Across all 4 Tina4 implementations:**
700
700
 
@@ -710,7 +710,7 @@ Run benchmarks locally: `python benchmarks/benchmark.py --python`
710
710
 
711
711
  ## Cross-Framework Parity
712
712
 
713
- Tina4 ships identical features across four languages: same architecture, same conventions, same 55 features:
713
+ Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
714
714
 
715
715
  | | Python | PHP | Ruby | Node.js |
716
716
  |---|--------|-----|------|---------|
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.79"
3
+ version = "3.13.82"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -291,7 +291,7 @@ Queue(topic="tasks").push({"action": "send_email"})
291
291
  6. **Connection strings**: v3 uses standard URL format: `driver://host:port/database` with separate `username` and `password` parameters. Example: `Database("firebird://localhost:3050//path/to/db", "SYSDBA", "masterkey")`. Environment variable: `TINA4_DATABASE_URL`.
292
292
  7. **Running the app**: use the `tina4` CLI — `tina4 serve`. The framework **refuses to boot via a plain `python app.py`** (it must be launched by the CLI; `TINA4_OVERRIDE_CLIENT=true` only for embedding). Port/name are CLI args.
293
293
  8. **SCSS**: Files in `src/scss/` are auto-compiled to `src/public/css/` on startup
294
- 12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling.
294
+ 12. **Background tasks**: Use `background(fn, interval)` from `tina4_python.core.server` — never use `threading.Thread` for periodic work. The `background()` function runs tasks cooperatively in the asyncio event loop with proper shutdown handling. It returns a `BackgroundTask` handle: call `handle.stop()` to end ONE task and deregister it (idempotent; returns `False` if already stopped). `background_task_count()` reports how many are registered and `stop_all_background_tasks()` clears them all — a stopped task is never left in the registry.
295
295
 
296
296
 
297
297
  ---
@@ -1912,7 +1912,7 @@ async def dashboard(request, response):
1912
1912
 
1913
1913
  ## v3 Features Summary
1914
1914
 
1915
- - **55 built-in features**, zero third-party dependencies
1915
+ - **98 built-in features**, zero third-party dependencies
1916
1916
  - **2,899 tests** passing across all modules
1917
1917
  - **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
1918
1918
  - **`tina4python generate`**: model, route, migration, middleware scaffolding
@@ -89,6 +89,9 @@ from tina4_python.auth import Auth # noqa: E402, F401
89
89
  # ── Queue ──
90
90
  from tina4_python.queue import Queue # noqa: E402, F401
91
91
 
92
+ # ── MQTT (IoT) ──
93
+ from tina4_python.mqtt import Mqtt, MqttMessage, MqttError # noqa: E402, F401
94
+
92
95
  # ── Template engine ──
93
96
  from tina4_python.frond import Frond # noqa: E402, F401
94
97
 
@@ -101,7 +104,12 @@ from tina4_python.cache import ( # noqa: E402, F401
101
104
  from tina4_python.container import Container # noqa: E402, F401
102
105
 
103
106
  # ── Server ──
104
- from tina4_python.core.server import run, background # noqa: E402, F401
107
+ from tina4_python.core.server import ( # noqa: E402, F401
108
+ run,
109
+ background,
110
+ background_task_count,
111
+ stop_all_background_tasks,
112
+ )
105
113
 
106
114
  # ── HTTP Client ──
107
115
  from tina4_python.api import Api # noqa: E402, F401
@@ -13,9 +13,15 @@ CLI commands for development workflow.
13
13
  tina4python test # Run tests
14
14
  tina4python generate # Generate scaffolding
15
15
  tina4python ai # Detect AI tools and install context
16
+
17
+ Three further commands — doctor, setup, deploy — are owned by the `tina4` client
18
+ and reached by DELEGATION (see DELEGATED / _delegate_to_client below), so
19
+ `tina4python doctor` behaves exactly like `tina4 doctor` without cloning the
20
+ client's implementation into four languages.
16
21
  """
17
22
  import os
18
23
  import re
24
+ import shutil
19
25
  import signal
20
26
  import subprocess
21
27
  import sys
@@ -187,6 +193,83 @@ def _kill_process_on_port(port: int) -> bool:
187
193
  return False
188
194
 
189
195
 
196
+ # ── Delegation to the `tina4` client ─────────────────────────────────
197
+ #
198
+ # `doctor`, `setup` and `deploy` are owned by the Rust `tina4` client, not by any
199
+ # framework. `doctor` probes ALL FOUR runtimes plus package managers, ports and
200
+ # global AI-skills currency; `setup` installs language runtimes (Homebrew /
201
+ # Chocolatey, with UAC elevation on Windows) and scaffolds a project from
202
+ # nothing; `deploy` writes deployment boilerplate baked into the client binary.
203
+ # Cloning any of them into four languages would duplicate hundreds of lines per
204
+ # language for zero new capability — and four copies would immediately drift.
205
+ #
206
+ # So the framework CLI DELEGATES: it resolves `tina4` on PATH, runs it with the
207
+ # same argv, and exits with the client's exit code. All four frameworks reach the
208
+ # SAME implementation, which is a stronger parity guarantee than four ports.
209
+ #
210
+ # Delegation is ALLOW-LISTED, never blind. The client forwards ITS unknown
211
+ # commands to the framework CLI, so a framework that forwarded its unknowns back
212
+ # would ping-pong an unknown command between two processes forever. The closed
213
+ # DELEGATED set contains only commands the client dispatches natively, so no
214
+ # loop is possible by construction, and a real typo still gets "Unknown command".
215
+
216
+ CLIENT_BINARY = "tina4"
217
+
218
+ # Internal process marker (same class as the client's own TINA4_SETUP_ELEVATED):
219
+ # set on the child so a client that resolves back to a framework CLI is caught
220
+ # instead of spawning forever. NOT user configuration — deliberately absent from
221
+ # the CLI's known_vars().
222
+ DELEGATION_GUARD_ENV = "TINA4_CLI_DELEGATED"
223
+
224
+ # Exit codes. 127 is the conventional "command not found" and covers both ways
225
+ # the client can be unreachable (absent from PATH, or the loop guard tripping).
226
+ EXIT_CLIENT_UNAVAILABLE = 127
227
+ EXIT_UNKNOWN_COMMAND = 1
228
+
229
+ CLIENT_INSTALL_HINT = (
230
+ " Install it: curl -fsSL https://tina4.com/install.sh | sh\n"
231
+ " Windows: irm https://tina4.com/install.ps1 | iex"
232
+ )
233
+
234
+
235
+ def _find_client() -> str | None:
236
+ """Absolute path of the `tina4` client on PATH, or None if it isn't there."""
237
+ return shutil.which(CLIENT_BINARY)
238
+
239
+
240
+ def _delegate_to_client(command: str, args: list) -> int:
241
+ """Run `tina4 <command> <args...>`, returning the client's exit code.
242
+
243
+ Returns EXIT_CLIENT_UNAVAILABLE (127) with an actionable message when the
244
+ client is not on PATH, or when the re-entry guard shows the resolved `tina4`
245
+ came back to a framework CLI (a delegation loop).
246
+ """
247
+ if os.environ.get(DELEGATION_GUARD_ENV) == command:
248
+ print(
249
+ f" Refusing to delegate '{command}' again — the 'tina4' on your PATH\n"
250
+ f" resolved back to a framework CLI instead of the tina4 client.\n\n"
251
+ f" Check which 'tina4' comes first on your PATH and put the client first.",
252
+ file=sys.stderr,
253
+ )
254
+ return EXIT_CLIENT_UNAVAILABLE
255
+
256
+ client = _find_client()
257
+ if client is None:
258
+ print(
259
+ f" '{command}' is provided by the tina4 client, which is not on your PATH.\n\n"
260
+ f"{CLIENT_INSTALL_HINT}\n\n"
261
+ f" Then run: {CLIENT_BINARY} {command}",
262
+ file=sys.stderr,
263
+ )
264
+ return EXIT_CLIENT_UNAVAILABLE
265
+
266
+ # stdio is inherited, so the client's interactive prompts (setup) and colour
267
+ # output work exactly as if it had been invoked directly.
268
+ env = dict(os.environ)
269
+ env[DELEGATION_GUARD_ENV] = command
270
+ return subprocess.run([client, command, *args], env=env).returncode
271
+
272
+
190
273
  # ── Main entry point ─────────────────────────────────────────────────
191
274
 
192
275
  def main():
@@ -205,9 +288,14 @@ def main():
205
288
  spec = COMMANDS.get(command)
206
289
  if spec:
207
290
  spec["handler"](cmd_args)
291
+ elif command in DELEGATED:
292
+ sys.exit(_delegate_to_client(command, cmd_args))
208
293
  else:
294
+ # A genuinely unknown command is an ERROR: exit non-zero so a typo in a
295
+ # script or CI step fails loudly instead of reporting success.
209
296
  print(f"Unknown command: {command}")
210
297
  _help([])
298
+ sys.exit(EXIT_UNKNOWN_COMMAND)
211
299
 
212
300
 
213
301
  def _env_migrate(args):
@@ -259,21 +347,26 @@ def _env_migrate(args):
259
347
  def _help(args=None):
260
348
  """Print the human-readable command reference.
261
349
 
262
- Generated from the COMMANDS and GENERATORS registries — the SAME single
263
- source of truth that drives dispatch (`main`) and the `commands --json`
264
- manifest — so the help text can never drift from what the CLI actually does.
350
+ Generated from the COMMANDS, DELEGATED and GENERATORS registries — the SAME
351
+ single source of truth that drives dispatch (`main`) and the
352
+ `commands --json` manifest — so the help text can never drift from what the
353
+ CLI actually does.
265
354
  """
266
355
  command_rows = [
267
356
  (f"{name} {spec.get('usage', '')}".rstrip(), spec["summary"])
268
357
  for name, spec in COMMANDS.items()
269
358
  ]
359
+ delegated_rows = [
360
+ (f"{name} {spec.get('usage', '')}".rstrip(), spec["summary"])
361
+ for name, spec in DELEGATED.items()
362
+ ]
270
363
  generator_rows = [
271
364
  (f"generate {name} {spec.get('usage', '')}".rstrip(), spec["summary"])
272
365
  for name, spec in GENERATORS.items()
273
366
  ]
274
367
  # Align summaries in a column; a left cell longer than the cap overflows
275
368
  # cleanly (2-space gap) rather than pushing every other summary out.
276
- pad = min(46, max(len(left) for left, _ in command_rows + generator_rows))
369
+ pad = min(46, max(len(left) for left, _ in command_rows + delegated_rows + generator_rows))
277
370
 
278
371
  def row(left, summary):
279
372
  gap = pad if len(left) <= pad else len(left)
@@ -281,6 +374,10 @@ def _help(args=None):
281
374
 
282
375
  lines = ["", "Tina4 Python — CLI", "", "Usage: tina4python <command> [options]", "", "Commands:"]
283
376
  lines += [row(left, summary) for left, summary in command_rows]
377
+ lines += ["", f"Delegated to the {CLIENT_BINARY} client (same behaviour in every framework):"]
378
+ lines += [row(left, summary) for left, summary in delegated_rows]
379
+ lines += [f" (these run the {CLIENT_BINARY} client — install: "
380
+ f"curl -fsSL https://tina4.com/install.sh | sh)"]
284
381
  lines += ["", "Generators:"]
285
382
  lines += [row(left, summary) for left, summary in generator_rows]
286
383
  lines += [
@@ -763,7 +860,8 @@ def _routes(args):
763
860
 
764
861
  def _test(args):
765
862
  """Run the test suite."""
766
- subprocess.run([sys.executable, "-m", "pytest", "tests/"] + args)
863
+ result = subprocess.run([sys.executable, "-m", "pytest", "tests/"] + args)
864
+ sys.exit(result.returncode)
767
865
 
768
866
 
769
867
  def _build(args):
@@ -2942,15 +3040,20 @@ def _queue(args):
2942
3040
  def _commands_manifest() -> dict:
2943
3041
  """Build the machine-readable manifest of the CLI's command surface.
2944
3042
 
2945
- Pure data: reads the module-level COMMANDS registry and the framework
2946
- version — no bootstrap, no database, no migrations, no app imports. This is
2947
- exactly what `commands --json` serializes and what the tina4 Rust client
2948
- consumes to discover which commands this framework supports.
3043
+ Pure data: reads the module-level COMMANDS and DELEGATED registries and the
3044
+ framework version — no bootstrap, no database, no migrations, no app imports.
3045
+ This is exactly what `commands --json` serializes and what the tina4 Rust
3046
+ client consumes to discover which commands this framework supports.
3047
+
3048
+ Commands the framework hands to the `tina4` client carry `"delegated": true`,
3049
+ so the manifest describes the WHOLE surface the CLI accepts while still
3050
+ saying who implements each one. The client needs no change for this: its help
3051
+ renderer already drops manifest names that clash with its own natives.
2949
3052
 
2950
3053
  Shape::
2951
3054
 
2952
3055
  {"framework": "python", "version": "<x.y.z>",
2953
- "commands": [{"name", "summary", "args"?, "subcommands"?}, ...]}
3056
+ "commands": [{"name", "summary", "args"?, "subcommands"?, "delegated"?}, ...]}
2954
3057
  """
2955
3058
  from tina4_python import __version__
2956
3059
  commands = []
@@ -2961,6 +3064,11 @@ def _commands_manifest() -> dict:
2961
3064
  if spec.get("subcommands"):
2962
3065
  entry["subcommands"] = list(spec["subcommands"])
2963
3066
  commands.append(entry)
3067
+ for command_name, spec in DELEGATED.items():
3068
+ entry = {"name": command_name, "summary": spec["summary"], "delegated": True}
3069
+ if spec.get("args"):
3070
+ entry["args"] = list(spec["args"])
3071
+ commands.append(entry)
2964
3072
  return {"framework": "python", "version": __version__, "commands": commands}
2965
3073
 
2966
3074
 
@@ -2987,7 +3095,8 @@ def _commands(args=None):
2987
3095
  print(f"\nTina4 {manifest['framework']} — {manifest['version']}\n")
2988
3096
  width = max(len(command["name"]) for command in manifest["commands"])
2989
3097
  for command in manifest["commands"]:
2990
- print(f" {command['name']:<{width}} {command['summary']}")
3098
+ marker = f" ({CLIENT_BINARY} client)" if command.get("delegated") else ""
3099
+ print(f" {command['name']:<{width}} {command['summary']}{marker}")
2991
3100
  if command.get("subcommands"):
2992
3101
  print(f" {'':<{width}} {', '.join(command['subcommands'])}")
2993
3102
  print()
@@ -3045,5 +3154,18 @@ COMMANDS = {
3045
3154
  "help": {"handler": _help, "summary": "Show this help"},
3046
3155
  }
3047
3156
 
3157
+ # Commands the `tina4` client OWNS and this CLI reaches by delegation — see the
3158
+ # "Delegation to the `tina4` client" section above for why these are not ported.
3159
+ # There are no handlers here: `main()` runs `tina4 <name> <args...>` and exits
3160
+ # with its code. Keep this set closed and identical in all four frameworks — it
3161
+ # must contain ONLY commands the client dispatches natively, or delegation could
3162
+ # bounce back and loop. Summaries are the client's own wording, verbatim.
3163
+ DELEGATED = {
3164
+ "doctor": {"summary": "Check installed languages and tools"},
3165
+ "setup": {"summary": "Guided, menu-driven setup: install everything + scaffold a ready-to-run project"},
3166
+ "deploy": {"usage": "<docker|systemd|nginx|cpanel> [--force]", "args": ["target"],
3167
+ "summary": "Generate deployment scaffolding (Dockerfile, systemd unit, nginx block, cPanel)"},
3168
+ }
3169
+
3048
3170
 
3049
3171
  __all__ = ["main"]
@@ -11,6 +11,7 @@ import signal
11
11
  import asyncio
12
12
  import contextvars
13
13
  import importlib
14
+ import threading
14
15
  import uuid
15
16
  from email.utils import formatdate, parsedate_to_datetime
16
17
  from pathlib import Path
@@ -34,10 +35,63 @@ _ai_port_ctx: contextvars.ContextVar[bool] = contextvars.ContextVar("_ai_port_ct
34
35
  _start_time: float = 0
35
36
 
36
37
  # ── Background tasks registry ────────────────────────────────────────────
37
- _background_tasks: list[dict] = []
38
+ _background_tasks: list["BackgroundTask"] = []
39
+ # The registry is read from the event loop and written from user code (which may
40
+ # be on another thread), and stop() is a search-then-delete — not atomic under
41
+ # the GIL. One lock guards every mutation.
42
+ _background_lock = threading.Lock()
38
43
 
39
44
 
40
- def background(callback, interval: float = 1.0):
45
+ class BackgroundTask:
46
+ """Handle for one registered background task.
47
+
48
+ Returned by :func:`background`. Call :meth:`stop` to end the task AND remove
49
+ it from the registry — a stopped task is never left behind, so
50
+ :func:`background_task_count` always reports what is actually running.
51
+
52
+ Mirrors Node's ``background()`` handle (``handle.stop()``) and Ruby's
53
+ ``Tina4::Background.stop_task(task)``.
54
+ """
55
+
56
+ __slots__ = ("callback", "interval", "stopped", "_runner")
57
+
58
+ def __init__(self, callback, interval: float):
59
+ self.callback = callback
60
+ self.interval = float(interval)
61
+ self.stopped = False
62
+ # The asyncio.Task ticking this callback, once the server loop has
63
+ # started it. None while the task is registered but not yet running.
64
+ self._runner = None
65
+
66
+ def stop(self) -> bool:
67
+ """Stop this task and deregister it.
68
+
69
+ Works whether or not the server is running: before start it simply
70
+ leaves the registry, and once ticking it also cancels its runner.
71
+
72
+ Idempotent — a second call is a safe no-op.
73
+
74
+ Returns:
75
+ True if this call removed the task, False if it was already gone.
76
+ """
77
+ removed = False
78
+ with _background_lock:
79
+ # Identity, not equality: only THIS handle goes, never a sibling
80
+ # that happens to hold the same callback and interval.
81
+ for index, registered in enumerate(_background_tasks):
82
+ if registered is self:
83
+ del _background_tasks[index]
84
+ removed = True
85
+ break
86
+
87
+ self.stopped = True
88
+ runner, self._runner = self._runner, None
89
+ if runner is not None and not runner.done():
90
+ runner.cancel()
91
+ return removed
92
+
93
+
94
+ def background(callback, interval: float = 1.0) -> BackgroundTask:
41
95
  """Register a background task that runs periodically in the server event loop.
42
96
 
43
97
  Matches PHP's $app->background(fn, interval) pattern.
@@ -45,8 +99,59 @@ def background(callback, interval: float = 1.0):
45
99
  Args:
46
100
  callback: Function to call (sync or async, no arguments).
47
101
  interval: Seconds between invocations (default: 1.0).
102
+
103
+ Returns:
104
+ A BackgroundTask handle — call ``.stop()`` to end and deregister it.
48
105
  """
49
- _background_tasks.append({"callback": callback, "interval": interval})
106
+ task = BackgroundTask(callback, interval)
107
+ with _background_lock:
108
+ _background_tasks.append(task)
109
+ return task
110
+
111
+
112
+ def background_task_count() -> int:
113
+ """Number of REGISTERED background tasks (stopped ones are already gone)."""
114
+ with _background_lock:
115
+ return len(_background_tasks)
116
+
117
+
118
+ def stop_all_background_tasks() -> int:
119
+ """Stop and deregister every background task. Returns how many were stopped.
120
+
121
+ Each task deregisters itself, so there is no blanket ``clear()``: clearing
122
+ would also drop a task registered while this was running, leaving it ticking
123
+ but invisible in the registry.
124
+ """
125
+ with _background_lock:
126
+ snapshot = list(_background_tasks)
127
+ return sum(1 for task in snapshot if task.stop())
128
+
129
+
130
+ def _start_background_tasks(executor, shutdown) -> list:
131
+ """Start an asyncio runner for every registered task. Called once by _serve.
132
+
133
+ Each runner is bound to its handle so ``BackgroundTask.stop()`` can cancel a
134
+ task that is already ticking, not merely deregister one that never started.
135
+ """
136
+ with _background_lock:
137
+ # Never iterate the live list — a concurrent stop() mutates it.
138
+ snapshot = list(_background_tasks)
139
+
140
+ runners = []
141
+ for task in snapshot:
142
+ if task.stopped:
143
+ continue # stopped between registration and server start
144
+ runner = asyncio.create_task(
145
+ background_tick_loop(task.callback, task.interval, executor, shutdown)
146
+ )
147
+ task._runner = runner
148
+ # A stop() that landed while the runner was being created must still win,
149
+ # or the task would tick on with nothing holding a reference to cancel it.
150
+ if task.stopped:
151
+ runner.cancel()
152
+ continue
153
+ runners.append(runner)
154
+ return runners
50
155
 
51
156
 
52
157
  async def background_tick_loop(callback, interval: float, executor, shutdown):
@@ -2812,16 +2917,10 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
2812
2917
  # other of workers.
2813
2918
  import concurrent.futures
2814
2919
  _executor = concurrent.futures.ThreadPoolExecutor(
2815
- max_workers=max(len(_background_tasks), 2),
2920
+ max_workers=max(background_task_count(), 2),
2816
2921
  thread_name_prefix="tina4_bg",
2817
2922
  )
2818
- bg_tasks = []
2819
- for task_def in _background_tasks:
2820
- bg_tasks.append(asyncio.create_task(
2821
- background_tick_loop(
2822
- task_def["callback"], task_def["interval"], _executor, shutdown,
2823
- )
2824
- ))
2923
+ bg_tasks = _start_background_tasks(_executor, shutdown)
2825
2924
 
2826
2925
  await shutdown.wait()
2827
2926