kryten-webqueue 0.47.0__tar.gz → 0.48.0__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 (248) hide show
  1. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/CHANGELOG.md +13 -0
  2. kryten_webqueue-0.48.0/Containerfile +23 -0
  3. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/PKG-INFO +4 -1
  4. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/config.example.json +13 -1
  5. kryten_webqueue-0.48.0/deploy/podman/webqueue/webqueue-app.container +24 -0
  6. kryten_webqueue-0.48.0/deploy/podman/webqueue/webqueue.network +3 -0
  7. kryten_webqueue-0.48.0/docs/postgres-cutover.md +94 -0
  8. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/app.py +8 -0
  9. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/__init__.py +27 -15
  10. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_catalog.py +24 -0
  11. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_jobs_db.py +28 -0
  12. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/engine.py +29 -0
  13. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/schemas/catalog_schema.py +3 -3
  14. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +463 -0
  15. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/config.py +96 -27
  16. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/tasks.py +27 -0
  17. kryten_webqueue-0.48.0/kryten_webqueue/migrate_sqlite_to_pg.py +350 -0
  18. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0/kryten_webqueue}/scripts/split_databases.py +4 -12
  19. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/pyproject.toml +5 -1
  20. kryten_webqueue-0.48.0/scripts/split_databases.py +13 -0
  21. kryten_webqueue-0.48.0/tests/test_postgres_config_and_engine.py +77 -0
  22. kryten_webqueue-0.48.0/tests/test_postgres_pruner.py +131 -0
  23. kryten_webqueue-0.47.0/kryten_webqueue/scripts/split_databases.py +0 -14
  24. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.github/workflows/gitleaks.yml +0 -0
  25. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.github/workflows/python-publish.yml +0 -0
  26. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.github/workflows/release.yml +0 -0
  27. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.gitignore +0 -0
  28. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.gitleaks.toml +0 -0
  29. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/.pre-commit-config.yaml +0 -0
  30. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
  31. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/AGENTS.md +0 -0
  32. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/README.md +0 -0
  33. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/deploy/kryten-webqueue.service +0 -0
  34. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/deploy/nginx-queue.conf +0 -0
  35. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/DATABASE_ARCHITECTURE_PLAN.md +0 -0
  36. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  37. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/IMPL_API_GATE.md +0 -0
  38. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/IMPL_ECONOMY.md +0 -0
  39. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  40. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/IMPL_ROBOT.md +0 -0
  41. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  42. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  43. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  44. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  45. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
  46. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/PRE_PLAN_GAPS.md +0 -0
  47. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/PRODUCT_PLAN.md +0 -0
  48. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/PUBLIC_API.md +0 -0
  49. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
  50. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
  51. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  52. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
  53. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  54. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
  55. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/UX_POLISH_PLAN.md +0 -0
  56. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/PRD-postgres-migration.md +0 -0
  57. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +0 -0
  58. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +0 -0
  59. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +0 -0
  60. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +0 -0
  61. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +0 -0
  62. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +0 -0
  63. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +0 -0
  64. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +0 -0
  65. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +0 -0
  66. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +0 -0
  67. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +0 -0
  68. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/__init__.py +0 -0
  69. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/__main__.py +0 -0
  70. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  71. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/api_gate/client.py +0 -0
  72. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/__init__.py +0 -0
  73. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/device_keys.py +0 -0
  74. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/otp.py +0 -0
  75. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  76. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/session.py +0 -0
  77. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/__init__.py +0 -0
  78. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_base_domain.py +0 -0
  79. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
  80. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_catalog_db.py +0 -0
  81. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  82. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
  83. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  84. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  85. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
  86. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
  87. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_people.py +0 -0
  88. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  89. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  90. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_queue_db.py +0 -0
  91. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_users_db.py +0 -0
  92. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  93. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/schemas/__init__.py +0 -0
  94. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/schemas/jobs_schema.py +0 -0
  95. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/schemas/queue_schema.py +0 -0
  96. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/schemas/users_schema.py +0 -0
  97. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  98. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  99. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  100. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  101. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  102. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  103. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  104. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  105. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  106. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
  107. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  108. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  109. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  110. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  111. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/images.py +0 -0
  112. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  113. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/sync.py +0 -0
  114. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
  115. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
  116. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
  117. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
  118. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
  119. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
  120. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
  121. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/__init__.py +0 -0
  122. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  123. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  124. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
  125. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  126. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  127. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  128. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  129. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  130. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  131. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  132. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  133. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/__init__.py +0 -0
  134. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  135. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  136. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/log_capture.py +0 -0
  137. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/manager.py +0 -0
  138. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
  139. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  140. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/logging_config.py +0 -0
  141. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/__init__.py +0 -0
  142. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/builder.py +0 -0
  143. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/render.py +0 -0
  144. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/__init__.py +0 -0
  145. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  146. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/fire.py +0 -0
  147. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/importer.py +0 -0
  148. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/ordering.py +0 -0
  149. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  150. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/promos/__init__.py +0 -0
  151. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/promos/director.py +0 -0
  152. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/__init__.py +0 -0
  153. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/completion.py +0 -0
  154. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/ordering.py +0 -0
  155. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/poller.py +0 -0
  156. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/presence.py +0 -0
  157. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/race_poller.py +0 -0
  158. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/shadow.py +0 -0
  159. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/__init__.py +0 -0
  160. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  161. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  162. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  163. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  164. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  165. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_motd.py +0 -0
  166. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  167. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  168. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  169. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  170. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/auth.py +0 -0
  171. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/catalog.py +0 -0
  172. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/devices.py +0 -0
  173. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/feedback.py +0 -0
  174. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/pages.py +0 -0
  175. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/public_api.py +0 -0
  176. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/queue.py +0 -0
  177. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/user.py +0 -0
  178. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/scripts/__init__.py +0 -0
  179. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/css/main.css +0 -0
  180. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
  181. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  182. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  183. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  184. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  185. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin.js +0 -0
  186. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/keybindings.js +0 -0
  187. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/main.js +0 -0
  188. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/index.html +0 -0
  189. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/motd.html +0 -0
  190. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  191. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  192. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  193. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  194. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/auth/login.html +0 -0
  195. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/base.html +0 -0
  196. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  197. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
  198. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  199. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  200. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  201. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
  202. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/queue/index.html +0 -0
  203. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/race.html +0 -0
  204. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  205. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/link_device.html +0 -0
  206. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/my_list.html +0 -0
  207. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/__init__.py +0 -0
  208. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/handler.py +0 -0
  209. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/manager.py +0 -0
  210. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/scripts/AGENTS.md +0 -0
  211. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
  212. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/scripts/patch_mediacms_tags.py +0 -0
  213. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/scripts/trigger_enrich.py +0 -0
  214. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/__init__.py +0 -0
  215. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_catalog_blackout.py +0 -0
  216. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_catalog_delete.py +0 -0
  217. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_classify.py +0 -0
  218. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_concurrency_split.py +0 -0
  219. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_config_persistence.py +0 -0
  220. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_database_config_and_schemas.py +0 -0
  221. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_device_linking.py +0 -0
  222. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_feedback.py +0 -0
  223. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_fetch_queue_pacing.py +0 -0
  224. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_fetchurls_sharepoint.py +0 -0
  225. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_identify_step.py +0 -0
  226. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
  227. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_motd_publish.py +0 -0
  228. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_multi_db_connection.py +0 -0
  229. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_people_facets.py +0 -0
  230. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_phase1.py +0 -0
  231. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_phase2_jobs.py +0 -0
  232. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_phase3_jobs.py +0 -0
  233. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_phase4_live_fixes.py +0 -0
  234. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_playlist_import.py +0 -0
  235. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_presence_refund.py +0 -0
  236. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_promo_director.py +0 -0
  237. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_promo_pool_exclusion.py +0 -0
  238. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_providers.py +0 -0
  239. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_queue_announce.py +0 -0
  240. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_queue_preview_fallback.py +0 -0
  241. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_race_poller.py +0 -0
  242. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_recently_played_hide.py +0 -0
  243. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_save_results_to_playlist.py +0 -0
  244. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_schedule_lock.py +0 -0
  245. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_search_facets.py +0 -0
  246. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_title_normalization.py +0 -0
  247. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_tmdb_index.py +0 -0
  248. {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.0}/tests/test_watchlist.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.48.0] - 2026-09-23
4
+
5
+ ### Added
6
+
7
+ - **PostgreSQL & Chandra-1 Podman Migration Foundations (`postgres-migration`)**:
8
+ - Added PostgreSQL backend support and configuration in `kryten_webqueue/config.py` with `PostgresConfig` and `DatabaseConfig(backend="sqlite"|"postgres")`.
9
+ - Added SQLAlchemy 2.0 async engine and session factory builder in `kryten_webqueue/catalog/db/engine.py` (`create_pg_engine`) utilizing `asyncpg` with connection pooling (`pool_size=10`, `max_overflow=20`, `pool_pre_ping=True`).
10
+ - Added schema DDL `kryten_webqueue/catalog/db/sql/001_initial_schema.sql` defining 40 tables across 5 logical schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`), English `tsvector` generated search vector, and `pg_trgm` fuzzy trigram matching.
11
+ - Implemented automated 30-day job run log pruning task (`job_log_prune_job`) in `kryten_webqueue/jobs/tasks.py` and `kryten_webqueue/catalog/db/_jobs_db.py` with strict exemptions for financial, chat, feedback, and audit history.
12
+ - Implemented one-shot ETL streaming script `kryten_webqueue/migrate_sqlite_to_pg.py` (`kryten-webqueue-migrate-pg`) with batched row streaming, identity sequence synchronization (`setval`), type normalization, and verification reporting.
13
+ - Added rootful Podman Quadlet specifications (`deploy/podman/webqueue/webqueue.network` and `webqueue-app.container`) and Containerfile for deployment on `chandra-1`.
14
+ - Added comprehensive operational cutover runbook `docs/postgres-cutover.md`.
15
+
3
16
  ## [0.47.0] - 2026-09-23
4
17
 
5
18
  ### Added
@@ -0,0 +1,23 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update \
6
+ && apt-get install -y --no-install-recommends build-essential ffmpeg curl \
7
+ && rm -rf /var/lib/apt/lists/* \
8
+ && useradd --system --create-home --uid 1001 kryten
9
+
10
+ COPY . /tmp/kryten-webqueue
11
+
12
+ RUN pip install --no-cache-dir --upgrade pip \
13
+ && pip install --no-cache-dir /tmp/kryten-webqueue
14
+
15
+ RUN mkdir -p /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue \
16
+ && chown -R kryten:kryten /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue
17
+
18
+ USER kryten
19
+
20
+ EXPOSE 2010
21
+
22
+ ENTRYPOINT ["kryten-webqueue"]
23
+ CMD ["--config", "/etc/kryten-webqueue/config.json"]
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.47.0
3
+ Version: 0.48.0
4
4
  Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
5
5
  Author: grobertson
6
6
  License-Expression: MIT
7
7
  Requires-Python: >=3.12
8
8
  Requires-Dist: aiosqlite>=0.20
9
9
  Requires-Dist: apscheduler>=3.10
10
+ Requires-Dist: asyncpg>=0.29.0
10
11
  Requires-Dist: fastapi>=0.115
12
+ Requires-Dist: greenlet>=3.0
11
13
  Requires-Dist: httpx>=0.27
12
14
  Requires-Dist: jinja2>=3.1
13
15
  Requires-Dist: msal>=1.28
@@ -21,6 +23,7 @@ Requires-Dist: python-multipart>=0.0.9
21
23
  Requires-Dist: python-slugify>=8.0
22
24
  Requires-Dist: pyyaml>=6.0
23
25
  Requires-Dist: requests>=2.31
26
+ Requires-Dist: sqlalchemy>=2.0.30
24
27
  Requires-Dist: uvicorn[standard]>=0.30
25
28
  Requires-Dist: websockets>=12.0
26
29
  Requires-Dist: yt-dlp>=2024.1
@@ -63,13 +63,25 @@
63
63
  },
64
64
 
65
65
  "database": {
66
+ "backend": "sqlite",
66
67
  "layout": "monolith",
67
68
  "db_path": "/var/lib/kryten-webqueue/webqueue.db",
68
69
  "data_dir": "/var/lib/kryten-webqueue/data",
69
70
  "catalog_db_path": null,
70
71
  "queue_db_path": null,
71
72
  "jobs_db_path": null,
72
- "users_db_path": null
73
+ "users_db_path": null,
74
+ "postgres": {
75
+ "host": "host.containers.internal",
76
+ "port": 5432,
77
+ "user": "kryten",
78
+ "dbname": "webqueue",
79
+ "password_env": "KRYTEN_WEBQUEUE_PG_PASSWORD",
80
+ "dsn_env": null,
81
+ "dsn": null,
82
+ "pool_size": 10,
83
+ "max_overflow": 20
84
+ }
73
85
  },
74
86
  "db_path": "/var/lib/kryten-webqueue/webqueue.db",
75
87
 
@@ -0,0 +1,24 @@
1
+ [Unit]
2
+ Description=kryten-webqueue - CyTube Queue Management (FastAPI app)
3
+ After=network-online.target
4
+
5
+ [Container]
6
+ ContainerName=webqueue-app
7
+ Image=localhost/kryten-webqueue:latest
8
+ Network=podman-webqueue
9
+ PublishPort=2010:2010
10
+ AddHost=host.containers.internal:host-gateway
11
+
12
+ EnvironmentFile=/etc/kryten/webqueue/webqueue.env
13
+ Volume=/etc/kryten/webqueue/config.json:/etc/kryten-webqueue/config.json:ro,z
14
+ Volume=/var/lib/kryten/media:/var/lib/kryten-webqueue:Z
15
+ Volume=/var/log/kryten/webqueue:/var/log/kryten-webqueue:Z
16
+
17
+ Exec=uvicorn kryten_webqueue.app:app --host 0.0.0.0 --port 2010
18
+
19
+ [Service]
20
+ Restart=always
21
+ RestartSec=5
22
+
23
+ [Install]
24
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,3 @@
1
+ [Network]
2
+ NetworkName=podman-webqueue
3
+ Subnet=10.89.30.0/24
@@ -0,0 +1,94 @@
1
+ # PostgreSQL Cutover Runbook: Kryten-WebQueue
2
+
3
+ **Target Version**: `0.48.0`
4
+ **Host Target**: `chandra-1` (PostgreSQL 16 + Podman Quadlet)
5
+ **Source Host**: `grindhouse.local` (pipx systemd / SQLite partitioned data)
6
+ **Parent Plan**: [DATABASE_ARCHITECTURE_PLAN.md](DATABASE_ARCHITECTURE_PLAN.md)
7
+ **PRD**: [postgres-migration/PRD-postgres-migration.md](postgres-migration/PRD-postgres-migration.md)
8
+
9
+ ---
10
+
11
+ ## 1. Pre-Flight Checklist
12
+
13
+ - [ ] `chandra-1` has PostgreSQL 16+ active and `pg_trgm` extension installed.
14
+ - [ ] Database `webqueue` exists with owner `kryten`.
15
+ - [ ] Schema `001_initial_schema.sql` applied cleanly across `catalog`, `queue`, `jobs`, `users`, `tmdb`.
16
+ - [ ] Privileges granted to `kryten`:
17
+ ```sql
18
+ GRANT USAGE, CREATE ON SCHEMA catalog, queue, jobs, users, tmdb TO kryten;
19
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
20
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
21
+ ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON TABLES TO kryten;
22
+ ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON SEQUENCES TO kryten;
23
+ ```
24
+ - [ ] Podman image built on `chandra-1`:
25
+ ```bash
26
+ sudo podman build -f Containerfile -t localhost/kryten-webqueue:latest .
27
+ ```
28
+ - [ ] Environment file `/etc/kryten/webqueue/webqueue.env` created on `chandra-1`:
29
+ ```env
30
+ KRYTEN_WEBQUEUE_PG_PASSWORD=kryten_secret_password
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 2. Maintenance Window Cutover (Step-by-Step)
36
+
37
+ ### Step 1: Stop Source Service & Freeze Writes
38
+ On `grindhouse.local`:
39
+ ```bash
40
+ sudo systemctl stop kryten-webqueue.service
41
+ sudo systemctl status kryten-webqueue.service --no-pager
42
+ ```
43
+
44
+ ### Step 2: Checkpoint SQLite WAL & Create Final Snapshot
45
+ On `grindhouse.local`:
46
+ ```bash
47
+ SNAPSHOT_DIR="/var/lib/kryten-webqueue/backups/cutover_$(date +%Y%m%d_%H%M%S)"
48
+ sudo mkdir -p "$SNAPSHOT_DIR"
49
+
50
+ for db in catalog queue jobs users; do
51
+ sudo sqlite3 "/var/lib/kryten-webqueue/data/$db.sqlite3" "PRAGMA wal_checkpoint(TRUNCATE);"
52
+ done
53
+
54
+ sudo cp -av /var/lib/kryten-webqueue/data "$SNAPSHOT_DIR/"
55
+ sudo cp -av /etc/kryten-webqueue/config.json "$SNAPSHOT_DIR/"
56
+ ```
57
+
58
+ ### Step 3: Stream SQLite Data to PostgreSQL on Chandra-1
59
+ Run the ETL migration script with `--verify`:
60
+ ```bash
61
+ sudo -u kryten /home/kryten/.local/pipx/venvs/kryten-webqueue/bin/python -m kryten_webqueue.migrate_sqlite_to_pg \
62
+ --data-dir /var/lib/kryten-webqueue/data \
63
+ --pg-dsn "postgresql://kryten:kryten_secret_password@chandra-1.local:5432/webqueue"
64
+ ```
65
+ Verify `postgres_migration_report.json` indicates `"status": "success"` and 0 errors.
66
+
67
+ ### Step 4: Transfer Media & Image Assets to Chandra-1
68
+ Sync cover art images to `/var/lib/kryten/media`:
69
+ ```bash
70
+ rsync -avz /var/lib/kryten-webqueue/images/ groberts@chandra-1.local:/var/lib/kryten/media/images/
71
+ ```
72
+
73
+ ### Step 5: Start Podman Quadlet on Chandra-1
74
+ On `chandra-1`:
75
+ ```bash
76
+ sudo cp deploy/podman/webqueue/webqueue.network /etc/containers/systemd/
77
+ sudo cp deploy/podman/webqueue/webqueue-app.container /etc/containers/systemd/
78
+ sudo systemctl daemon-reload
79
+ sudo systemctl start webqueue-app.service
80
+ sudo systemctl status webqueue-app.service --no-pager
81
+ ```
82
+
83
+ ### Step 6: Smoke Testing & Health Verification
84
+ ```bash
85
+ curl -I http://127.0.0.1:2010/auth/login
86
+ curl -s http://127.0.0.1:2010/queue/next-schedule
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. Rollback & Forward Repair Policy
92
+
93
+ - **Pre-Cutover Failure**: If ETL validation fails or container startup fails before DNS/traffic cutover, immediately restart `kryten-webqueue.service` on `grindhouse.local`. SQLite databases remain untouched and consistent.
94
+ - **Post-Cutover Failure**: Once traffic is switched and PostgreSQL accepts writes (new spend requests, logins, completions), recovery is a **forward repair in PostgreSQL**. The retained SQLite snapshot is preserved read-only for forensics.
@@ -111,6 +111,7 @@ async def lifespan(app: FastAPI):
111
111
  motd_posters_job,
112
112
  tmdb_index_refresh_job,
113
113
  tmdb_coverage_report_job,
114
+ job_log_prune_job,
114
115
  CATALOG_ENRICH_SCHEMA,
115
116
  CATALOG_BLACKOUT_SCHEMA,
116
117
  DEVICE_KEY_BAN_RECONCILE_SCHEMA,
@@ -121,6 +122,7 @@ async def lifespan(app: FastAPI):
121
122
  MOTD_POSTERS_SCHEMA,
122
123
  TMDB_INDEX_REFRESH_SCHEMA,
123
124
  TMDB_COVERAGE_REPORT_SCHEMA,
125
+ JOB_LOG_PRUNE_SCHEMA,
124
126
  )
125
127
 
126
128
  job_manager.register(
@@ -168,6 +170,12 @@ async def lifespan(app: FastAPI):
168
170
  label="MOTD Posters (weekend poster grid)",
169
171
  schema=MOTD_POSTERS_SCHEMA,
170
172
  )
173
+ job_manager.register(
174
+ "job_log_prune",
175
+ job_log_prune_job,
176
+ label="Job Run Log Pruner (30-day retention)",
177
+ schema=JOB_LOG_PRUNE_SCHEMA,
178
+ )
171
179
  from .jobs.motd_publish import motd_publish_job, MOTD_PUBLISH_SCHEMA
172
180
 
173
181
  job_manager.register(
@@ -107,17 +107,17 @@ _DOMAIN_METHOD_MAP: dict[str, str] = {
107
107
  "get_job_schedule": "jobs",
108
108
  "upsert_job_schedule": "jobs",
109
109
  "delete_job_schedule": "jobs",
110
- "fetch_queue_add": "jobs",
111
- "fetch_queue_get": "jobs",
112
- "fetch_queue_update": "jobs",
113
- "fetch_queue_delete": "jobs",
114
- "fetch_queue_list": "jobs",
115
- "fetch_queue_count": "jobs",
116
- "fetch_queue_get_next_pending": "jobs",
117
- "fetch_queue_mark_started": "jobs",
118
- "fetch_queue_mark_finished": "jobs",
119
- "fetch_queue_requeue": "jobs",
120
- "fetch_queue_cleanup": "jobs",
110
+ "prune_job_run_logs": "jobs",
111
+ "enqueue_fetch": "jobs",
112
+ "claim_next_fetch_item": "jobs",
113
+ "finish_fetch_item": "jobs",
114
+ "requeue_fetch_item": "jobs",
115
+ "requeue_fetch_item_for_retry": "jobs",
116
+ "reset_running_fetch_items": "jobs",
117
+ "get_fetch_queue": "jobs",
118
+ "count_fetch_queue": "jobs",
119
+ "count_fetch_queue_pending": "jobs",
120
+ "delete_fetch_queue_item": "jobs",
121
121
  # users
122
122
  "watchlist_add": "users",
123
123
  "watchlist_remove": "users",
@@ -303,10 +303,22 @@ class Database(
303
303
  except AttributeError:
304
304
  layout = "monolith"
305
305
 
306
- if layout == "partitioned" and name in _DOMAIN_METHOD_MAP:
307
- domain_name = _DOMAIN_METHOD_MAP[name]
308
- domain_obj = object.__getattribute__(self, domain_name)
309
- return getattr(domain_obj, name)
306
+ if layout == "partitioned":
307
+ if name in Database.__dict__ or name.startswith("_"):
308
+ return object.__getattribute__(self, name)
309
+
310
+ if name in _DOMAIN_METHOD_MAP:
311
+ domain_name = _DOMAIN_METHOD_MAP[name]
312
+ domain_obj = object.__getattribute__(self, domain_name)
313
+ return getattr(domain_obj, name)
314
+
315
+ for domain_name in ("jobs", "queue", "users", "catalog"):
316
+ try:
317
+ domain_obj = object.__getattribute__(self, domain_name)
318
+ if hasattr(domain_obj, name) and not hasattr(_DBBase, name):
319
+ return getattr(domain_obj, name)
320
+ except AttributeError:
321
+ pass
310
322
 
311
323
  return object.__getattribute__(self, name)
312
324
 
@@ -1331,6 +1331,30 @@ class _CatalogMixin:
1331
1331
  async def delete_job_schedule(self, job_name: str) -> None:
1332
1332
  await self._execute("DELETE FROM job_schedules WHERE job_name=?", [job_name])
1333
1333
 
1334
+ async def prune_job_run_logs(
1335
+ self, retention_days: int = 30, batch_size: int = 5000
1336
+ ) -> int:
1337
+ total_deleted = 0
1338
+ cutoff = f"-{int(retention_days)} days"
1339
+ while True:
1340
+ cursor = await self._db.execute(
1341
+ """
1342
+ DELETE FROM job_run_logs
1343
+ WHERE id IN (
1344
+ SELECT id FROM job_run_logs
1345
+ WHERE logged_at < datetime('now', ?)
1346
+ LIMIT ?
1347
+ )
1348
+ """,
1349
+ [cutoff, batch_size],
1350
+ )
1351
+ await self._db.commit()
1352
+ count = cursor.rowcount or 0
1353
+ total_deleted += count
1354
+ if count < batch_size:
1355
+ break
1356
+ return total_deleted
1357
+
1334
1358
  # --- OTP ---
1335
1359
 
1336
1360
  async def store_otp(self, username: str, code: str, expires_at: str):
@@ -121,3 +121,31 @@ class _JobsDB(_FetchQueueMixin, _DomainDB):
121
121
 
122
122
  async def delete_job_schedule(self, job_name: str) -> None:
123
123
  await self._execute("DELETE FROM job_schedules WHERE job_name=?", [job_name])
124
+
125
+ async def prune_job_run_logs(
126
+ self, retention_days: int = 30, batch_size: int = 5000
127
+ ) -> int:
128
+ """Prune job_run_logs older than retention_days in bounded batches.
129
+
130
+ Strictly touches ONLY job_run_logs.
131
+ """
132
+ total_deleted = 0
133
+ cutoff = f"-{int(retention_days)} days"
134
+ while True:
135
+ cursor = await self._db.execute(
136
+ """
137
+ DELETE FROM job_run_logs
138
+ WHERE id IN (
139
+ SELECT id FROM job_run_logs
140
+ WHERE logged_at < datetime('now', ?)
141
+ LIMIT ?
142
+ )
143
+ """,
144
+ [cutoff, batch_size],
145
+ )
146
+ await self._db.commit()
147
+ count = cursor.rowcount or 0
148
+ total_deleted += count
149
+ if count < batch_size:
150
+ break
151
+ return total_deleted
@@ -0,0 +1,29 @@
1
+ """SQLAlchemy 2.0 async engine and session factory for PostgreSQL."""
2
+
3
+ from sqlalchemy.ext.asyncio import (
4
+ AsyncEngine,
5
+ AsyncSession,
6
+ async_sessionmaker,
7
+ create_async_engine,
8
+ )
9
+
10
+ from ...config import PostgresConfig
11
+
12
+
13
+ def create_pg_engine(
14
+ config: PostgresConfig,
15
+ ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
16
+ """Create a SQLAlchemy 2.0 async engine and sessionmaker from PostgresConfig."""
17
+ url = config.get_async_url()
18
+ engine = create_async_engine(
19
+ url,
20
+ pool_size=config.pool_size,
21
+ max_overflow=config.max_overflow,
22
+ pool_pre_ping=True,
23
+ )
24
+ session_factory = async_sessionmaker(
25
+ bind=engine,
26
+ class_=AsyncSession,
27
+ expire_on_commit=False,
28
+ )
29
+ return engine, session_factory
@@ -78,7 +78,7 @@ CATALOG_MIGRATIONS: list[str] = [
78
78
  CREATE INDEX IF NOT EXISTS idx_people_name ON people(name);
79
79
 
80
80
  CREATE TABLE IF NOT EXISTS catalog_people (
81
- friendly_token TEXT NOT NULL REFERENCES catalog(friendly_token) ON DELETE CASCADE,
81
+ friendly_token TEXT NOT NULL,
82
82
  person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
83
83
  role TEXT NOT NULL,
84
84
  position INTEGER NOT NULL DEFAULT 0,
@@ -93,14 +93,14 @@ CATALOG_MIGRATIONS: list[str] = [
93
93
  );
94
94
 
95
95
  CREATE TABLE IF NOT EXISTS catalog_studios (
96
- friendly_token TEXT NOT NULL REFERENCES catalog(friendly_token) ON DELETE CASCADE,
96
+ friendly_token TEXT NOT NULL,
97
97
  studio_id INTEGER NOT NULL REFERENCES studios(id) ON DELETE CASCADE,
98
98
  PRIMARY KEY (friendly_token, studio_id)
99
99
  );
100
100
  CREATE INDEX IF NOT EXISTS idx_catalog_studios_token ON catalog_studios(friendly_token);
101
101
 
102
102
  CREATE TABLE IF NOT EXISTS item_enrichment_state (
103
- friendly_token TEXT PRIMARY KEY REFERENCES catalog(friendly_token) ON DELETE CASCADE,
103
+ friendly_token TEXT PRIMARY KEY,
104
104
  content_type TEXT,
105
105
  hosted_show TEXT,
106
106
  lookup_title TEXT,