simple-module-background-tasks 0.0.37__tar.gz → 0.0.39__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 (182) hide show
  1. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/.gitignore +1 -2
  2. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/PKG-INFO +7 -7
  3. simple_module_background_tasks-0.0.39/background_tasks/__init__.py +39 -0
  4. simple_module_background_tasks-0.0.39/background_tasks/_lifecycle.py +174 -0
  5. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/_signal_bus.py +2 -1
  6. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/_signal_support.py +53 -1
  7. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/after_commit.py +18 -2
  8. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/constants.py +30 -4
  9. simple_module_background_tasks-0.0.39/background_tasks/contracts/logs.py +31 -0
  10. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/contracts/outbox.py +1 -1
  11. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/contracts/schemas.py +52 -3
  12. simple_module_background_tasks-0.0.39/background_tasks/db_settings.py +34 -0
  13. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/deps.py +9 -0
  14. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/endpoints/api_admin.py +5 -4
  15. simple_module_background_tasks-0.0.39/background_tasks/endpoints/api_status.py +70 -0
  16. simple_module_background_tasks-0.0.39/background_tasks/endpoints/logs_api.py +62 -0
  17. simple_module_background_tasks-0.0.39/background_tasks/endpoints/logs_views.py +44 -0
  18. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/endpoints/outbox_api.py +4 -3
  19. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/endpoints/outbox_views.py +6 -2
  20. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/endpoints/views.py +4 -2
  21. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/filters.py +4 -3
  22. simple_module_background_tasks-0.0.39/background_tasks/locales/de.json +222 -0
  23. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/locales/en.json +31 -6
  24. simple_module_background_tasks-0.0.39/background_tasks/log_purge.py +17 -0
  25. simple_module_background_tasks-0.0.39/background_tasks/log_service.py +88 -0
  26. simple_module_background_tasks-0.0.39/background_tasks/log_sink.py +136 -0
  27. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/models.py +69 -1
  28. simple_module_background_tasks-0.0.39/background_tasks/module.py +180 -0
  29. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_claim.py +5 -4
  30. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_loop.py +2 -2
  31. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_service.py +17 -20
  32. simple_module_background_tasks-0.0.39/background_tasks/outbox_sweep.py +58 -0
  33. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_transport.py +10 -5
  34. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_worker.py +19 -5
  35. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/Detail.tsx +28 -11
  36. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/Index.tsx +12 -14
  37. simple_module_background_tasks-0.0.39/background_tasks/pages/Logs.tsx +92 -0
  38. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/Outbox.tsx +23 -13
  39. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/Workers.tsx +17 -24
  40. simple_module_background_tasks-0.0.39/background_tasks/pages/api.generated.ts +965 -0
  41. simple_module_background_tasks-0.0.39/background_tasks/pages/api.ts +9 -0
  42. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/ExecutionsTable.tsx +2 -33
  43. simple_module_background_tasks-0.0.39/background_tasks/pages/components/LogLevelSelect.tsx +29 -0
  44. simple_module_background_tasks-0.0.39/background_tasks/pages/components/LogTable.tsx +76 -0
  45. simple_module_background_tasks-0.0.39/background_tasks/pages/components/LogsPanel.tsx +46 -0
  46. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/OutboxTable.tsx +7 -26
  47. simple_module_background_tasks-0.0.39/background_tasks/pages/components/ProgressCard.tsx +43 -0
  48. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/WorkerHealthBanner.tsx +12 -10
  49. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/constants.ts +5 -0
  50. simple_module_background_tasks-0.0.39/background_tasks/pages/logs.ts +53 -0
  51. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/outbox.ts +23 -32
  52. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/retry.ts +38 -41
  53. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/retry_service.py +13 -6
  54. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/runner.py +16 -2
  55. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/services.py +4 -0
  56. simple_module_background_tasks-0.0.39/background_tasks/settings.py +147 -0
  57. simple_module_background_tasks-0.0.37/background_tasks/settings.py → simple_module_background_tasks-0.0.39/background_tasks/settings_celery.py +14 -106
  58. simple_module_background_tasks-0.0.39/background_tasks/settings_logs.py +45 -0
  59. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/signals.py +14 -55
  60. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/sync_db.py +48 -3
  61. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/task_heartbeat.py +56 -6
  62. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/tasks.py +13 -13
  63. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/tenant_context.py +80 -6
  64. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/worker_settings.py +1 -1
  65. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/package.json +2 -1
  66. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/pyproject.toml +7 -7
  67. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/conftest.py +18 -1
  68. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_background_tasks_settings.py +3 -3
  69. simple_module_background_tasks-0.0.39/tests/test_bg_db_timeouts.py +95 -0
  70. simple_module_background_tasks-0.0.39/tests/test_bg_inprocess_sweep.py +187 -0
  71. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_invalidation.py +1 -1
  72. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_invalidation_degraded.py +12 -8
  73. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_invalidation_redis.py +43 -31
  74. simple_module_background_tasks-0.0.39/tests/test_bg_log_api.py +118 -0
  75. simple_module_background_tasks-0.0.39/tests/test_bg_log_purge.py +45 -0
  76. simple_module_background_tasks-0.0.39/tests/test_bg_log_sink.py +102 -0
  77. simple_module_background_tasks-0.0.39/tests/test_bg_log_sink_loop.py +60 -0
  78. simple_module_background_tasks-0.0.39/tests/test_bg_log_sink_nul.py +26 -0
  79. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_eager_platform.py +4 -11
  80. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_loop_interrupt.py +1 -1
  81. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_retry_policy.py +4 -2
  82. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_swallowed_cancel.py +1 -1
  83. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_thread_loops.py +19 -1
  84. simple_module_background_tasks-0.0.39/tests/test_bg_pagination_bounds.py +51 -0
  85. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_redis_url.py +3 -3
  86. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_runner_settings.py +7 -1
  87. simple_module_background_tasks-0.0.39/tests/test_bg_startup_log_masking.py +26 -0
  88. simple_module_background_tasks-0.0.39/tests/test_bg_task_progress.py +139 -0
  89. simple_module_background_tasks-0.0.39/tests/test_bg_task_progress_audit.py +53 -0
  90. simple_module_background_tasks-0.0.39/tests/test_bg_task_status_api.py +122 -0
  91. simple_module_background_tasks-0.0.39/tests/test_bg_users_ordering.py +30 -0
  92. simple_module_background_tasks-0.0.39/tests/test_bg_worker_audit.py +129 -0
  93. simple_module_background_tasks-0.0.39/tests/test_bg_worker_audit_bookkeeping.py +96 -0
  94. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_worker_settings.py +1 -1
  95. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_log_context.py +5 -5
  96. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_outbox_concurrency.py +4 -1
  97. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_worker_inspector.py +4 -1
  98. simple_module_background_tasks-0.0.39/tests-js/background_tasks_DetailPage.test.tsx +158 -0
  99. simple_module_background_tasks-0.0.39/tests-js/background_tasks_IndexPage.test.tsx +180 -0
  100. simple_module_background_tasks-0.0.39/tests-js/background_tasks_LogsTab.test.tsx +56 -0
  101. simple_module_background_tasks-0.0.39/tests-js/background_tasks_OutboxPage.test.tsx +185 -0
  102. simple_module_background_tasks-0.0.39/tests-js/background_tasks_WorkerHealthBanner.test.tsx +63 -0
  103. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests-js/outbox.test.ts +9 -16
  104. simple_module_background_tasks-0.0.39/tests-js/stub-fetch.ts +18 -0
  105. simple_module_background_tasks-0.0.37/background_tasks/__init__.py +0 -19
  106. simple_module_background_tasks-0.0.37/background_tasks/module.py +0 -287
  107. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/LICENSE +0 -0
  108. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/README.md +0 -0
  109. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/celery_app.py +0 -0
  110. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/config_guards.py +0 -0
  111. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/contracts/__init__.py +0 -0
  112. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/contracts/events.py +0 -0
  113. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/endpoints/__init__.py +0 -0
  114. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/health.py +0 -0
  115. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/invalidation.py +0 -0
  116. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/log_context.py +0 -0
  117. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_clock.py +0 -0
  118. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_dispatch.py +0 -0
  119. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_guard.py +0 -0
  120. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/outbox_support.py +0 -0
  121. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/DetailFacts.tsx +0 -0
  122. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/ExecutionRow.tsx +0 -0
  123. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/PayloadCards.tsx +0 -0
  124. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/RetryAllDialog.tsx +0 -0
  125. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
  126. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/RoutesTable.tsx +0 -0
  127. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/StatusPill.tsx +0 -0
  128. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/StatusStrip.tsx +0 -0
  129. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/TaskFilters.tsx +0 -0
  130. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/TasksEmpty.tsx +0 -0
  131. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/TracebackCard.tsx +0 -0
  132. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/WorkerCard.tsx +0 -0
  133. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/components/WorkerEmptyStates.tsx +0 -0
  134. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/pages/worker-health.ts +0 -0
  135. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/py.typed +0 -0
  136. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/service.py +0 -0
  137. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/settings_env_checks.py +0 -0
  138. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/task_routing.py +0 -0
  139. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/worker_inspector.py +0 -0
  140. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/background_tasks/workers_state.py +0 -0
  141. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_admin_api.py +0 -0
  142. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_background_tasks_after_commit.py +0 -0
  143. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_background_tasks_heartbeat.py +0 -0
  144. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_background_tasks_tz_columns.py +0 -0
  145. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_beat_schedule.py +0 -0
  146. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_config_guards.py +0 -0
  147. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_index_has_executions.py +0 -0
  148. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_index_worker_presence.py +0 -0
  149. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_list_item_payload.py +0 -0
  150. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_fence_guard.py +0 -0
  151. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_guard_savepoint.py +0 -0
  152. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_publish_scope.py +0 -0
  153. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_outbox_worker_chain.py +0 -0
  154. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_page_clamp.py +0 -0
  155. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_quiet_sweep_unrecorded.py +0 -0
  156. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_redis_health.py +0 -0
  157. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_routes_override_api.py +0 -0
  158. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_service.py +0 -0
  159. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_settings_env.py +0 -0
  160. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_bg_task_routing.py +0 -0
  161. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_detail_props.py +0 -0
  162. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_execution_tenancy.py +0 -0
  163. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_index_filters.py +0 -0
  164. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_outbox_admin.py +0 -0
  165. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_outbox_delivery.py +0 -0
  166. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_outbox_tenancy.py +0 -0
  167. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_outbox_worker_task.py +0 -0
  168. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_retry_failed_bulk.py +0 -0
  169. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_retry_failed_guards.py +0 -0
  170. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_retry_sweep_ordering.py +0 -0
  171. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_signal_tenant_stamp.py +0 -0
  172. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_signals.py +0 -0
  173. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_sweep_stuck.py +0 -0
  174. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_sync_db.py +0 -0
  175. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_sync_db_fork.py +0 -0
  176. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_tenant_context.py +0 -0
  177. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_worker_tenancy.py +0 -0
  178. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_workers_endpoints.py +0 -0
  179. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests/test_workers_uptime.py +0 -0
  180. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests-js/retry.test.ts +0 -0
  181. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tests-js/task-formatters.test.ts +0 -0
  182. {simple_module_background_tasks-0.0.37 → simple_module_background_tasks-0.0.39}/tsconfig.json +0 -0
@@ -7,8 +7,6 @@ build/
7
7
  .venv/
8
8
  *.egg
9
9
 
10
- # UV
11
- uv.lock
12
10
 
13
11
  # Node
14
12
  node_modules/
@@ -65,3 +63,4 @@ host/client_app/.playwright-cli/*
65
63
  .qa/
66
64
  .verify/
67
65
  /openapi.json
66
+ qa-shots/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_background_tasks
3
- Version: 0.0.37
3
+ Version: 0.0.39
4
4
  Summary: Celery + Redis task queue with admin UI for monitoring and retrying failed/stuck tasks
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -21,12 +21,12 @@ Classifier: Topic :: Internet :: WWW/HTTP
21
21
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
- Requires-Dist: celery[redis]>=5.4
25
- Requires-Dist: redis>=5
26
- Requires-Dist: simple-module-core==0.0.37
27
- Requires-Dist: simple-module-db==0.0.37
28
- Requires-Dist: simple-module-hosting==0.0.37
29
- Requires-Dist: simple-module-settings==0.0.37
24
+ Requires-Dist: celery[redis]<6,>=5.4
25
+ Requires-Dist: redis<7,>=5
26
+ Requires-Dist: simple-module-core==0.0.39
27
+ Requires-Dist: simple-module-db==0.0.39
28
+ Requires-Dist: simple-module-hosting==0.0.39
29
+ Requires-Dist: simple-module-settings==0.0.39
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # simple_module_background_tasks
@@ -0,0 +1,39 @@
1
+ """BackgroundTasks module — Celery + Redis task queue with admin UI."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from background_tasks.after_commit import enqueue_after_commit
6
+ from background_tasks.log_context import (
7
+ bind_task_context,
8
+ get_log_context,
9
+ install_log_filter,
10
+ )
11
+
12
+ if TYPE_CHECKING:
13
+ from background_tasks.task_heartbeat import heartbeat
14
+ from background_tasks.worker_settings import settings_for
15
+
16
+ _LAZY_HEARTBEAT = "heartbeat"
17
+ _LAZY_SETTINGS = "settings_for"
18
+
19
+
20
+ def __getattr__(name: str) -> Any:
21
+ if name == _LAZY_HEARTBEAT:
22
+ from background_tasks.task_heartbeat import heartbeat
23
+
24
+ return heartbeat
25
+ if name == _LAZY_SETTINGS:
26
+ from background_tasks.worker_settings import settings_for
27
+
28
+ return settings_for
29
+ raise AttributeError(name)
30
+
31
+
32
+ __all__ = [
33
+ "bind_task_context",
34
+ "enqueue_after_commit",
35
+ "get_log_context",
36
+ "heartbeat",
37
+ "install_log_filter",
38
+ "settings_for",
39
+ ]
@@ -0,0 +1,174 @@
1
+ """Startup/shutdown wiring for the BackgroundTasks module.
2
+
3
+ ``BackgroundTasksModule`` keeps the ``on_startup`` / ``on_shutdown`` hooks and
4
+ delegates here, so the module file stays a declaration of what the module
5
+ contributes and this file owns the order things are installed and torn down.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from fastapi import FastAPI
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ async def startup(app: FastAPI, module_name: str) -> None:
20
+ """Build the Celery app, install signal handlers, hand-off to worker."""
21
+ import asyncio
22
+
23
+ from simple_module_hosting.log_sinks import redact
24
+
25
+ from background_tasks.celery_app import build_celery
26
+ from background_tasks.db_settings import worker_database_options
27
+ from background_tasks.log_sink import install_log_sink
28
+ from background_tasks.signals import bind_event_bus
29
+ from background_tasks.sync_db import configure_database, set_database_url
30
+ from background_tasks.tenant_context import set_default_tenant
31
+
32
+ services = app.state.background_tasks
33
+ # Pin the sync engine to the same URL the host's async settings
34
+ # resolved — pydantic-settings reads ``.env`` but never propagates
35
+ # to ``os.environ``, so signals would otherwise fall back to the
36
+ # SQLite default and silently drop ``TaskExecution`` rows.
37
+ host = app.state.sm.settings
38
+ multi = bool(getattr(host, "multi_tenant", False))
39
+ default_tenant = None if multi else getattr(host, "default_tenant", "") or None
40
+ set_database_url(host.database_url, tenant_strict=multi, default_tenant=default_tenant)
41
+ configure_database(worker_database_options(host, services.settings))
42
+ set_default_tenant(default_tenant)
43
+ # build_celery imports `signals` for side effects and runs
44
+ # `autodiscover_tasks` across every installed module.
45
+ services.celery = build_celery(services.settings)
46
+ # Let signal handlers hop onto the API loop to publish events.
47
+ # In a standalone worker process this bind never runs, so signals
48
+ # stay a silent no-op — that's the documented cross-process limit.
49
+ bind_event_bus(app.state.sm.event_bus, asyncio.get_running_loop())
50
+ install_durable_transport(app)
51
+ install_log_sink(services.settings)
52
+ logger.info(
53
+ "BackgroundTasks: Celery app ready (broker=%s, queue=%s)",
54
+ redact(services.settings.broker_url),
55
+ services.settings.task_default_queue,
56
+ )
57
+
58
+ await start_invalidation_transport(app)
59
+
60
+ # Registered here, not in register_health_checks: the Celery app does
61
+ # not exist until this hook builds it, and the check must follow later
62
+ # settings changes rather than pinning the boot-time instance.
63
+ from simple_module_core.health import HealthCheck
64
+
65
+ from background_tasks.health import CHECK_REDIS, build_redis_check
66
+
67
+ app.state.sm.health_registry.add(
68
+ HealthCheck(
69
+ name=CHECK_REDIS,
70
+ check=build_redis_check(app),
71
+ module=module_name,
72
+ # On demand only, like the mailer and storage checks. An
73
+ # earlier version had this on the readiness probe, reasoning
74
+ # that Redis is infrastructure this app owns rather than a
75
+ # third party. That conflates two different questions: whether
76
+ # this process can serve requests, and whether the system is
77
+ # fully functional. Failing readiness pulls the web tier out of
78
+ # the load balancer, turning "background jobs are backed up"
79
+ # into "the site is down" — and it made /health/ready report
80
+ # unhealthy on every deployment without Redis, which CI caught.
81
+ # The wizard's connection step and the settings screen's "Test
82
+ # connection" both run it explicitly, which is what it is for.
83
+ probe=False,
84
+ )
85
+ )
86
+
87
+
88
+ def install_durable_transport(app: FastAPI) -> None:
89
+ """Make ``bus.publish_durable`` write to this module's outbox (GH #459)."""
90
+ from background_tasks.constants import OutboxMode
91
+ from background_tasks.outbox_sweep import start_sweep
92
+ from background_tasks.outbox_transport import OutboxTransport
93
+ from background_tasks.outbox_worker import use_bus
94
+
95
+ bus = app.state.sm.event_bus
96
+ services = app.state.background_tasks
97
+ mode = (
98
+ OutboxMode.INPROCESS
99
+ if services.celery.conf.task_always_eager
100
+ else OutboxMode(services.settings.outbox_mode)
101
+ )
102
+ transport = OutboxTransport(services=services, bus=bus, db_state=app.state.sm.db, mode=mode)
103
+ bus.durable_transport = transport
104
+ if mode == OutboxMode.INPROCESS:
105
+ start_sweep(services, app.state.sm.db.session_factory, bus, transport.policy)
106
+ # An eager in-process dispatch delivers with the app's bus, which already
107
+ # carries every module's durable subscriptions.
108
+ use_bus(bus)
109
+
110
+
111
+ async def start_invalidation_transport(app: FastAPI) -> None:
112
+ """Hand the framework bus the Redis connection this module configures.
113
+
114
+ The transport is installed here rather than in a registration hook
115
+ because it needs a running event loop for its listener task, and it is
116
+ installed by *this* module because it owns the Redis URL — a cache-owning
117
+ module opening its own connection is the duplication GH #318 exists to
118
+ avoid.
119
+
120
+ A worker process that runs Celery without the FastAPI app never reaches
121
+ this, so it publishes nothing and hears nothing. That is the same
122
+ documented cross-process limit as ``bind_event_bus`` above: a task that
123
+ invalidates a cache should do it through an API call or accept the TTL.
124
+ """
125
+ from background_tasks.invalidation import RedisInvalidationTransport
126
+
127
+ services = app.state.background_tasks
128
+ s = services.settings
129
+ if not s.broadcast_invalidations:
130
+ logger.info(
131
+ "BackgroundTasks: cross-worker cache invalidation disabled "
132
+ "(broadcast_invalidations=false) — per-process caches expire on their own TTL"
133
+ )
134
+ return
135
+
136
+ bus = app.state.sm.invalidation
137
+ transport = RedisInvalidationTransport(bus, s.broker_url, s.invalidation_channel)
138
+ await transport.start()
139
+ bus.set_transport(transport)
140
+ services.invalidation_transport = transport
141
+ logger.info(
142
+ "BackgroundTasks: invalidation transport on %r for channel(s) %s",
143
+ s.invalidation_channel,
144
+ ", ".join(bus.channels) or "(none subscribed)",
145
+ )
146
+
147
+
148
+ async def shutdown(app: FastAPI) -> None:
149
+ from background_tasks.log_sink import uninstall_log_sink
150
+ from background_tasks.outbox_sweep import stop_sweep
151
+ from background_tasks.outbox_worker import reset_worker_state
152
+ from background_tasks.signals import unbind_event_bus
153
+ from background_tasks.sync_db import dispose_sync_engine
154
+
155
+ await stop_sweep(app.state.background_tasks)
156
+ bus = getattr(app.state.sm, "event_bus", None)
157
+ if bus is not None:
158
+ drain = getattr(bus.durable_transport, "drain", None)
159
+ if drain is not None:
160
+ # Eager deliveries still running on this loop.
161
+ await drain()
162
+ bus.durable_transport = None
163
+ reset_worker_state()
164
+ services = app.state.background_tasks
165
+ if services.invalidation_transport is not None:
166
+ app.state.sm.invalidation.clear_transport()
167
+ await services.invalidation_transport.stop()
168
+ services.invalidation_transport = None
169
+ if services.celery is not None:
170
+ services.celery.close()
171
+ unbind_event_bus()
172
+ # Before the engine goes: the final flush still needs it.
173
+ uninstall_log_sink()
174
+ dispose_sync_engine()
@@ -7,6 +7,7 @@ re-exports :func:`bind_event_bus` / :func:`unbind_event_bus`.
7
7
  from __future__ import annotations
8
8
 
9
9
  import asyncio
10
+ import concurrent.futures
10
11
  import logging
11
12
  from typing import Any
12
13
 
@@ -53,7 +54,7 @@ def publish_from_signal(event: Any) -> None:
53
54
  future.add_done_callback(_log_publish_failure)
54
55
 
55
56
 
56
- def _log_publish_failure(future: asyncio.Future[Any]) -> None:
57
+ def _log_publish_failure(future: concurrent.futures.Future[Any]) -> None:
57
58
  if future.cancelled():
58
59
  return
59
60
  exc = future.exception()
@@ -7,14 +7,21 @@ These helpers are intentionally side-effect-free except for the DB write in
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import logging
10
11
  import traceback as tb_mod
12
+ from collections.abc import Callable
11
13
  from datetime import UTC, datetime
12
14
  from typing import Any
13
15
 
14
16
  from sqlalchemy import select
15
17
  from sqlalchemy.orm import Session
18
+ from sqlmodel import col
16
19
 
20
+ from background_tasks.constants import INTERNAL_TASK_SWEEP_OUTBOX, TaskStatus
17
21
  from background_tasks.models import TaskExecution
22
+ from background_tasks.sync_db import sync_session
23
+
24
+ logger = logging.getLogger(__name__)
18
25
 
19
26
 
20
27
  def now_utc() -> datetime:
@@ -47,7 +54,7 @@ def upsert_by_celery_id(
47
54
  """
48
55
  row: TaskExecution | None = None
49
56
  if celery_task_id:
50
- stmt = select(TaskExecution).where(TaskExecution.celery_task_id == celery_task_id)
57
+ stmt = select(TaskExecution).where(col(TaskExecution.celery_task_id) == celery_task_id)
51
58
  row = session.execute(stmt).scalar_one_or_none()
52
59
  if row is None:
53
60
  row = TaskExecution(celery_task_id=celery_task_id, **defaults)
@@ -103,3 +110,48 @@ def render_traceback(einfo: Any, exception: BaseException | None) -> str | None:
103
110
  if exception is not None:
104
111
  return "".join(tb_mod.format_exception(type(exception), exception, exception.__traceback__))
105
112
  return None
113
+
114
+
115
+ # Beat ticks that fire every few seconds and are a no-op almost every time;
116
+ # recording each run buried real work (QA, GH #459). Only a run that went wrong
117
+ # is worth a row.
118
+ _UNRECORDED_WHEN_QUIET = frozenset({INTERNAL_TASK_SWEEP_OUTBOX})
119
+ _QUIET_STATUSES = frozenset({None, TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.SUCCESS})
120
+
121
+
122
+ def apply_signal(
123
+ handler: str,
124
+ *,
125
+ celery_task_id: str | None,
126
+ defaults: dict[str, Any],
127
+ after: Callable[[TaskExecution], None] | None = None,
128
+ tenant_id: str | None = None,
129
+ owner_user_id: str | None = None,
130
+ ) -> TaskExecution | None:
131
+ """Open a sync session, upsert by celery_task_id, log on failure.
132
+
133
+ Returns the upserted row (or ``None`` if the session raised) so callers
134
+ can read DB-assigned fields like ``id`` without a second round trip.
135
+
136
+ ``tenant_id`` and ``owner_user_id`` (the message's) are written only when
137
+ known, so a signal whose message carries neither never blanks a row already
138
+ stamped.
139
+ """
140
+ if (
141
+ defaults.get("task_name") in _UNRECORDED_WHEN_QUIET
142
+ and defaults.get("status") in _QUIET_STATUSES
143
+ ):
144
+ return None
145
+ if tenant_id:
146
+ defaults = {**defaults, "tenant_id": tenant_id}
147
+ if owner_user_id:
148
+ defaults = {**defaults, "owner_user_id": owner_user_id}
149
+ try:
150
+ with sync_session() as session:
151
+ row = upsert_by_celery_id(session, celery_task_id=celery_task_id, defaults=defaults)
152
+ if after is not None:
153
+ after(row)
154
+ return row
155
+ except Exception:
156
+ logger.exception("%s failed for task_id=%s", handler, celery_task_id)
157
+ return None
@@ -30,13 +30,24 @@ import uuid
30
30
  from collections.abc import Callable, Sequence
31
31
  from typing import Any
32
32
 
33
- from celery import current_app
34
33
  from simple_module_db import RequestSession, bind_current_tenant, mark_written
35
34
  from simple_module_db.transaction import SESSION_START_KEY
36
35
  from sqlalchemy import event
37
36
  from sqlalchemy.ext.asyncio import AsyncSession
38
37
  from sqlalchemy.orm import Session, SessionTransaction
39
38
 
39
+
40
+ class _LazyCurrentApp:
41
+ """``celery.current_app``, imported on first use so importing this module stays light."""
42
+
43
+ def __getattr__(self, name: str) -> Any:
44
+ from celery import current_app as _current_app
45
+
46
+ return getattr(_current_app, name)
47
+
48
+
49
+ current_app: Any = _LazyCurrentApp()
50
+
40
51
  logger = logging.getLogger(__name__)
41
52
 
42
53
  type Publish = Callable[[], object]
@@ -48,6 +59,11 @@ _HOOKED_KEY = "background_tasks_after_commit_hooked"
48
59
  def enqueue_after_commit(db: AsyncSession | Session, task: Any, *args: Any, **kwargs: Any) -> str:
49
60
  """Send *task* once *db*'s transaction commits; never if it rolls back.
50
61
 
62
+ Delivery is at-most-once: publish intent exists only in memory. Broker
63
+ failure after commit is logged without retry; a process exit between commit
64
+ and publish also loses the job. For must-run work use the event bus's
65
+ ``publish_durable(event, db)`` transactional outbox with a durable handler.
66
+
51
67
  *task* is a Celery task (``send_receipt``), a signature
52
68
  (``send_receipt.s().set(queue="mail")``, for routing options), or a
53
69
  registered task name. ``*args`` / ``**kwargs`` are the task's own
@@ -78,7 +94,7 @@ def after_commit(
78
94
  request. Left off for a single publish so Celery's eager mode — which runs
79
95
  the task body inside the publish — stays on the loop's thread. One failed
80
96
  publish is logged and does not stop the rest: the rows are already
81
- committed, and an unsent ``pending`` row is visible and retryable.
97
+ committed. Only callers with persisted pending work can reconcile failures.
82
98
  """
83
99
  batch = list(publishes)
84
100
  if isinstance(db, RequestSession) and SESSION_START_KEY in db.info:
@@ -18,9 +18,8 @@ TABLE_TASK_EXECUTION = f"{TABLE_PREFIX}task_execution"
18
18
  TABLE_OUTBOX = f"{TABLE_PREFIX}outbox"
19
19
 
20
20
  # ── Module dependencies ─────────────────────────────────────────
21
- _MODULE_USERS = "Users"
22
- # register_settings() calls settings.registration.register_module_settings,
23
- # which reads app.state.settings — so Settings must register first.
21
+ # Sorted after Settings for its admin editor and reload path; registering
22
+ # settings itself no longer needs it (the framework owns the registry, #500).
24
23
  _MODULE_SETTINGS = "Settings"
25
24
 
26
25
  # ── Env / settings ──────────────────────────────────────────────
@@ -42,10 +41,14 @@ API_PREFIX = "/api/background_tasks"
42
41
  VIEW_PREFIX = "/admin/background-tasks"
43
42
  ADMIN_ROUTER_PREFIX = "/admin"
44
43
 
44
+ STATUS_ROUTER_PREFIX = "/tasks"
45
+
45
46
  # ── Menu ────────────────────────────────────────────────────────
46
47
  MENU_LABEL = "Background tasks"
47
48
  MENU_ICON = "activity"
48
49
  MENU_ORDER = 120
50
+ LOGS_MENU_LABEL = "Logs"
51
+ LOGS_MENU_ICON = "scroll-text"
49
52
 
50
53
  # ── Page identifiers ────────────────────────────────────────────
51
54
  # Kept as literals at the call site (see endpoints/views.py) so
@@ -141,7 +144,7 @@ class OutboxScope(StrEnum):
141
144
 
142
145
 
143
146
  DEFAULT_OUTBOX_MAX_ATTEMPTS = 8
144
- DEFAULT_OUTBOX_SWEEP_INTERVAL_SECONDS = 30
147
+ DEFAULT_OUTBOX_SWEEP_INTERVAL_SECONDS = 5
145
148
  # How long a claimed row stays ``running`` before another worker may reclaim it.
146
149
  # Durable handlers are cut off ``outbox_lease_margin`` before it, so no run can
147
150
  # outlive its claim and overlap the rerun a reclaim would start.
@@ -173,3 +176,26 @@ OUTBOX_KEY_MAX_LENGTH = 255
173
176
  TENANT_ID_MAX_LENGTH = 50
174
177
  # Filter value meaning "executions with no tenant" (beat / platform publishes).
175
178
  PLATFORM_TENANT_FILTER = "__platform__"
179
+
180
+ OWNER_ID_MAX_LENGTH = 64
181
+ PROGRESS_MESSAGE_MAX_LENGTH = 255
182
+ PROGRESS_PHASE_MAX_LENGTH = 64
183
+
184
+
185
+ class OutboxMode(StrEnum):
186
+ CELERY = "celery"
187
+ INPROCESS = "inprocess"
188
+
189
+
190
+ # ── Retained logs (GH #537) ─────────────────────────────────────
191
+ TABLE_LOG_RECORD = f"{TABLE_PREFIX}log_record"
192
+ LOG_MESSAGE_MAX_LENGTH = 4096
193
+ LOG_NAME_MAX_LENGTH = 255
194
+ LOG_ID_MAX_LENGTH = 64
195
+ LOG_EXTRA_MAX_KEYS = 20
196
+ LOG_EXTRA_VALUE_MAX_LENGTH = 1000
197
+ DEFAULT_LOG_SINK_LEVEL = "INFO"
198
+ DEFAULT_LOG_RETENTION_DAYS = 7
199
+ LOG_PER_PAGE = 50
200
+ # Level names the admin filter offers, lowest first.
201
+ LOG_LEVEL_NAMES = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
@@ -0,0 +1,31 @@
1
+ """DTOs for the retained-log screens (GH #537)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from simple_module_db.types import AwareDateTime
10
+ from sqlmodel import Field, SQLModel
11
+
12
+
13
+ class LogItem(SQLModel):
14
+ id: uuid.UUID
15
+ ts: datetime = Field(sa_type=AwareDateTime())
16
+ level: int
17
+ level_name: str
18
+ logger: str
19
+ message: str
20
+ task_id: str | None = None
21
+ job_id: str | None = None
22
+ tenant_id: str | None = None
23
+ correlation_id: str | None = None
24
+ extra: dict[str, Any] = Field(default_factory=dict)
25
+
26
+
27
+ class LogListResponse(SQLModel):
28
+ items: list[LogItem]
29
+ total: int
30
+ page: int
31
+ per_page: int
@@ -15,7 +15,7 @@ from background_tasks.constants import OutboxStatus
15
15
  class OutboxItem(SQLModel):
16
16
  """One (event, durable handler) delivery as the admin screen shows it."""
17
17
 
18
- model_config = ConfigDict(from_attributes=True)
18
+ model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
19
19
 
20
20
  id: uuid.UUID
21
21
  event_key: str
@@ -6,7 +6,7 @@ import uuid
6
6
  from datetime import datetime
7
7
  from typing import Any
8
8
 
9
- from pydantic import ConfigDict
9
+ from pydantic import ConfigDict, field_validator
10
10
  from sqlmodel import SQLModel
11
11
 
12
12
  from background_tasks.constants import TaskStatus
@@ -21,7 +21,7 @@ class TaskExecutionListItem(SQLModel):
21
21
  the operator needs to read it *before* confirming, not on a second page.
22
22
  """
23
23
 
24
- model_config = ConfigDict(from_attributes=True)
24
+ model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
25
25
 
26
26
  id: uuid.UUID
27
27
  celery_task_id: str | None = None
@@ -39,11 +39,21 @@ class TaskExecutionListItem(SQLModel):
39
39
  exception_type: str | None = None
40
40
  retried_from_id: uuid.UUID | None = None
41
41
 
42
+ @field_validator("args", mode="before")
43
+ @classmethod
44
+ def _args_none(cls, v: Any) -> Any:
45
+ return [] if v is None else v
46
+
47
+ @field_validator("kwargs", mode="before")
48
+ @classmethod
49
+ def _kwargs_none(cls, v: Any) -> Any:
50
+ return {} if v is None else v
51
+
42
52
 
43
53
  class TaskExecutionDetail(SQLModel):
44
54
  """Full row for the detail page (adds args/kwargs/result/traceback)."""
45
55
 
46
- model_config = ConfigDict(from_attributes=True)
56
+ model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
47
57
 
48
58
  id: uuid.UUID
49
59
  celery_task_id: str | None = None
@@ -59,6 +69,45 @@ class TaskExecutionDetail(SQLModel):
59
69
  worker: str | None = None
60
70
  retries: int = 0
61
71
  retried_from_id: uuid.UUID | None = None
72
+ owner_user_id: str | None = None
73
+ progress_current: int | None = None
74
+ progress_total: int | None = None
75
+ progress_message: str | None = None
76
+ progress_phase: str | None = None
77
+ queued_at: datetime | None = None
78
+ started_at: datetime | None = None
79
+ finished_at: datetime | None = None
80
+ heartbeat_at: datetime | None = None
81
+
82
+ @field_validator("args", mode="before")
83
+ @classmethod
84
+ def _args_none(cls, v: Any) -> Any:
85
+ return [] if v is None else v
86
+
87
+ @field_validator("kwargs", mode="before")
88
+ @classmethod
89
+ def _kwargs_none(cls, v: Any) -> Any:
90
+ return {} if v is None else v
91
+
92
+
93
+ class TaskStatusDTO(SQLModel):
94
+ """What the owner of a task may see of it (#536).
95
+
96
+ Deliberately narrower than :class:`TaskExecutionDetail`: no args, kwargs,
97
+ result or traceback, and the error is the exception's *type name* only —
98
+ a message or traceback can carry other users' data or server internals.
99
+ """
100
+
101
+ model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
102
+
103
+ celery_task_id: str
104
+ task_name: str
105
+ status: TaskStatus
106
+ progress_current: int | None = None
107
+ progress_total: int | None = None
108
+ progress_message: str | None = None
109
+ progress_phase: str | None = None
110
+ exception_type: str | None = None
62
111
  queued_at: datetime | None = None
63
112
  started_at: datetime | None = None
64
113
  finished_at: datetime | None = None
@@ -0,0 +1,34 @@
1
+ """Restart-required worker engine settings."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings
7
+
8
+ _RESTART = {"requires_restart": True, "group": "Database"}
9
+
10
+
11
+ class WorkerDatabaseSettings(BaseSettings):
12
+ """Inherited by BackgroundTasksSettings with its SM_BG_TASKS_ env prefix."""
13
+
14
+ db_pool_size: int = Field(default=2, ge=1, json_schema_extra=_RESTART)
15
+ db_max_overflow: int = Field(default=3, ge=0, json_schema_extra=_RESTART)
16
+ db_recycle: int = Field(default=1800, ge=0, json_schema_extra=_RESTART)
17
+ db_statement_timeout_ms: int | None = Field(default=None, ge=0, json_schema_extra=_RESTART)
18
+
19
+
20
+ def worker_database_options(host: Any, worker: WorkerDatabaseSettings) -> dict[str, int]:
21
+ """Use host timeouts unless the worker has an explicit statement limit."""
22
+ return {
23
+ "pool_size": worker.db_pool_size,
24
+ "max_overflow": worker.db_max_overflow,
25
+ "pool_recycle": worker.db_recycle,
26
+ "pool_timeout": host.db_pool_timeout,
27
+ "connect_timeout": host.db_connect_timeout,
28
+ "statement_timeout_ms": (
29
+ host.db_statement_timeout_ms
30
+ if worker.db_statement_timeout_ms is None
31
+ else worker.db_statement_timeout_ms
32
+ ),
33
+ "idle_in_tx_timeout_ms": host.db_idle_in_tx_timeout_ms,
34
+ }
@@ -8,6 +8,7 @@ from simple_module_core.permissions import is_admin
8
8
  from simple_module_db import RequestSession, current_tenant_id
9
9
  from simple_module_db.deps import get_db
10
10
 
11
+ from background_tasks.log_service import LogScope, LogService
11
12
  from background_tasks.outbox_service import OutboxScope, OutboxService
12
13
  from background_tasks.outbox_transport import OutboxTransport
13
14
  from background_tasks.service import BackgroundTaskService
@@ -49,3 +50,11 @@ def outbox_scope(request: Request) -> OutboxScope:
49
50
  return OutboxScope(tenant_id=tenant_id)
50
51
  multi_tenant = bool(getattr(request.app.state.sm.settings, "multi_tenant", False))
51
52
  return OutboxScope(everything=not multi_tenant)
53
+
54
+
55
+ async def get_log_service(
56
+ request: Request,
57
+ db: RequestSession = Depends(get_db),
58
+ ) -> LogService:
59
+ scope = outbox_scope(request)
60
+ return LogService(db=db, scope=LogScope(everything=scope.everything, tenant_id=scope.tenant_id))
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import uuid
6
6
 
7
7
  from fastapi import APIRouter, Depends, HTTPException, Query, Request
8
+ from simple_module_db.pagination import PageParams
9
+ from simple_module_hosting.pagination import page_params
8
10
  from simple_module_hosting.permissions import RequiresPermission
9
11
 
10
12
  from background_tasks.constants import (
@@ -38,8 +40,7 @@ async def list_executions(
38
40
  task_name: str | None = Query(default=None, deprecated=True),
39
41
  queue: str | None = Query(default=None),
40
42
  tenant_id: str | None = Query(default=None),
41
- page: int = Query(default=1, ge=1),
42
- per_page: int = Query(default=20, ge=1, le=200),
43
+ params: PageParams = Depends(page_params(20)),
43
44
  service: BackgroundTaskService = Depends(get_background_task_service),
44
45
  ) -> TaskExecutionListResponse:
45
46
  """List executions, filtered exactly as the page filters them.
@@ -55,8 +56,8 @@ async def list_executions(
55
56
  task_name=q or task_name,
56
57
  queue=queue,
57
58
  tenant_id=tenant_id,
58
- page=page,
59
- per_page=per_page,
59
+ page=params.page,
60
+ per_page=params.per_page,
60
61
  )
61
62
 
62
63