tina4-python 3.13.42__tar.gz → 3.13.43__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 (158) hide show
  1. {tina4_python-3.13.42 → tina4_python-3.13.43}/PKG-INFO +1 -1
  2. {tina4_python-3.13.42 → tina4_python-3.13.43}/pyproject.toml +1 -1
  3. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/__init__.py +31 -8
  5. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/kafka_backend.py +18 -8
  6. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/mongo_backend.py +20 -7
  7. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/rabbitmq_backend.py +18 -8
  8. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue_backends/mongo_backend.py +23 -1
  9. {tina4_python-3.13.42 → tina4_python-3.13.43}/.gitignore +0 -0
  10. {tina4_python-3.13.42 → tina4_python-3.13.43}/README.md +0 -0
  11. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/HtmlElement.py +0 -0
  13. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/Testing.py +0 -0
  14. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/crud/__init__.py +0 -0
  31. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/__init__.py +0 -0
  32. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/adapter.py +0 -0
  33. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/connection.py +0 -0
  34. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/firebird.py +0 -0
  35. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/mongodb.py +0 -0
  36. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/mssql.py +0 -0
  37. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/mysql.py +0 -0
  38. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/odbc.py +0 -0
  39. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/postgres.py +0 -0
  40. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/database/sqlite.py +0 -0
  41. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/debug/__init__.py +0 -0
  42. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/debug/error_overlay.py +0 -0
  43. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/dev_admin/__init__.py +0 -0
  44. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/dev_admin/metrics.py +0 -0
  45. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/dev_admin/plan.py +0 -0
  46. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/dev_admin/project_index.py +0 -0
  47. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/docs.py +0 -0
  48. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/docstore/__init__.py +0 -0
  49. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/dotenv/__init__.py +0 -0
  50. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/env.py +0 -0
  51. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/frond/FROND.md +0 -0
  52. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/frond/__init__.py +0 -0
  53. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/frond/engine.py +0 -0
  54. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/auth/meta.json +0 -0
  55. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  56. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/database/meta.json +0 -0
  57. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  58. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/error-overlay/meta.json +0 -0
  59. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  60. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/orm/meta.json +0 -0
  61. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  62. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  63. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/queue/meta.json +0 -0
  64. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  65. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/rest-api/meta.json +0 -0
  66. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  67. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/templates/meta.json +0 -0
  68. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  69. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  70. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/graphql/__init__.py +0 -0
  71. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/i18n/__init__.py +0 -0
  72. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/mcp/__init__.py +0 -0
  73. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/mcp/protocol.py +0 -0
  74. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/mcp/tools.py +0 -0
  75. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/messenger/__init__.py +0 -0
  76. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/migration/__init__.py +0 -0
  77. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/migration/runner.py +0 -0
  78. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/orm/__init__.py +0 -0
  79. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/orm/fields.py +0 -0
  80. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/orm/model.py +0 -0
  81. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/__feedback/widget.js +0 -0
  82. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/css/tina4.css +0 -0
  83. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/css/tina4.min.css +0 -0
  84. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/favicon.ico +0 -0
  85. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/images/logo.svg +0 -0
  86. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  87. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/frond.js +0 -0
  88. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/frond.min.js +0 -0
  89. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  90. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  91. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/tina4.min.js +0 -0
  92. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/js/tina4js.min.js +0 -0
  93. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/swagger/index.html +0 -0
  94. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  95. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/query_builder/__init__.py +0 -0
  96. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/job.py +0 -0
  97. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue/lite_backend.py +0 -0
  98. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue_backends/__init__.py +0 -0
  99. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue_backends/kafka_backend.py +0 -0
  100. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/__init__.py +0 -0
  102. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  103. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_badges.scss +0 -0
  104. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  105. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_cards.scss +0 -0
  106. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_forms.scss +0 -0
  107. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_grid.scss +0 -0
  108. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_modals.scss +0 -0
  109. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_nav.scss +0 -0
  110. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_reset.scss +0 -0
  111. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_tables.scss +0 -0
  112. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_typography.scss +0 -0
  113. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  114. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/_variables.scss +0 -0
  115. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/base.scss +0 -0
  116. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/colors.scss +0 -0
  117. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/scss/tina4css/tina4.scss +0 -0
  118. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/seeder/__init__.py +0 -0
  119. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/service/__init__.py +0 -0
  120. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/session/__init__.py +0 -0
  121. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/session_handlers/__init__.py +0 -0
  122. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  123. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/session_handlers/redis_handler.py +0 -0
  124. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/session_handlers/valkey_handler.py +0 -0
  125. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/swagger/__init__.py +0 -0
  126. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/components/crud.twig +0 -0
  127. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  128. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  129. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/docker/python/Dockerfile +0 -0
  130. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  131. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/302.twig +0 -0
  132. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/401.twig +0 -0
  133. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/403.twig +0 -0
  134. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/404.twig +0 -0
  135. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/500.twig +0 -0
  136. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/502.twig +0 -0
  137. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/503.twig +0 -0
  138. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/errors/base.twig +0 -0
  139. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/frontend/README.md +0 -0
  140. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/templates/readme.md +0 -0
  141. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/test/__init__.py +0 -0
  142. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/test_client/__init__.py +0 -0
  143. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/validator/__init__.py +0 -0
  156. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/websocket/__init__.py +0 -0
  157. {tina4_python-3.13.42 → tina4_python-3.13.43}/tina4_python/websocket/backplane.py +0 -0
  158. {tina4_python-3.13.42 → tina4_python-3.13.43}/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.42
3
+ Version: 3.13.43
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.42"
3
+ version = "3.13.43"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -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.42"
11
+ __version__ = "3.13.43"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -61,7 +61,7 @@ def _resolve_backend(topic: str, backend: str | None, max_retries: int,
61
61
  # Consumer-group offsets manage redelivery — framework timeout N/A.
62
62
  return KafkaBackend(topic, max_retries)
63
63
  elif chosen in ("mongodb", "mongo"):
64
- return MongoBackend(topic, max_retries, visibility_timeout)
64
+ return MongoBackend(topic, max_retries, visibility_timeout, retry_backoff)
65
65
  else:
66
66
  raise ValueError(f"Unknown queue backend: {chosen!r}. Use 'file', 'rabbitmq', 'kafka', or 'mongodb'.")
67
67
 
@@ -82,9 +82,12 @@ class Queue:
82
82
  visibility_timeout: float | None = None):
83
83
  self.topic = topic
84
84
  self.max_retries = max_retries
85
- # Seconds to wait before a failed job is re-attempted (file backend).
85
+ # Seconds to wait before a failed job is re-attempted.
86
86
  # Default 0 = retry on the very next pop()/consume() iteration.
87
87
  self.retry_backoff = retry_backoff
88
+ # Remember an explicit backend= so retargeting a topic (produce/consume)
89
+ # does not silently fall back to the TINA4_QUEUE_BACKEND env default.
90
+ self._backend_choice = backend
88
91
  # Reservation/visibility timeout (seconds). A popped job is reserved for
89
92
  # this long; if the consumer dies before complete()/fail() the next
90
93
  # pop() reclaims it (at-least-once delivery). Falls back to
@@ -96,6 +99,24 @@ class Queue:
96
99
  self._backend = _resolve_backend(topic, backend, max_retries, retry_backoff,
97
100
  self.visibility_timeout)
98
101
 
102
+ def _retarget(self, topic: str) -> None:
103
+ """Point this queue (and its backend) at ``topic`` in place.
104
+
105
+ produce()/consume()/process() call this so a topic argument actually
106
+ changes which topic is read or written. Without it the argument was
107
+ accepted but ignored on the read path — pop() always used the
108
+ construction-time topic, so ``consume("other")`` silently drained the
109
+ wrong queue. Reuses the explicit backend choice (``_backend_choice``)
110
+ so an explicit ``backend=`` is preserved across the switch.
111
+ """
112
+ if topic == self.topic:
113
+ return
114
+ self.topic = topic
115
+ self._backend = _resolve_backend(
116
+ topic, self._backend_choice, self.max_retries,
117
+ self.retry_backoff, self.visibility_timeout,
118
+ )
119
+
99
120
  def push(self, data: dict, priority: int = 0, delay_seconds: int = 0):
100
121
  """Add a job to the queue. Returns job ID."""
101
122
  return self._backend.push(data, priority, delay_seconds)
@@ -134,6 +155,8 @@ class Queue:
134
155
  batch_size: Number of jobs to pass to handler at once (default 1).
135
156
  When > 1, handler receives a list of Jobs.
136
157
  """
158
+ if topic is not None:
159
+ self._retarget(topic)
137
160
  processed = 0
138
161
  while max_jobs is None or processed < max_jobs:
139
162
  if batch_size > 1:
@@ -235,15 +258,11 @@ class Queue:
235
258
  delay_seconds = max(0, offset)
236
259
 
237
260
  old_topic = self.topic
238
- self.topic = topic
239
- self._backend = _resolve_backend(topic, None, self.max_retries, self.retry_backoff,
240
- self.visibility_timeout)
261
+ self._retarget(topic)
241
262
  try:
242
263
  return self.push(data, priority, delay_seconds)
243
264
  finally:
244
- self.topic = old_topic
245
- self._backend = _resolve_backend(old_topic, None, self.max_retries, self.retry_backoff,
246
- self.visibility_timeout)
265
+ self._retarget(old_topic)
247
266
 
248
267
  def consume(self, topic: str = None, job_id: str = None, poll_interval: float = 1.0,
249
268
  iterations: int = 0, batch_size: int = 1):
@@ -282,6 +301,10 @@ class Queue:
282
301
  import time
283
302
 
284
303
  topic = topic or self.topic
304
+ # Honor the topic argument: point the queue (and the backend that
305
+ # pop()/job.complete()/job.fail() route through) at it. Previously the
306
+ # argument was ignored and consume() drained the construction-time topic.
307
+ self._retarget(topic)
285
308
 
286
309
  if job_id is not None:
287
310
  # Consume a specific job by ID — single yield, no polling
@@ -54,16 +54,21 @@ class KafkaBackend:
54
54
  def purge(self, status: str = "completed"):
55
55
  pass # Kafka does not support purging
56
56
 
57
- def retry_failed(self) -> int:
58
- jobs = self.failed()
57
+ def retry_failed(self, max_retries: int = None) -> int:
58
+ jobs = self.failed(max_retries)
59
59
  count = 0
60
60
  for job in jobs:
61
61
  if self.retry_job(job.get("id", "")):
62
62
  count += 1
63
63
  return count
64
64
 
65
- def failed(self) -> list[dict]:
66
- """Consume dead_letter topic, republish, return jobs under max_retries."""
65
+ def failed(self, max_retries: int = None) -> list[dict]:
66
+ """Consume dead_letter topic, republish, return jobs under max_retries.
67
+
68
+ Accepts max_retries to match the LiteBackend contract — Queue.retry_failed()
69
+ passes it as a kwarg, so without this signature the call raised TypeError.
70
+ """
71
+ mr = max_retries if max_retries is not None else self._max_retries
67
72
  dl_topic = f"{self._topic}.dead_letter"
68
73
  results = []
69
74
  requeue = []
@@ -73,7 +78,7 @@ class KafkaBackend:
73
78
  break
74
79
  payload = msg.get("payload", msg)
75
80
  attempts = msg.get("attempts", 0)
76
- if attempts < self._max_retries:
81
+ if attempts < mr:
77
82
  results.append({"id": msg.get("id"), "data": payload,
78
83
  "attempts": attempts, "error": msg.get("error")})
79
84
  requeue.append(msg)
@@ -81,8 +86,13 @@ class KafkaBackend:
81
86
  self._backend.enqueue(dl_topic, msg)
82
87
  return results
83
88
 
84
- def dead_letters(self) -> list[dict]:
85
- """Consume dead_letter topic, republish, return jobs at/over max_retries."""
89
+ def dead_letters(self, max_retries: int = None) -> list[dict]:
90
+ """Consume dead_letter topic, republish, return jobs at/over max_retries.
91
+
92
+ Accepts max_retries to match the LiteBackend contract — Queue.dead_letters()
93
+ passes it as a kwarg, so without this signature the call raised TypeError.
94
+ """
95
+ mr = max_retries if max_retries is not None else self._max_retries
86
96
  dl_topic = f"{self._topic}.dead_letter"
87
97
  results = []
88
98
  requeue = []
@@ -92,7 +102,7 @@ class KafkaBackend:
92
102
  break
93
103
  payload = msg.get("payload", msg)
94
104
  attempts = msg.get("attempts", 0)
95
- if attempts >= self._max_retries:
105
+ if attempts >= mr:
96
106
  results.append({"id": msg.get("id"), "data": payload,
97
107
  "attempts": attempts, "error": msg.get("error")})
98
108
  requeue.append(msg)
@@ -23,11 +23,12 @@ def _future(seconds: int) -> str:
23
23
  class MongoBackend:
24
24
  """Backend adapter wrapping MongoBackend for the unified Queue API."""
25
25
 
26
- def __init__(self, topic: str, max_retries: int, visibility_timeout: float = 300.0):
26
+ def __init__(self, topic: str, max_retries: int, visibility_timeout: float = 300.0,
27
+ retry_backoff: float = 0):
27
28
  from tina4_python.queue_backends import MongoConnector as _MongoBackend
28
29
 
29
30
  url = os.environ.get("TINA4_QUEUE_URL", "")
30
- config = {"visibility_timeout": visibility_timeout}
31
+ config = {"visibility_timeout": visibility_timeout, "retry_backoff": retry_backoff}
31
32
  if url:
32
33
  config["uri"] = url
33
34
  self._backend = _MongoBackend(**config)
@@ -70,12 +71,19 @@ class MongoBackend:
70
71
  if status == "pending":
71
72
  self._backend.clear(self._topic)
72
73
 
73
- def retry_failed(self) -> int:
74
+ def retry_failed(self, max_retries: int = None) -> int:
75
+ # Accept max_retries to match the LiteBackend contract (Queue passes it
76
+ # as a kwarg) — without this signature, Queue.retry_failed() raised
77
+ # TypeError on MongoDB.
78
+ mr = max_retries if max_retries is not None else self._max_retries
74
79
  self._backend._ensure_connected()
75
80
  result = self._backend._collection.update_many(
76
81
  {"topic": self._topic, "status": "failed",
77
- "attempts": {"$lt": self._max_retries}},
78
- {"$set": {"status": "pending", "error": None}},
82
+ "attempts": {"$lt": mr}},
83
+ # Reset available_at so re-queued failed jobs are visible again
84
+ # (they were reserved with available_at in the future at dequeue).
85
+ {"$set": {"status": "pending", "error": None, "available_at": _now(),
86
+ "reserved_at": None}},
79
87
  )
80
88
  return result.modified_count
81
89
 
@@ -90,8 +98,13 @@ class MongoBackend:
90
98
  "attempts": d.get("attempts", 0), "error": d.get("error")}
91
99
  for d in docs]
92
100
 
93
- def dead_letters(self) -> list[dict]:
94
- """Query the dead_letter collection in MongoDB."""
101
+ def dead_letters(self, max_retries: int = None) -> list[dict]:
102
+ """Query the dead_letter collection in MongoDB.
103
+
104
+ Accepts max_retries (unused — dead letters are terminal) to match the
105
+ LiteBackend contract; Queue.dead_letters() passes it as a kwarg, so
106
+ without this parameter the call raised TypeError on MongoDB.
107
+ """
95
108
  self._backend._ensure_connected()
96
109
  dl_topic = f"{self._topic}.dead_letter"
97
110
  docs = self._backend._collection.find({"topic": dl_topic})
@@ -82,8 +82,8 @@ class RabbitMQBackend:
82
82
  if status == "pending":
83
83
  self._backend.clear(self._topic)
84
84
 
85
- def retry_failed(self) -> int:
86
- jobs = self.failed()
85
+ def retry_failed(self, max_retries: int = None) -> int:
86
+ jobs = self.failed(max_retries)
87
87
  count = 0
88
88
  for job in jobs:
89
89
  job_id = job.get("id", "")
@@ -91,8 +91,13 @@ class RabbitMQBackend:
91
91
  count += 1
92
92
  return count
93
93
 
94
- def failed(self) -> list[dict]:
95
- """Drain the dead_letter queue, re-enqueue, and return jobs still under max_retries."""
94
+ def failed(self, max_retries: int = None) -> list[dict]:
95
+ """Drain the dead_letter queue, re-enqueue, and return jobs still under max_retries.
96
+
97
+ Accepts max_retries to match the LiteBackend contract — Queue.retry_failed()
98
+ passes it as a kwarg, so without this signature the call raised TypeError.
99
+ """
100
+ mr = max_retries if max_retries is not None else self._max_retries
96
101
  dl_topic = f"{self._topic}.dead_letter"
97
102
  results = []
98
103
  requeue = []
@@ -102,7 +107,7 @@ class RabbitMQBackend:
102
107
  break
103
108
  payload = msg.get("payload", msg)
104
109
  attempts = msg.get("attempts", 0)
105
- if attempts < self._max_retries:
110
+ if attempts < mr:
106
111
  results.append({"id": msg.get("id"), "data": payload,
107
112
  "attempts": attempts, "error": msg.get("error")})
108
113
  requeue.append(msg)
@@ -110,8 +115,13 @@ class RabbitMQBackend:
110
115
  self._backend.enqueue(dl_topic, msg)
111
116
  return results
112
117
 
113
- def dead_letters(self) -> list[dict]:
114
- """Drain the dead_letter queue, re-enqueue, and return jobs at/over max_retries."""
118
+ def dead_letters(self, max_retries: int = None) -> list[dict]:
119
+ """Drain the dead_letter queue, re-enqueue, and return jobs at/over max_retries.
120
+
121
+ Accepts max_retries to match the LiteBackend contract — Queue.dead_letters()
122
+ passes it as a kwarg, so without this signature the call raised TypeError.
123
+ """
124
+ mr = max_retries if max_retries is not None else self._max_retries
115
125
  dl_topic = f"{self._topic}.dead_letter"
116
126
  results = []
117
127
  requeue = []
@@ -121,7 +131,7 @@ class RabbitMQBackend:
121
131
  break
122
132
  payload = msg.get("payload", msg)
123
133
  attempts = msg.get("attempts", 0)
124
- if attempts >= self._max_retries:
134
+ if attempts >= mr:
125
135
  results.append({"id": msg.get("id"), "data": payload,
126
136
  "attempts": attempts, "error": msg.get("error")})
127
137
  requeue.append(msg)
@@ -56,6 +56,14 @@ class MongoConnector:
56
56
  ))
57
57
  except (TypeError, ValueError):
58
58
  self._visibility_timeout = 300.0
59
+ # Seconds to delay a requeued (rejected/retried) job before it is
60
+ # eligible again. Default 0 = available on the very next dequeue, so a
61
+ # fail()'d job retries immediately (matching the file backend) instead
62
+ # of waiting out the visibility window.
63
+ try:
64
+ self._retry_backoff = float(config.get("retry_backoff", 0))
65
+ except (TypeError, ValueError):
66
+ self._retry_backoff = 0.0
59
67
 
60
68
  self._pymongo = None
61
69
  self._client = None
@@ -143,6 +151,13 @@ class MongoConnector:
143
151
 
144
152
  result = doc.get("data", {})
145
153
  result["id"] = doc["_id"]
154
+ # Surface the LIVE document-level attempts/priority, not the push-time
155
+ # snapshot stored inside ``data``. reclaim_expired()/reject() increment
156
+ # the top-level ``attempts``; without this the consumer always saw the
157
+ # original 0, so fail()'s ``attempts >= max_retries`` check never tripped
158
+ # and a job could be retried forever instead of dead-lettering.
159
+ result["attempts"] = doc.get("attempts", result.get("attempts", 0))
160
+ result["priority"] = doc.get("priority", result.get("priority", 0))
146
161
  return result
147
162
 
148
163
  def reclaim_expired(self, topic: str, max_retries: int) -> int:
@@ -201,9 +216,16 @@ class MongoConnector:
201
216
  """Reject a message. Optionally requeue it."""
202
217
  self._ensure_connected()
203
218
  if requeue:
219
+ # Reset available_at so the requeued job is visible again right away
220
+ # (or after retry_backoff). dequeue() pushed available_at out to the
221
+ # reservation expiry; leaving it there stranded a fail()'d job for the
222
+ # full visibility window instead of retrying it on the next pop().
223
+ available = _future(self._retry_backoff) if self._retry_backoff > 0 else _now()
204
224
  self._collection.update_one(
205
225
  {"_id": message_id, "topic": topic},
206
- {"$set": {"status": "pending"}, "$inc": {"attempts": 1}},
226
+ {"$set": {"status": "pending", "available_at": available,
227
+ "reserved_at": None},
228
+ "$inc": {"attempts": 1}},
207
229
  )
208
230
  else:
209
231
  self._collection.update_one(
File without changes