kryten-webqueue 0.48.1__tar.gz → 0.49.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.
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/CHANGELOG.md +29 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/PKG-INFO +1 -1
- kryten_webqueue-0.49.0/docs/postgres-migration/HANDOFF-repository-port-status.md +510 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/__init__.py +75 -19
- kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_base_domain.py +157 -0
- kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_catalog_db.py +1128 -0
- kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_jobs_db.py +318 -0
- kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_queue_db.py +782 -0
- kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_users_db.py +265 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_playlists.py +7 -3
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +1 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/config.py +35 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/pyproject.toml +1 -1
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_postgres_config_and_engine.py +36 -0
- kryten_webqueue-0.49.0/tests/test_postgres_domain_crud.py +624 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.gitignore +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/AGENTS.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/Containerfile +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/README.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/config.example.json +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/deploy/podman/webqueue/webqueue-app.container +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/deploy/podman/webqueue/webqueue.network +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/DATABASE_ARCHITECTURE_PLAN.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/PUBLIC_API.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-cutover.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/PRD-postgres-migration.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/device_keys.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_base_domain.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_catalog_db.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_jobs_db.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_people.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_queue_db.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_users_db.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/engine.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/catalog_schema.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/jobs_schema.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/queue_schema.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/users_schema.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/log_capture.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/migrate_sqlite_to_pg.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/builder.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/render.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_motd.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/devices.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/public_api.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/scripts/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/scripts/split_databases.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/keybindings.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/motd.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/link_device.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/my_list.html +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/scripts/AGENTS.md +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/scripts/patch_mediacms_tags.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/scripts/split_databases.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/scripts/trigger_enrich.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_catalog_blackout.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_catalog_delete.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_classify.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_concurrency_split.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_database_config_and_schemas.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_device_linking.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_fetch_queue_pacing.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_identify_step.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_motd_publish.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_multi_db_connection.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_people_facets.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_postgres_live.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_postgres_pruner.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_providers.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_queue_preview_fallback.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_search_facets.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_title_normalization.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_tmdb_index.py +0 -0
- {kryten_webqueue-0.48.1 → kryten_webqueue-0.49.0}/tests/test_watchlist.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.49.0] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Postgres repository port complete (Sortie 2/3)**: all ~140 SQLite domain methods
|
|
8
|
+
across `catalog`/`queue`/`jobs`/`users` are now implemented for the `asyncpg`/Postgres
|
|
9
|
+
backend (`_pg_catalog_db.py`, `_pg_queue_db.py`, `_pg_jobs_db.py`, `_pg_users_db.py`),
|
|
10
|
+
wired into `Database.__init__` for `database.backend == "postgres"`. `search()`/
|
|
11
|
+
`search_count()` use a hybrid `tsvector` + `pg_trgm` two-CTE query in place of SQLite's
|
|
12
|
+
FTS5. Live-tested end-to-end against a disposable `webqueue_test` database on
|
|
13
|
+
`chandra-1` (`tests/test_postgres_domain_crud.py`).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`Database` facade never dispatched cross-domain orchestration methods (`browse`,
|
|
18
|
+
`search`, `get_item`, `is_restricted`, etc.) for the Postgres backend** — these all
|
|
19
|
+
checked `layout == "partitioned"` only, never `backend == "postgres"`, so a Postgres
|
|
20
|
+
`Database` would silently fall through to the SQLite-monolith code path. Introduced
|
|
21
|
+
`Database._domain_dispatch` (`layout == "partitioned" or backend == "postgres"`) and
|
|
22
|
+
updated every dispatch check to use it.
|
|
23
|
+
- **`rotate_playlist_item_to_bottom` silently failed on every real play completion** in
|
|
24
|
+
both partitioned SQLite and Postgres: it queried a bare `catalog` table from within the
|
|
25
|
+
`queue`-only connection/schema, which doesn't exist there (`OperationalError`/
|
|
26
|
+
`UndefinedTableError`), swallowed by a bare `except Exception` in
|
|
27
|
+
`queue/completion.py`. Added an `is_partitioned` flag to skip the same-connection
|
|
28
|
+
lookup, and a `Database.rotate_playlist_item_to_bottom` facade override that resolves
|
|
29
|
+
`friendly_token -> manifest_url` via the `catalog` domain first, mirroring the existing
|
|
30
|
+
`_get_reserved_tokens()` pattern.
|
|
31
|
+
|
|
3
32
|
## [0.48.1] - 2026-09-23
|
|
4
33
|
|
|
5
34
|
### Fixed
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# HANDOFF: PostgreSQL Repository Port (Sortie 2/3) — In-Progress Status
|
|
2
|
+
|
|
3
|
+
**Status as of**: 2026-09-23, session end
|
|
4
|
+
**For**: The next agent/engineer picking up the Postgres cutover of `kryten-webqueue`
|
|
5
|
+
**Read this before touching anything.** Production (`https://queue.dropsugar.co/`) is
|
|
6
|
+
currently served by `kryten-webqueue.service` on `grindhouse.local`, running SQLite in
|
|
7
|
+
`layout=partitioned` mode at **v0.48.1**. It is healthy and untouched by this work.
|
|
8
|
+
**Nothing in this document has been deployed to production.**
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. TL;DR
|
|
13
|
+
|
|
14
|
+
The goal is to run `kryten-webqueue` as a Podman container on `chandra-1`, reading/writing
|
|
15
|
+
the already-provisioned PostgreSQL database `webqueue` (schemas `catalog`, `queue`, `jobs`,
|
|
16
|
+
`users`, `tmdb`) instead of SQLite.
|
|
17
|
+
|
|
18
|
+
**What actually exists and is verified working:**
|
|
19
|
+
- PostgreSQL `webqueue` database on `chandra-1`, schema applied, `pg_trgm` installed,
|
|
20
|
+
`kryten` role granted full privileges on all 5 schemas.
|
|
21
|
+
- A one-shot ETL (`kryten_webqueue/migrate_sqlite_to_pg.py`) has already been run once and
|
|
22
|
+
reported 100% row-count parity — **but this data is now stale** (see §5.6). It must be
|
|
23
|
+
re-run in the final maintenance window, not relied upon as-is.
|
|
24
|
+
- A Podman image `localhost/kryten-webqueue:latest` exists on `chandra-1`, but it was built
|
|
25
|
+
**2026-09-23 15:21 UTC from commit `533d6ed` (v0.48.0)** — it predates the v0.48.1 hotfix
|
|
26
|
+
and **all** of the Postgres repository-port work described below. **It must be rebuilt
|
|
27
|
+
before any deployment.**
|
|
28
|
+
- Quadlet unit files exist in the repo (`deploy/podman/webqueue/webqueue.network`,
|
|
29
|
+
`webqueue-app.container`) but are **not installed** on `chandra-1`
|
|
30
|
+
(`/etc/containers/systemd/` has nothing webqueue-related). No `webqueue-app.service`
|
|
31
|
+
exists yet; nothing is running there.
|
|
32
|
+
- nginx on `grindhouse.local` still points at `127.0.0.1:2010`. It has not been touched.
|
|
33
|
+
|
|
34
|
+
**What does NOT exist yet (the actual remaining work):**
|
|
35
|
+
- The application's database layer (`kryten_webqueue/catalog/db/_catalog.py`, `_queue.py`,
|
|
36
|
+
`_playlists.py`, `_jobs_db.py`, `_users_db.py`, `_devices.py`, `_feedback.py`,
|
|
37
|
+
`_watchlist.py`, `_people.py`, `_enrichment.py`, `_fetch_queue.py`, `_blackouts.py`,
|
|
38
|
+
`_motd.py` — roughly **4,700 lines**) is **100% SQLite-only**. There is no code path that
|
|
39
|
+
reads or writes Postgres for any actual application query yet. `Database.__init__` in
|
|
40
|
+
`kryten_webqueue/catalog/db/__init__.py` only branches on SQLite `layout`
|
|
41
|
+
(monolith/partitioned); it does not look at `config.database.backend` at all.
|
|
42
|
+
- This session started that port (see §3) but only the shared low-level connection helper
|
|
43
|
+
(`_pg_base_domain.py`) exists so far. **Zero of the ~140 domain methods have been ported.**
|
|
44
|
+
|
|
45
|
+
**Do not build/deploy the chandra-1 pod against Postgres until §4's checklist is complete
|
|
46
|
+
and tested.** Standing up the container today would either crash immediately (schema
|
|
47
|
+
mismatch, e.g. missing `id` column on `users.otps` — see §3.2) or, worse, silently write
|
|
48
|
+
malformed data if partially wired.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Why this exists / prior session context
|
|
53
|
+
|
|
54
|
+
This is the continuation of the `postgres-migration` sprint documented in
|
|
55
|
+
`docs/postgres-migration/PRD-postgres-migration.md` and its `SPEC-Sortie-*.md` files. Prior
|
|
56
|
+
sessions completed:
|
|
57
|
+
- Sortie 1 (config + engine scaffold) — done, `PostgresConfig`/`DatabaseConfig.backend` in
|
|
58
|
+
`kryten_webqueue/config.py`, `kryten_webqueue/catalog/db/engine.py`.
|
|
59
|
+
- Sortie 4 (TMDB schema) and Sortie 5 (ETL) — schema + script exist and were run once.
|
|
60
|
+
- Sortie 6 (pruner, Quadlet files, cutover runbook) — files exist in the repo, not deployed.
|
|
61
|
+
|
|
62
|
+
**Sortie 2 (SQLAlchemy 2.0 repository port) and Sortie 3 (FTS5 → tsvector/pg_trgm) were
|
|
63
|
+
never done.** That is the actual bulk of "finishing the migration," and it's what this
|
|
64
|
+
session began. See `docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md` and
|
|
65
|
+
`SPEC-Sortie-3-fts5-to-tsvector.md` for the original design intent (SQLAlchemy Core). **This
|
|
66
|
+
session took a different, lighter-weight implementation approach — see §3.1 for why, and
|
|
67
|
+
make a deliberate decision about whether to continue with it or switch to the SQLAlchemy
|
|
68
|
+
plan before writing more code.**
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 3. What this session actually did
|
|
73
|
+
|
|
74
|
+
### 3.1 Architecture decision made (needs confirmation/ratification)
|
|
75
|
+
|
|
76
|
+
Instead of the SPEC's SQLAlchemy Core/ORM approach, this session started a **direct
|
|
77
|
+
`asyncpg`, per-domain connection pool** design:
|
|
78
|
+
|
|
79
|
+
- Each of the 4 domains (`catalog`, `queue`, `jobs`, `users`) gets its own `asyncpg.Pool`,
|
|
80
|
+
created with an `init` callback that runs `SET search_path TO <schema>, public` on every
|
|
81
|
+
connection.
|
|
82
|
+
- Because of that `search_path` scoping, **the existing SQL text's unqualified table names
|
|
83
|
+
(`catalog`, `tags`, `queue_shadow`, `job_runs`, `otps`, etc.) resolve correctly to
|
|
84
|
+
`<schema>.<table>` without rewriting every query to be schema-qualified.** This was the
|
|
85
|
+
key insight that makes porting ~140 methods tractable instead of requiring a full ORM
|
|
86
|
+
rewrite.
|
|
87
|
+
- `?` positional placeholders are mechanically translated to asyncpg's `$1, $2, …` by a
|
|
88
|
+
shared `to_pg_sql()` helper. This is the *only* automatic translation — everything else
|
|
89
|
+
(SQLite-specific functions, `rowid`/`lastrowid`, upsert syntax, FTS5) must be hand-fixed
|
|
90
|
+
per call site (see §4).
|
|
91
|
+
|
|
92
|
+
This is **not** what `kryten_webqueue/catalog/db/engine.py` was scaffolded for — that file
|
|
93
|
+
builds a SQLAlchemy `AsyncEngine`/`async_sessionmaker` and **is not used by anything**
|
|
94
|
+
(dead code as of now). Before continuing, an explicit decision is needed:
|
|
95
|
+
|
|
96
|
+
- **(A) Keep the asyncpg/search_path approach** (what this session started) — faster to
|
|
97
|
+
finish, less abstraction, `engine.py` should probably be deleted or repurposed once this
|
|
98
|
+
is proven, and `SPEC-Sortie-2-connection-layer-port.md` should be updated to reflect
|
|
99
|
+
reality.
|
|
100
|
+
- **(B) Discard `_pg_base_domain.py` and do the original SQLAlchemy Core port** — more
|
|
101
|
+
idiomatic, matches the existing SPEC and `engine.py`, but is materially more work (schema
|
|
102
|
+
metadata, Core `text()` binds, session-per-request wiring in FastAPI dependencies) with no
|
|
103
|
+
session-1 progress toward it yet.
|
|
104
|
+
|
|
105
|
+
**Recommendation: (A).** The reasoning above (search_path reuse) is sound and most of the
|
|
106
|
+
remaining work is mechanical per-file translation, not architecture. This document assumes
|
|
107
|
+
(A) going forward.
|
|
108
|
+
|
|
109
|
+
### 3.2 Files created/modified this session
|
|
110
|
+
|
|
111
|
+
| File | Status |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `kryten_webqueue/catalog/db/_pg_base_domain.py` | **New.** `_PgDomainDB` base class: connection pool, `_execute`, `_execute_returning_id`, `_executemany`, `_fetch_one`, `_fetch_all`, `_fetch_val`, plus `to_pg_sql()` and a `_PgResult` shim (`.rowcount`, `.lastrowid`) so call sites can look similar to the aiosqlite ones. **Untested — never connected to a live database, never imported anywhere.** |
|
|
114
|
+
| `kryten_webqueue/catalog/db/sql/001_initial_schema.sql` | **Modified.** Added `id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY` to `users.otps` (SQLite's `verify_otp`/etc. use `rowid`, which doesn't exist in Postgres — needed a real PK). **This change is repo-only. It has NOT been applied to the live `chandra-1` database.** Live `users.otps` on chandra-1 currently has no `id` column (verified via `\d users.otps` this session). |
|
|
115
|
+
|
|
116
|
+
**Nothing else changed.** No domain classes, no facade wiring, no tests, no deployment.
|
|
117
|
+
|
|
118
|
+
### 3.3 Immediate next action if picking (A)
|
|
119
|
+
|
|
120
|
+
Apply the schema fix live before writing/testing the users-domain Postgres class:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ssh groberts@chandra-1.local
|
|
124
|
+
PGPASSWORD=kryten_secret_password psql -h 127.0.0.1 -U kryten -d webqueue -c \
|
|
125
|
+
"ALTER TABLE users.otps ADD COLUMN id BIGINT GENERATED ALWAYS AS IDENTITY;"
|
|
126
|
+
PGPASSWORD=kryten_secret_password psql -h 127.0.0.1 -U kryten -d webqueue -c \
|
|
127
|
+
"ALTER TABLE users.otps ADD PRIMARY KEY (id);"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Verify existing rows got sequential ids (`SELECT * FROM users.otps ORDER BY id;`) and that
|
|
131
|
+
this didn't violate anything (it won't — `otps` currently has ~4-8 rows, no FK references
|
|
132
|
+
it).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. Remaining work: exact checklist
|
|
137
|
+
|
|
138
|
+
Authoritative method lists below are taken directly from `_DOMAIN_METHOD_MAP` in
|
|
139
|
+
`kryten_webqueue/catalog/db/__init__.py` — that dict is the single source of truth for what
|
|
140
|
+
must exist on each domain object. Cross-check against it; it may have shifted since this
|
|
141
|
+
was written.
|
|
142
|
+
|
|
143
|
+
### 4.1 Wiring (do this last, after at least one domain is ported and tested)
|
|
144
|
+
|
|
145
|
+
In `kryten_webqueue/catalog/db/__init__.py`, `Database.__init__` currently only branches on
|
|
146
|
+
`self._layout` (`monolith` vs `partitioned`) — it never looks at
|
|
147
|
+
`self._db_config.backend`. Needs a new top-level branch:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
if self._db_config.backend == "postgres":
|
|
151
|
+
dsn = self._db_config.postgres.get_async_url() # already asyncpg-flavored (postgresql+asyncpg://...)
|
|
152
|
+
# NOTE: asyncpg.connect()/create_pool() wants a plain "postgresql://" DSN, NOT
|
|
153
|
+
# "postgresql+asyncpg://" (that prefix is a SQLAlchemy convention). Strip it, or add
|
|
154
|
+
# a separate get_asyncpg_dsn() to PostgresConfig that returns the bare form.
|
|
155
|
+
self.catalog = _PgCatalogDB(dsn, schema="catalog")
|
|
156
|
+
self.queue = _PgQueueDB(dsn, schema="queue")
|
|
157
|
+
self.jobs = _PgJobsDB(dsn, schema="jobs")
|
|
158
|
+
self.users = _PgUsersDB(dsn, schema="users")
|
|
159
|
+
elif self._layout == "partitioned":
|
|
160
|
+
... # existing sqlite branch, unchanged
|
|
161
|
+
else:
|
|
162
|
+
... # existing sqlite monolith branch, unchanged
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`connect()`, `close()` already just `asyncio.gather` across `self.catalog/queue/jobs/users`
|
|
166
|
+
— those should work unchanged as long as the Pg domain classes expose `connect()`/`close()`
|
|
167
|
+
with the same signature (they do, per `_pg_base_domain.py`).
|
|
168
|
+
|
|
169
|
+
`run_migrations()` must **skip** entirely for the Postgres branch — schema is applied via
|
|
170
|
+
`sql/001_initial_schema.sql` out-of-band, not via the SQLite-style versioned migration list.
|
|
171
|
+
`_PgDomainDB.run_migrations()` is already a no-op; just make sure `Database.run_migrations()`
|
|
172
|
+
doesn't call it in a way that breaks (currently fine, it's `await asyncio.gather(...)` on
|
|
173
|
+
whatever `.run_migrations()` no-ops to).
|
|
174
|
+
|
|
175
|
+
**PostgresConfig needs a fix**: `get_async_url()` returns `postgresql+asyncpg://...` which is
|
|
176
|
+
correct for SQLAlchemy but `asyncpg.connect()`/`create_pool()` do **not** accept the
|
|
177
|
+
`+asyncpg` dialect suffix — they want bare `postgresql://`. Add a
|
|
178
|
+
`get_asyncpg_dsn()` method (or strip the suffix inline) before wiring this up. This will
|
|
179
|
+
crash instantly if not fixed — write a quick unit test for it.
|
|
180
|
+
|
|
181
|
+
### 4.2 Domain: `users` (smallest, do first)
|
|
182
|
+
|
|
183
|
+
Methods (from `_DOMAIN_METHOD_MAP`, `users` domain), source mixins in parens:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
watchlist_add, watchlist_remove, watchlist_tokens, watchlist_count,
|
|
187
|
+
get_user_watchlist_tokens (_watchlist.py, _users_db.py)
|
|
188
|
+
store_otp, verify_otp, cleanup_expired_otps (_users_db.py)
|
|
189
|
+
create_link_code, get_valid_link_code, delete_link_code,
|
|
190
|
+
link_code_exists, purge_expired_link_codes,
|
|
191
|
+
create_device_key, get_device_key_by_hash, touch_device_key,
|
|
192
|
+
list_device_keys, delete_device_key, revoke_user_device_keys,
|
|
193
|
+
device_key_usernames (_devices.py)
|
|
194
|
+
add_feedback, list_feedback, count_feedback,
|
|
195
|
+
set_feedback_status, delete_feedback,
|
|
196
|
+
add_title_suggestion, list_title_suggestions,
|
|
197
|
+
count_title_suggestions, set_title_suggestion_status,
|
|
198
|
+
delete_title_suggestion (_feedback.py)
|
|
199
|
+
```
|
|
200
|
+
(Note: `create_feedback`/`update_feedback_status`/`create_title_suggestion`/
|
|
201
|
+
`update_title_suggestion_status` are also in the map as aliases — check whether those are
|
|
202
|
+
actually called anywhere or are dead map entries before implementing twice.)
|
|
203
|
+
|
|
204
|
+
**Known SQLite-isms to fix in this domain** (grep for these patterns yourself too, this list
|
|
205
|
+
may not be exhaustive):
|
|
206
|
+
- `_users_db.py::verify_otp` — `SELECT rowid FROM otps ...` / `UPDATE otps SET used=1 WHERE
|
|
207
|
+
rowid=?` → use the new `id` column (§3.3) instead of `rowid`.
|
|
208
|
+
- `datetime('now')` → `now()`. Appears in: `_devices.py` (`get_valid_link_code`,
|
|
209
|
+
`purge_expired_link_codes`, `touch_device_key`), `_users_db.py` (`verify_otp`,
|
|
210
|
+
`cleanup_expired_otps`).
|
|
211
|
+
- `datetime(expires_at) > datetime('now')` → `expires_at > now()` (the `datetime(...)` wrap
|
|
212
|
+
on a timestamptz column is a SQLite-ism for string-normalizing; unnecessary and invalid in
|
|
213
|
+
Postgres — just drop it).
|
|
214
|
+
- `cursor.lastrowid` → `_execute_returning_id(sql + " RETURNING id", params)`. Appears in
|
|
215
|
+
`_devices.py::create_device_key`, `_feedback.py::add_feedback` and
|
|
216
|
+
`add_title_suggestion`.
|
|
217
|
+
- `INSERT OR IGNORE INTO user_watchlist (...)` (`_watchlist.py::watchlist_add`) → `INSERT
|
|
218
|
+
INTO user_watchlist (...) VALUES (...) ON CONFLICT DO NOTHING`. Return value should be
|
|
219
|
+
based on whether a row was actually inserted — asyncpg's `execute()` status string for a
|
|
220
|
+
no-op conflict is `"INSERT 0 0"` vs `"INSERT 0 1"` on success; `_PgResult.rowcount` already
|
|
221
|
+
parses the trailing number, so `rowcount > 0` still works as the existing code expects.
|
|
222
|
+
|
|
223
|
+
### 4.3 Domain: `jobs`
|
|
224
|
+
|
|
225
|
+
Methods:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
start_job_run, finish_job_run, update_job_run_detail, add_job_run_logs,
|
|
229
|
+
get_job_run_logs, get_job_run, get_job_runs, reconcile_orphaned_job_runs,
|
|
230
|
+
get_job_schedules, get_job_schedule, upsert_job_schedule, delete_job_schedule,
|
|
231
|
+
prune_job_run_logs (_jobs_db.py)
|
|
232
|
+
enqueue_fetch, claim_next_fetch_item, finish_fetch_item, requeue_fetch_item,
|
|
233
|
+
requeue_fetch_item_for_retry, reset_running_fetch_items, get_fetch_queue,
|
|
234
|
+
count_fetch_queue, count_fetch_queue_pending, delete_fetch_queue_item
|
|
235
|
+
(_fetch_queue.py)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Known SQLite-isms:**
|
|
239
|
+
- `cursor.lastrowid` in `start_job_run`, `_fetch_queue.py::enqueue_fetch` → `RETURNING id`.
|
|
240
|
+
- `_db.executemany(...)` in `add_job_run_logs` → `_executemany()` (already implemented in
|
|
241
|
+
`_pg_base_domain.py` via `conn.executemany`).
|
|
242
|
+
- `datetime('now')` / `datetime('now', ?)` throughout (`finish_job_run`,
|
|
243
|
+
`upsert_job_schedule`'s `updated_at`, `prune_job_run_logs`'s `WHERE logged_at <
|
|
244
|
+
datetime('now', ?)`, `_fetch_queue.py`'s `started_at`/`finished_at`/the
|
|
245
|
+
`added_at = (SELECT datetime(MAX(added_at), '+1 second') FROM fetch_queue)` self-referencing
|
|
246
|
+
insert, and `finished_at < datetime('now', '-24 hours')`).
|
|
247
|
+
- `datetime('now', ?)` with a param like `'-30 days'` → `(now() + ($n)::interval)` — the
|
|
248
|
+
interval literal syntax (`'-30 days'`) is valid in both dialects, so the **parameter
|
|
249
|
+
value itself doesn't need to change**, only the SQL text wrapping it.
|
|
250
|
+
- `datetime(MAX(added_at), '+1 second')` → `(MAX(added_at) + interval '1 second')`.
|
|
251
|
+
- `datetime('now', '-24 hours')` (literal, no param) → `(now() - interval '24 hours')`.
|
|
252
|
+
- `ON CONFLICT(job_name) DO UPDATE SET ... updated_at = datetime('now')` in
|
|
253
|
+
`upsert_job_schedule` — the `ON CONFLICT` structure is already Postgres-compatible, just
|
|
254
|
+
fix the `datetime('now')` inside it.
|
|
255
|
+
- **This is the domain with the automated 30-day log pruner** (`prune_job_run_logs`,
|
|
256
|
+
see `docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md` §3.1). The compliance
|
|
257
|
+
constraint from that spec still applies: this must only ever delete from
|
|
258
|
+
`jobs.job_run_logs`. There's an existing SQLite test for this
|
|
259
|
+
(`tests/test_postgres_pruner.py`) — port/duplicate it for the Postgres path once this
|
|
260
|
+
domain works, don't skip it.
|
|
261
|
+
|
|
262
|
+
### 4.4 Domain: `queue`
|
|
263
|
+
|
|
264
|
+
Methods (largest non-catalog domain):
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
get_shadow_items, upsert_shadow_item, remove_shadow_items, update_shadow_position,
|
|
268
|
+
update_shadow_estimated_start, get_last_pay_uid, get_shadow_position_after,
|
|
269
|
+
get_pay_items, get_request_id_for_uid (_queue.py)
|
|
270
|
+
save_spend_request, get_spend_request, mark_spend_refunded, add_queue_history,
|
|
271
|
+
get_user_queue_history (_queue.py)
|
|
272
|
+
create_saved_playlist, get_saved_playlist, get_saved_playlists, list_saved_playlists,
|
|
273
|
+
update_saved_playlist, delete_saved_playlist, save_playlist_items,
|
|
274
|
+
get_saved_playlist_items, replace_playlist_items, append_playlist_item,
|
|
275
|
+
append_playlist_items, rotate_playlist_item_to_bottom, get_most_recent_playlist,
|
|
276
|
+
get_playlist_by_name, get_playlist_by_name_any, get_promo_pools,
|
|
277
|
+
get_promo_pool_items (_playlists.py)
|
|
278
|
+
create_playlist_schedule, get_playlist_schedule, list_playlist_schedules,
|
|
279
|
+
update_playlist_schedule, delete_playlist_schedule, get_schedules, get_schedule,
|
|
280
|
+
create_schedule, update_schedule, delete_schedule, mark_schedule_fired,
|
|
281
|
+
get_active_schedule, set_active_schedule, clear_active_schedule,
|
|
282
|
+
disable_active_lock, is_event_lock_active (_playlists.py)
|
|
283
|
+
record_play_completion, unrecord_play_completion, clear_play_state,
|
|
284
|
+
get_active_hidden_media_ids, get_reserved_media_ids, get_active_blackout_tokens,
|
|
285
|
+
is_media_restricted, get_played_at_for_tokens, get_promo_pool_media_ids,
|
|
286
|
+
purge_promo_completions, get_recently_played_completions (_queue_db.py)
|
|
287
|
+
upsert_blackout, prune_expired_blackouts, is_blackout,
|
|
288
|
+
count_active_blackouts, list_active_blackouts (_blackouts.py)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Known SQLite-isms — this is the highest-risk domain (real money via `spend_requests`,
|
|
292
|
+
live playback scheduling via `playlist_schedules`):**
|
|
293
|
+
- `INSERT OR REPLACE INTO queue_shadow (...)` (`_queue.py::upsert_shadow_item`) → needs
|
|
294
|
+
`ON CONFLICT (uid) DO UPDATE SET <every column> = EXCLUDED.<column>`. Write out every
|
|
295
|
+
column explicitly; don't try to be clever/generic here, get it right by hand and test it.
|
|
296
|
+
- `INSERT OR REPLACE INTO active_schedule (...)` (`_playlists.py::set_active_schedule`) →
|
|
297
|
+
same pattern, conflict target is `(id)` (it's always id=1, a singleton row) —
|
|
298
|
+
`ON CONFLICT (id) DO UPDATE SET ...`.
|
|
299
|
+
- `INSERT OR REPLACE INTO playlist_item_played (...)` (`_queue_db.py::record_play_completion`)
|
|
300
|
+
→ conflict target `(playlist_id, position)`.
|
|
301
|
+
- `INSERT OR IGNORE INTO spend_requests (...)` (`_queue.py`) → `ON CONFLICT DO NOTHING`.
|
|
302
|
+
- `cursor.lastrowid` in `_playlists.py` (playlist creation, schedule creation) →
|
|
303
|
+
`RETURNING id`.
|
|
304
|
+
- Pervasive `datetime('now')` / `datetime(col, ...)` arithmetic in `_playlists.py`'s
|
|
305
|
+
schedule-firing queries — **read the existing code comment above
|
|
306
|
+
`get_due_schedules`-equivalent logic carefully, it explicitly warns**:
|
|
307
|
+
> `# NOTE: fire_at must be wrapped in datetime() on BOTH sides. fire_at is ... A bare
|
|
308
|
+
> 'fire_at > datetime('now')' is a [bug]`
|
|
309
|
+
The Postgres equivalent needs the same care but is actually **simpler** since `fire_at` is
|
|
310
|
+
a native `timestamptz` column — comparisons like `fire_at > now()` just work without any
|
|
311
|
+
wrapping. But the *dynamic interval arithmetic* needs an explicit cast:
|
|
312
|
+
```sql
|
|
313
|
+
-- SQLite:
|
|
314
|
+
datetime(fire_at, '-' || pre_fire_lock_minutes || ' minutes') <= datetime('now')
|
|
315
|
+
-- Postgres:
|
|
316
|
+
(fire_at - (pre_fire_lock_minutes::text || ' minutes')::interval) <= now()
|
|
317
|
+
```
|
|
318
|
+
There are at least 3 near-identical copies of this predicate in `_playlists.py`
|
|
319
|
+
(`get_due_schedules`, `claim_due_schedule`-ish methods — check current method names).
|
|
320
|
+
**Write one test per schedule-firing method with a schedule due in 1 minute and one due in
|
|
321
|
+
1 hour, confirm only the due one is returned, before trusting this.**
|
|
322
|
+
- `record_play_completion` in `_queue_db.py` also does `datetime('now', ?)` for the
|
|
323
|
+
recently-played window — same interval-cast fix as jobs domain.
|
|
324
|
+
- `is_media_restricted`, `get_reserved_media_ids`, `resolve_friendly_tokens` — these were
|
|
325
|
+
added in the *SQLite partitioned* cross-domain-decoupling work (Sortie 3 of the SQLite
|
|
326
|
+
sprint, not the Postgres one) to work around SQLite's inability to `JOIN` across separate
|
|
327
|
+
database files. **In Postgres, catalog/queue/jobs/users are schemas in the *same*
|
|
328
|
+
database** — a real cross-schema `JOIN` is possible again (grant already covers it). You
|
|
329
|
+
have a choice here:
|
|
330
|
+
- (a) Keep the same Python-side decoupling pattern (`Database._get_reserved_tokens()` etc.
|
|
331
|
+
in `__init__.py`) for consistency with the SQLite code path and lower risk, or
|
|
332
|
+
- (b) Simplify to a single cross-schema SQL query now that it's actually possible.
|
|
333
|
+
**Recommendation: keep (a) for this port.** Don't do (b) as part of this migration; it's
|
|
334
|
+
a separate, lower-priority cleanup that touches both backends' facade methods and isn't
|
|
335
|
+
required for correctness. Flag it as a follow-up instead.
|
|
336
|
+
- **Important correctness note**: the SQLite path's `get_reserved_media_ids()` returns raw
|
|
337
|
+
`media_id` values that are often manifest URLs, not `friendly_token`s, and there is a
|
|
338
|
+
`catalog.resolve_friendly_tokens()` helper that maps them back (this was itself a bug fix
|
|
339
|
+
made earlier in this same session for the SQLite partitioned path — see CHANGELOG
|
|
340
|
+
v0.48.1 entry). **The Postgres queue domain's `get_reserved_media_ids()` must have the
|
|
341
|
+
same behavior** (return raw `media_id`, let the catalog domain resolve it) — don't
|
|
342
|
+
"fix" this differently for Postgres or you'll reintroduce the exact bug that was just
|
|
343
|
+
patched for SQLite.
|
|
344
|
+
|
|
345
|
+
### 4.5 Domain: `catalog` (largest, hardest, do last)
|
|
346
|
+
|
|
347
|
+
Methods:
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
get_item, get_item_admin, resolve_media, delete_catalog_item, get_catalog_brief,
|
|
351
|
+
get_item_facets, get_categories, get_tags, upsert_category, upsert_tag,
|
|
352
|
+
set_catalog_categories, set_catalog_tags, add_catalog_tag, remove_catalog_tag,
|
|
353
|
+
insert_catalog, update_catalog, update_cover_art, set_imdb_tt, get_item_by_imdb_tt,
|
|
354
|
+
delete_stale_catalog_items, find_catalog_by_title, start_sync_log, finish_sync_log,
|
|
355
|
+
get_sync_logs, log_item_edit, get_item_edit_history, get_items_by_tokens,
|
|
356
|
+
get_hidden_category_and_tag_tokens, resolve_friendly_tokens (_catalog.py)
|
|
357
|
+
get_people, get_studios, upsert_person, upsert_studio,
|
|
358
|
+
set_catalog_people, set_catalog_studios (_people.py)
|
|
359
|
+
get_enrichment_state, update_enrichment_state, get_enrichment_candidates,
|
|
360
|
+
get_identify_coverage (_enrichment.py)
|
|
361
|
+
get_motd_override, get_motd_overrides_for_week, upsert_motd_override,
|
|
362
|
+
delete_motd_override (_motd.py)
|
|
363
|
+
```
|
|
364
|
+
Plus **`browse`, `browse_count`, `search`, `search_count`** — these are NOT in the domain
|
|
365
|
+
map because they're already overridden directly on the `Database` facade in `__init__.py`
|
|
366
|
+
(cross-domain orchestration between `catalog` and `queue`). The facade methods call
|
|
367
|
+
`self.catalog.browse(..., is_partitioned=True, exclude_tokens=...)` etc. — the underlying
|
|
368
|
+
`_catalog.py::browse`/`browse_count` already accept `is_partitioned`/`exclude_tokens` params
|
|
369
|
+
(added for the SQLite partitioned path). **Whatever Postgres catalog implementation you
|
|
370
|
+
write needs to accept the same two params with the same semantics**, so the existing facade
|
|
371
|
+
orchestration code in `__init__.py` keeps working unchanged for both backends.
|
|
372
|
+
|
|
373
|
+
**Known SQLite-isms — this is where FTS5 must be replaced (Sortie 3's actual scope):**
|
|
374
|
+
- `search()`/`search_count()` currently do `FROM catalog_fts fts JOIN catalog c ON c.rowid =
|
|
375
|
+
fts.rowid WHERE catalog_fts MATCH ?` — **`catalog_fts` does not exist in Postgres and
|
|
376
|
+
`rowid` doesn't either.** This must become the hybrid `tsvector` + `pg_trgm` query
|
|
377
|
+
described in `docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md` §4.3:
|
|
378
|
+
```sql
|
|
379
|
+
WITH fts_matches AS (
|
|
380
|
+
SELECT c.friendly_token, c.title, ts_rank(c.search_vector, q) AS rank_score, 1 AS match_type
|
|
381
|
+
FROM catalog c, websearch_to_tsquery('english', $1) q
|
|
382
|
+
WHERE c.search_vector @@ q
|
|
383
|
+
),
|
|
384
|
+
trgm_matches AS (
|
|
385
|
+
SELECT c.friendly_token, c.title, similarity(c.title, $1) AS rank_score, 2 AS match_type
|
|
386
|
+
FROM catalog c
|
|
387
|
+
WHERE similarity(c.title, $1) > 0.3
|
|
388
|
+
AND c.friendly_token NOT IN (SELECT friendly_token FROM fts_matches)
|
|
389
|
+
)
|
|
390
|
+
SELECT * FROM fts_matches UNION ALL SELECT * FROM trgm_matches ORDER BY match_type ASC, rank_score DESC;
|
|
391
|
+
```
|
|
392
|
+
This is a genuinely different query shape (ranking, pagination, and the existing
|
|
393
|
+
`_facet_filter`/`_duration_range_filter`/exclusion-fragment helpers in `_catalog.py`
|
|
394
|
+
compose SQL via string concatenation onto a `WHERE` clause) — you cannot just swap the
|
|
395
|
+
`FROM`/`WHERE` and keep everything else; the whole `search()` method needs rewriting
|
|
396
|
+
around this two-CTE shape. Budget real time for this specifically; it's the single
|
|
397
|
+
hardest piece in the whole port. There is a fixture-corpus test plan in the SPEC (§6) —
|
|
398
|
+
build it before considering this domain "done."
|
|
399
|
+
- `insert_catalog`/`update_catalog` currently maintain the FTS5 index manually
|
|
400
|
+
(`INSERT INTO catalog_fts(rowid, friendly_token, ...) SELECT rowid, ... FROM catalog WHERE
|
|
401
|
+
friendly_token = ?`). **Delete this logic entirely for Postgres** — `search_vector` is a
|
|
402
|
+
`GENERATED ALWAYS AS (...) STORED` column, Postgres maintains it automatically on
|
|
403
|
+
INSERT/UPDATE. Do not try to port the manual FTS maintenance calls.
|
|
404
|
+
- `cursor.lastrowid` — none expected in this domain (catalog rows are keyed by
|
|
405
|
+
`friendly_token`, not autoincrement), but double-check `upsert_category`/`upsert_tag`/
|
|
406
|
+
`upsert_person`/`upsert_studio`/`upsert_month...` — those likely do need `RETURNING id`.
|
|
407
|
+
- `INSERT OR IGNORE INTO catalog_categories/catalog_tags/catalog_people/catalog_studios` →
|
|
408
|
+
`ON CONFLICT DO NOTHING` (these are all pure junction tables with composite PKs already).
|
|
409
|
+
- `datetime('now')` in `update_catalog`'s `updated_at`, `log_item_edit`'s `edited_at`
|
|
410
|
+
(check — may already default via `DEFAULT clock_timestamp()` in the Postgres schema, in
|
|
411
|
+
which case just omit the column from the INSERT rather than porting the SQLite literal).
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 5. Testing plan (do not skip this)
|
|
416
|
+
|
|
417
|
+
There is currently **zero** test coverage for any Postgres domain CRUD path. Existing
|
|
418
|
+
`tests/test_postgres_live.py` and `tests/test_postgres_config_and_engine.py` only check
|
|
419
|
+
schema/config, not application behavior, and they run against **the real chandra-1 database
|
|
420
|
+
that now holds real migrated production data** (spend history, watchlists, etc. from the
|
|
421
|
+
one ETL run).
|
|
422
|
+
|
|
423
|
+
### 5.1 Do not write test data into the shared chandra-1 database carelessly
|
|
424
|
+
|
|
425
|
+
Any new domain-CRUD tests must either:
|
|
426
|
+
- Use uniquely-prefixed, easily-identifiable test rows (e.g. `username='__test_pguser__'`,
|
|
427
|
+
`friendly_token` prefixed `test_pg_`) and clean them up in a fixture teardown, **or**
|
|
428
|
+
- Stand up a disposable test database/schema set on chandra-1 (e.g. `webqueue_test`) so real
|
|
429
|
+
data is never touched. **This is the safer option and is recommended** — ask for/verify
|
|
430
|
+
whether creating a second database is acceptable, or reuse the existing `sql/
|
|
431
|
+
001_initial_schema.sql` against a throwaway DB name.
|
|
432
|
+
|
|
433
|
+
### 5.2 Suggested test structure
|
|
434
|
+
|
|
435
|
+
Mirror `tests/test_multi_db_connection.py` (the SQLite partitioned test file) — same test
|
|
436
|
+
names/scenarios, but against the new Pg domain classes. That file already has good coverage
|
|
437
|
+
of exactly the cross-domain behaviors that matter (decoupled watchlist, reserved-item
|
|
438
|
+
exclusion via manifest_url resolution, blackout/recently-played filtering, job log
|
|
439
|
+
independence). Port those tests 1:1 as a baseline, then add the FTS/trigram-specific search
|
|
440
|
+
tests from `SPEC-Sortie-3-fts5-to-tsvector.md` §6.
|
|
441
|
+
|
|
442
|
+
### 5.3 Run order
|
|
443
|
+
|
|
444
|
+
1. Apply the `users.otps.id` migration (§3.3).
|
|
445
|
+
2. Fix `PostgresConfig.get_asyncpg_dsn()` (§4.1) and add a unit test for it.
|
|
446
|
+
3. Port `users` domain, test it in isolation.
|
|
447
|
+
4. Port `jobs` domain (includes the pruner — port `tests/test_postgres_pruner.py` for it),
|
|
448
|
+
test it.
|
|
449
|
+
5. Port `queue` domain (highest risk — economy + scheduling), test it thoroughly, including
|
|
450
|
+
the `datetime(fire_at, ...)` interval-arithmetic edge cases explicitly.
|
|
451
|
+
6. Port `catalog` domain including the FTS/trigram search rewrite, test it against a fixture
|
|
452
|
+
corpus per the SPEC.
|
|
453
|
+
7. Wire `Database.__init__` backend branch (§4.1).
|
|
454
|
+
8. Run the **entire** existing test suite (`uv run pytest -q`, currently 407 passing) to
|
|
455
|
+
confirm the SQLite path is completely unaffected — it must still be 100% green.
|
|
456
|
+
9. Only then: rebuild the chandra-1 Podman image, install the Quadlet units, start the
|
|
457
|
+
service, and smoke-test it **standing alone, before touching nginx**.
|
|
458
|
+
|
|
459
|
+
### 5.4 Smoke test after deployment (before nginx cutover)
|
|
460
|
+
|
|
461
|
+
From `chandra-1` itself (never through the public domain until this passes):
|
|
462
|
+
```bash
|
|
463
|
+
curl -I http://127.0.0.1:2010/auth/login
|
|
464
|
+
curl -s http://127.0.0.1:2010/queue/next-schedule
|
|
465
|
+
```
|
|
466
|
+
Then exercise an actual login + browse + search + watchlist round trip using a real browser
|
|
467
|
+
session pointed at `http://chandra-1.local:2010` directly (bypass nginx entirely for this
|
|
468
|
+
first pass).
|
|
469
|
+
|
|
470
|
+
### 5.5 Re-run the ETL immediately before real cutover
|
|
471
|
+
|
|
472
|
+
The one-shot ETL was run once, earlier, from a since-superseded snapshot of the grindhouse
|
|
473
|
+
SQLite data. `grindhouse.local` has continued serving live production traffic since then
|
|
474
|
+
(spend requests, watchlist adds, job runs, etc.). **Before the actual maintenance-window
|
|
475
|
+
cutover**, re-run `kryten_webqueue/migrate_sqlite_to_pg.py` against a *fresh* stop-the-world
|
|
476
|
+
snapshot, per `docs/postgres-cutover.md`. Do not assume the existing chandra-1 data is
|
|
477
|
+
current — it is now stale by however many days elapse between this session and the actual
|
|
478
|
+
cutover.
|
|
479
|
+
|
|
480
|
+
### 5.6 Rebuild and redeploy the image
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
# on chandra-1, after pulling latest main (including this port + v0.48.1 hotfix)
|
|
484
|
+
cd /opt/Devel/Kryten-Ecosystem/kryten-webqueue
|
|
485
|
+
git fetch --all --tags
|
|
486
|
+
git checkout <new-tag> # whatever version this port ships as
|
|
487
|
+
sudo podman build -f Containerfile -t localhost/kryten-webqueue:latest .
|
|
488
|
+
```
|
|
489
|
+
Then install the Quadlet units and start the service per
|
|
490
|
+
`docs/postgres-cutover.md` §"Step 5: Start Podman Quadlet on Chandra-1".
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 6. Safety reminders
|
|
495
|
+
|
|
496
|
+
- **Never point nginx at chandra-1 until the smoke tests in §5.4 pass directly against the
|
|
497
|
+
container.** Reverting nginx is the fast rollback if something goes wrong post-cutover —
|
|
498
|
+
see `docs/postgres-cutover.md` §3 for the exact rollback commands.
|
|
499
|
+
- **The economy/spend-request and playlist-scheduling logic in the `queue` domain is the
|
|
500
|
+
highest blast-radius code in this port.** Real money and live playback scheduling depend
|
|
501
|
+
on it. Do not rush the `datetime(fire_at, ...)` interval-arithmetic translation — test it
|
|
502
|
+
explicitly with schedules due in the near future vs. far future before trusting it.
|
|
503
|
+
- **Do not delete/rename the SQLite domain classes or the `_layout` branch in
|
|
504
|
+
`Database.__init__`.** `grindhouse.local` continues running SQLite in production during
|
|
505
|
+
and after this work; both code paths must coexist and both must stay green in the test
|
|
506
|
+
suite.
|
|
507
|
+
- If anything here is unclear or a method/table has changed since this was written, treat
|
|
508
|
+
`_DOMAIN_METHOD_MAP` in `kryten_webqueue/catalog/db/__init__.py` and the live chandra-1
|
|
509
|
+
schema (`\dt catalog.*`, `\dt queue.*`, etc. via `psql`) as the sources of truth, not this
|
|
510
|
+
document's method lists.
|