kryten-webqueue 0.46.0__tar.gz → 0.47.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.46.0 → kryten_webqueue-0.47.0}/.gitignore +4 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/CHANGELOG.md +36 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/config.example.json +9 -0
- kryten_webqueue-0.47.0/docs/DATABASE_ARCHITECTURE_PLAN.md +206 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/PRD-postgres-migration.md +203 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +90 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +137 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +113 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +96 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +69 -0
- kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +93 -0
- kryten_webqueue-0.47.0/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +144 -0
- kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +113 -0
- kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +106 -0
- kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +104 -0
- kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +51 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/app.py +1 -1
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/__init__.py +668 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_base_domain.py +138 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_catalog.py +165 -68
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_catalog_db.py +57 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_jobs_db.py +123 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_queue_db.py +198 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_users_db.py +53 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/__init__.py +13 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/catalog_schema.py +148 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/jobs_schema.py +67 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/queue_schema.py +136 -0
- kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/users_schema.py +78 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/config.py +81 -1
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/ytpipe/downloader.py +1 -3
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/builder.py +27 -23
- kryten_webqueue-0.47.0/kryten_webqueue/scripts/__init__.py +1 -0
- kryten_webqueue-0.47.0/kryten_webqueue/scripts/split_databases.py +14 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/pyproject.toml +2 -1
- kryten_webqueue-0.47.0/scripts/split_databases.py +379 -0
- kryten_webqueue-0.47.0/tests/test_concurrency_split.py +294 -0
- kryten_webqueue-0.47.0/tests/test_database_config_and_schemas.py +163 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_motd_publish.py +13 -10
- kryten_webqueue-0.47.0/tests/test_multi_db_connection.py +197 -0
- kryten_webqueue-0.46.0/kryten_webqueue/catalog/db/__init__.py +0 -42
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/AGENTS.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/README.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PUBLIC_API.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/device_keys.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_people.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/log_capture.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/render.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_motd.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/devices.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/public_api.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/keybindings.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/motd.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/link_device.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/my_list.html +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/AGENTS.md +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/patch_mediacms_tags.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/trigger_enrich.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_catalog_blackout.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_catalog_delete.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_classify.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_device_linking.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_fetch_queue_pacing.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_identify_step.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_people_facets.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_providers.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_queue_preview_fallback.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_search_facets.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_title_normalization.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_tmdb_index.py +0 -0
- {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_watchlist.py +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.47.0] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **SQLite Domain Partitioning (`sqlite-domain-separation`)**:
|
|
8
|
+
- Split database architecture isolating heavy background write jobs from real-time pollers across 4 discrete SQLite databases:
|
|
9
|
+
- `catalog.sqlite3` — Catalog items, FTS5 index, categories, tags, people, studios, enrichment state, MOTD overrides.
|
|
10
|
+
- `queue.sqlite3` — Queue shadow, spend requests, queue history, saved playlists, schedules, play completions, blackouts.
|
|
11
|
+
- `jobs.sqlite3` — Background job runs, per-line job run logs, schedules, and fetch queue.
|
|
12
|
+
- `users.sqlite3` — OTPs, device link codes, device API keys, watchlists, feedback, title suggestions.
|
|
13
|
+
- Multi-database connection facade (`Database`) managing domain sub-connections (`self.catalog`, `self.queue`, `self.jobs`, `self.users`) with independent WAL, checkpoints, and busy timeouts.
|
|
14
|
+
- Configurable `DatabaseConfig` supporting `layout="monolith"` and `layout="partitioned"` with safe path resolution and guards against accidental silent bypass of existing monolith databases.
|
|
15
|
+
- Automated database migration ETL script `scripts/split_databases.py` (`kryten-webqueue-split-db`) with checkpoint-safe reading, foreign key verification, and deterministic SHA-256 data parity validation.
|
|
16
|
+
- Concurrency stress test suite (`tests/test_concurrency_split.py`) verifying zero cross-domain lock contention between high-frequency queue polling, burst job logging, and web browse requests.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Decoupled cross-domain SQL queries:
|
|
21
|
+
- User watchlist ("My List") queries `users.sqlite3` for ordered tokens and hydrates item metadata from `catalog.sqlite3`.
|
|
22
|
+
- Recently-played hiding and weekend blackout exclusions resolved across domain boundaries without cross-file SQL joins.
|
|
23
|
+
- Promo-pool hide state purge orchestrated across `queue.sqlite3` and `catalog.sqlite3`.
|
|
24
|
+
|
|
25
|
+
## [0.46.1] - 2026-09-10
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **The grid is an even split again** (6/6 for a 12-slot Friday+Saturday
|
|
30
|
+
weekend). The 5/7 shape 0.46.0 inferred from the live MOTD was an artifact of
|
|
31
|
+
a markup bug: with the `<a>` tags left unclosed, the blank line after the
|
|
32
|
+
fifth poster looks like a night break but isn't, and most screen widths hide
|
|
33
|
+
the resulting reflow. The generated snippet closes its tags, so it renders the
|
|
34
|
+
intended 6/6.
|
|
35
|
+
- A night with more scheduled titles than grid positions now records a warning
|
|
36
|
+
(job result, admin panel, and log) instead of silently dropping the extras.
|
|
37
|
+
|
|
3
38
|
## [0.46.0] - 2026-09-10
|
|
4
39
|
|
|
5
40
|
### Changed
|
|
@@ -15,6 +50,7 @@
|
|
|
15
50
|
night is never truncated, so an over-full weekend shows every scheduled movie;
|
|
16
51
|
`motd.slots` is a target the grid is only padded *up* to when the weekend is
|
|
17
52
|
still thin.
|
|
53
|
+
(Reverted in 0.46.1 — the 5/7 reading was a markup artifact.)
|
|
18
54
|
- **Sunday is excluded.** New `motd.nights` (default `[1, 2]`) controls which
|
|
19
55
|
nights the grid covers; Sunday has no schedule yet, so its workbook titles are
|
|
20
56
|
parsed but ignored. Add `3` once the scheduled-playlists feature fills that
|
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
|
|
65
|
+
"database": {
|
|
66
|
+
"layout": "monolith",
|
|
67
|
+
"db_path": "/var/lib/kryten-webqueue/webqueue.db",
|
|
68
|
+
"data_dir": "/var/lib/kryten-webqueue/data",
|
|
69
|
+
"catalog_db_path": null,
|
|
70
|
+
"queue_db_path": null,
|
|
71
|
+
"jobs_db_path": null,
|
|
72
|
+
"users_db_path": null
|
|
73
|
+
},
|
|
65
74
|
"db_path": "/var/lib/kryten-webqueue/webqueue.db",
|
|
66
75
|
|
|
67
76
|
"image_dir": "/var/lib/kryten-webqueue/images",
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Database Architecture & Evolution Plan: Kryten-WebQueue
|
|
2
|
+
|
|
3
|
+
**Status**: Approved Architecture Plan
|
|
4
|
+
**Target System**: `kryten-webqueue`
|
|
5
|
+
**Author**: Senior Microservice Architect (Kryten)
|
|
6
|
+
**Last Updated**: September 2026
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Executive Summary
|
|
11
|
+
|
|
12
|
+
`kryten-webqueue` has evolved from a lightweight catalog viewer into a multi-faceted web application hosting the CyTube channel catalog, real-time queue shadows, automated playlist schedulers, background media ingestion, TMDB enrichment pipelines, user watchlists, device API authentication, and per-line job logging.
|
|
13
|
+
|
|
14
|
+
Under heavy concurrent operation, the application suffers from SQLite database write-lock contention (`sqlite3.OperationalError: database is locked`) because all domains share a single SQLite database file and a single async worker connection.
|
|
15
|
+
|
|
16
|
+
This document outlines the **two-part architectural roadmap**:
|
|
17
|
+
|
|
18
|
+
1. **Part 1 (Immediate / Stopgap)**: **SQLite Domain Partitioning** — Split the monolithic SQLite database into 4 logical database files (`catalog.db`, `queue.db`, `jobs.db`, `users.db`) to isolate high-throughput writers (job logs, enrichment syncs) from real-time pollers and public browse traffic. This removes cross-domain contention; it does not remove writer serialization within a single domain file.
|
|
19
|
+
2. **Part 2 (Strategic / Chandra-1 Migration)**: **PostgreSQL on Chandra-1 via Podman** — Migrate all partitioned domains into one PostgreSQL database (`webqueue`) on `chandra-1` utilizing schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`), powered by **SQLAlchemy 2.0 (`asyncpg`)**, PostgreSQL Full-Text Search (`tsvector` + GIN) + `pg_trgm` fuzzy matching, and automated 30-day job log pruning (strictly exempting all financial, chat, and purchase records).
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌────────────────────────────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ TWO-PART ROADMAP │
|
|
24
|
+
├────────────────────────────────────────┬───────────────────────────────────────────────┤
|
|
25
|
+
│ PART 1: SQLite Domain Partitioning │ PART 2: Chandra-1 PostgreSQL & Podman Pod │
|
|
26
|
+
├────────────────────────────────────────┼───────────────────────────────────────────────┤
|
|
27
|
+
│ • Split single DB into 4 files: │ • Single Postgres database `webqueue` on │
|
|
28
|
+
│ - catalog.db (MediaCMS mirror, art) │ chandra-1 with logical schemas. │
|
|
29
|
+
│ - queue.db (shadow, schedules, play) │ • SQLAlchemy 2.0 (asyncpg) connection pool. │
|
|
30
|
+
│ - jobs.db (job runs, logs, fetch) │ • English `tsvector` + GIN + `pg_trgm` search.│
|
|
31
|
+
│ - users.db (OTPs, devices, list) │ • 30-day job run log pruning policy (strictly │
|
|
32
|
+
│ • Decouple cross-table JOINs in Python │ exempting economy, spend, and chat data). │
|
|
33
|
+
│ • Zero lock contention between batch │ • Podman container deployment on chandra-1 │
|
|
34
|
+
│ jobs and real-time playback pollers. │ with shared media asset volumes. │
|
|
35
|
+
└────────────────────────────────────────┴───────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Part 1: SQLite Domain Partitioning & Lock Elimination
|
|
41
|
+
|
|
42
|
+
### 1.1 Root Cause of Current Contention
|
|
43
|
+
|
|
44
|
+
1. **Single Writer Serialization**: In SQLite WAL mode, any number of readers can proceed concurrently, but only one write transaction can execute at a time per database file.
|
|
45
|
+
2. **Single Connection Dispatcher**: `_connection.py` instantiates a single `aiosqlite.Connection`. In `aiosqlite`, all calls queue onto a single background OS thread, serializing even concurrent read operations.
|
|
46
|
+
3. **Collision of Asynchronous Workloads**:
|
|
47
|
+
- **Heavy Writers**: `catalog_enrich` (updating thousands of item records), `catalog_sync`, `fetch_queue_drain` (yt-dlp ingestion), and `log_capture.py` (bulk inserting hundreds of log rows per job run).
|
|
48
|
+
- **Real-Time Pollers & API**: `StatePoller` (every 2–5s), `RacePoller`, `PlaylistScheduler`, `CompletionRecorder`, `PresenceRefundMonitor`, and device API key authentication (updating `last_used_at` per HTTP request).
|
|
49
|
+
- When a batch job holds the WAL write lock or issues rapid write transactions, real-time pollers and web requests exceed the 5000ms `busy_timeout` and crash with `database is locked`.
|
|
50
|
+
|
|
51
|
+
### 1.2 Domain Partitioning Strategy
|
|
52
|
+
|
|
53
|
+
We partition tables into 4 dedicated SQLite databases:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌──────────────────────────┐ ┌──────────────────────────┐
|
|
57
|
+
│ catalog.db │ │ queue.db │
|
|
58
|
+
├──────────────────────────┤ ├──────────────────────────┤
|
|
59
|
+
│ • catalog │ │ • queue_shadow │
|
|
60
|
+
│ • catalog_fts (FTS5) │ │ • spend_requests │
|
|
61
|
+
│ • categories │ │ • queue_history │
|
|
62
|
+
│ • catalog_categories │ │ • saved_playlists │
|
|
63
|
+
│ • tags │ │ • saved_playlist_items │
|
|
64
|
+
│ • catalog_tags │ │ • playlist_schedules │
|
|
65
|
+
│ • people │ │ • active_schedule │
|
|
66
|
+
│ • catalog_people │ │ • play_completions │
|
|
67
|
+
│ • studios │ │ • playlist_item_played │
|
|
68
|
+
│ • catalog_studios │ │ • catalog_blackouts │
|
|
69
|
+
│ • item_enrichment_state │ └──────────────────────────┘
|
|
70
|
+
│ • item_edit_log │
|
|
71
|
+
│ • sync_log │ ┌──────────────────────────┐
|
|
72
|
+
│ • motd_overrides │ │ users.db │
|
|
73
|
+
└──────────────────────────┘ ├──────────────────────────┤
|
|
74
|
+
│ • user_watchlist │
|
|
75
|
+
┌──────────────────────────┐ │ • otps │
|
|
76
|
+
│ jobs.db │ │ • device_link_codes │
|
|
77
|
+
├──────────────────────────┤ │ • device_api_keys │
|
|
78
|
+
│ • job_runs │ │ • feedback │
|
|
79
|
+
│ • job_run_logs │ │ • title_suggestions │
|
|
80
|
+
│ • job_schedules │ └──────────────────────────┘
|
|
81
|
+
│ • fetch_queue │
|
|
82
|
+
└──────────────────────────┘
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 1.3 Cross-Domain Query Decoupling
|
|
86
|
+
|
|
87
|
+
Separating SQLite database files removes the ability to perform single SQL `JOIN`s across tables in different files. We resolve the existing cross-table queries in the application layer:
|
|
88
|
+
|
|
89
|
+
1. **Recently-Played Catalog Suppression Filter**:
|
|
90
|
+
- *Previous*: `catalog` query joined `play_completions` and `playlist_item_played`.
|
|
91
|
+
- *Partitioned*: `queue.db` executes `get_active_hidden_media_ids() -> set[str]`. The browse query in `catalog.db` filters with `WHERE friendly_token NOT IN (...)`.
|
|
92
|
+
2. **User Watchlist ("My List")**:
|
|
93
|
+
- *Previous*: `user_watchlist JOIN catalog`.
|
|
94
|
+
- *Partitioned*: Query `users.db` for `friendly_tokens` for the user, then query `catalog.db` with `get_catalog_items_by_tokens(tokens)`.
|
|
95
|
+
3. **Title Suggestions Catalog Match**:
|
|
96
|
+
- *Previous*: `title_suggestions` lookup checking if `catalog_token` exists.
|
|
97
|
+
- *Partitioned*: Check `catalog.db` by token/title independently during triage or submission.
|
|
98
|
+
4. **Job Run Logs & Job History**:
|
|
99
|
+
- `JobManager` writes exclusively to `jobs.db`. Background enrichment tasks write their metadata updates to `catalog.db` without locking the job log tables.
|
|
100
|
+
|
|
101
|
+
### 1.4 Partitioning Limits and Safety
|
|
102
|
+
|
|
103
|
+
Partitioning removes only cross-domain SQLite contention. `catalog` still contains browse reads
|
|
104
|
+
and enrichment writes, while `queue` still contains poller, scheduler, and queue-history writes.
|
|
105
|
+
Each domain therefore uses short, bounded transactions, retry/latency metrics, and domain-scoped
|
|
106
|
+
concurrency tests. A `busy_timeout` is a wait budget, not a correctness mechanism.
|
|
107
|
+
|
|
108
|
+
The layout is selected explicitly as `monolith` or `partitioned`. Startup never chooses a new
|
|
109
|
+
default data directory when a legacy database exists, and migration history is never copied from
|
|
110
|
+
the monolith: each partition has a fresh baseline schema version after ETL validation.
|
|
111
|
+
|
|
112
|
+
### 1.5 Part 1 Implementation Artifacts
|
|
113
|
+
|
|
114
|
+
- **Sprint Specs**: Located in `docs/sqlite-domain-separation/`
|
|
115
|
+
- `PRD-sqlite-domain-separation.md`
|
|
116
|
+
- `SPEC-Sortie-1-schema-partitioning-and-config.md`
|
|
117
|
+
- `SPEC-Sortie-2-multi-db-connection-layer.md`
|
|
118
|
+
- `SPEC-Sortie-3-cross-domain-query-decoupling.md`
|
|
119
|
+
- `SPEC-Sortie-4-etl-split-script-and-validation.md`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Part 2: Chandra-1 PostgreSQL & Podman Migration
|
|
124
|
+
|
|
125
|
+
### 2.1 Target Architecture
|
|
126
|
+
|
|
127
|
+
Upon completion of Part 1, the partitioned domain model directly maps to PostgreSQL. Instead of maintaining multiple SQLite files, the application connects to a **single PostgreSQL database (`webqueue`)** on `chandra-1`, structured with logical schemas:
|
|
128
|
+
|
|
129
|
+
- `catalog.*` — MediaCMS catalog mirror, cast/crew, tags, categories, enrichment state, MOTD overrides.
|
|
130
|
+
- `queue.*` — Queue shadow, spend requests, saved playlists, schedules, play completions, blackouts.
|
|
131
|
+
- `jobs.*` — Job schedules, job runs, per-line job logs, fetch queue.
|
|
132
|
+
- `users.*` — OTP authentication, device link codes & API keys, user watchlists, feedback, title suggestions.
|
|
133
|
+
- `tmdb.*` — Persistent, queryable local TMDB dump index.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
┌────────────────────────────────────────────────────────────────────────────────────────┐
|
|
137
|
+
│ chandra-1 Host System │
|
|
138
|
+
│ │
|
|
139
|
+
│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
|
|
140
|
+
│ │ Podman Pod: kryten-webqueue │ │
|
|
141
|
+
│ │ │ │
|
|
142
|
+
│ │ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │ │
|
|
143
|
+
│ │ │ kryten-webqueue-app │ │ nginx proxy │ │ │
|
|
144
|
+
│ │ │ (FastAPI + uvicorn) │◄─────────►│ (Reverse Proxy + SSL) │ │ │
|
|
145
|
+
│ │ │ SQLAlchemy 2.0 (asyncpg) │ │ │ │ │
|
|
146
|
+
│ │ └──────────────┬──────────────┘ └────────────────┬─────────────────┘ │ │
|
|
147
|
+
│ └─────────────────┼───────────────────────────────────────────┼────────────────────┘ │
|
|
148
|
+
│ │ │ │
|
|
149
|
+
│ ▼ ▼ │
|
|
150
|
+
│ ┌─────────────────────────────────────┐ ┌──────────────────────────────────────┐ │
|
|
151
|
+
│ │ PostgreSQL on chandra-1 │ │ Shared Media Volume │ │
|
|
152
|
+
│ │ Database: webqueue │ │ /var/lib/kryten/media │ │
|
|
153
|
+
│ │ Schemas: catalog, queue, │ │ - cover art / posters │ │
|
|
154
|
+
│ │ jobs, users, tmdb │ │ - TMDB dump files │ │
|
|
155
|
+
│ └─────────────────────────────────────┘ └──────────────────────────────────────┘ │
|
|
156
|
+
└────────────────────────────────────────────────────────────────────────────────────────┘
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 2.2 Core Technical Specifications
|
|
160
|
+
|
|
161
|
+
1. **ORM & Driver**: **SQLAlchemy 2.0 with `asyncpg`** (`create_async_engine`, `async_sessionmaker`, `AsyncSession`).
|
|
162
|
+
- Retains clean async repository layer with type-safe query construction and parameterized execution.
|
|
163
|
+
- Automatic connection pooling (`pool_size=10, max_overflow=20, pool_pre_ping=True`).
|
|
164
|
+
2. **Search Engine**:
|
|
165
|
+
- **Full-Text Search**: English `tsvector` generated column + `GIN` index on `catalog.title` and `catalog.description`.
|
|
166
|
+
- **Typo-Tolerant Matching**: `pg_trgm` extension with GIN/GiST index on `catalog.title` for fuzzy `similarity()` matching (e.g., matching "terminatr" to "The Terminator").
|
|
167
|
+
- Eliminates all SQLite FTS5 query-sanitization crashes.
|
|
168
|
+
3. **Automated Log Pruning Policy**:
|
|
169
|
+
- A scheduled background job cleans up rows in `jobs.job_run_logs` older than **30 days**.
|
|
170
|
+
- **CRITICAL COMPLIANCE CONSTRAINT**: This pruning policy applies **EXCLUSIVELY** to `jobs.job_run_logs`. It must **NEVER** touch or prune:
|
|
171
|
+
- Chat logs or user communication records
|
|
172
|
+
- Economy or z-coin purchase history (`spend_requests`, `queue_history`)
|
|
173
|
+
- User feedback or moderation audit logs (`item_edit_log`, `feedback`)
|
|
174
|
+
4. **ETL & Data Migration**:
|
|
175
|
+
- Streaming migration script (`migrate_sqlite_to_pg.py`) reading from the 4 SQLite databases and writing into PostgreSQL with foreign key verification, sequence resets, and JSONB conversion.
|
|
176
|
+
|
|
177
|
+
### 2.3 Cutover Contract
|
|
178
|
+
|
|
179
|
+
WebQueue runs as a rootful Quadlet application container on `chandra-1`; PostgreSQL is
|
|
180
|
+
host-managed on the same host and reached through an explicit container host gateway, never
|
|
181
|
+
container-localhost. The PostgreSQL cutover uses a maintenance window and forward-fix recovery:
|
|
182
|
+
the SQLite snapshot remains read-only for one release, but it is not an automatic rollback target
|
|
183
|
+
after PostgreSQL accepts writes.
|
|
184
|
+
|
|
185
|
+
### 2.4 Part 2 Implementation Artifacts
|
|
186
|
+
|
|
187
|
+
- **Sprint Specs**: Located in `docs/postgres-migration/`
|
|
188
|
+
- `PRD-postgres-migration.md`
|
|
189
|
+
- `SPEC-Sortie-1-db-config-and-pool.md`
|
|
190
|
+
- `SPEC-Sortie-2-connection-layer-port.md`
|
|
191
|
+
- `SPEC-Sortie-3-fts5-to-tsvector.md`
|
|
192
|
+
- `SPEC-Sortie-4-tmdb-index-database.md`
|
|
193
|
+
- `SPEC-Sortie-5-etl-migration.md`
|
|
194
|
+
- `SPEC-Sortie-6-tests-cutover-release.md`
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Sequencing & Next Steps
|
|
199
|
+
|
|
200
|
+
1. **Phase 1 Execution (Immediate)**:
|
|
201
|
+
- Implement Part 1 (SQLite Domain Partitioning) to resolve production write-lock crashes immediately without requiring infrastructure migration on chandra-1.
|
|
202
|
+
- Run `split_databases.py` in staging/production to migrate single SQLite DB to `catalog.sqlite3`, `queue.sqlite3`, `jobs.sqlite3`, `users.sqlite3`.
|
|
203
|
+
2. **Phase 2 Execution (Subsequent)**:
|
|
204
|
+
- Bootstrap PostgreSQL `webqueue` database on `chandra-1`.
|
|
205
|
+
- Deploy Podman container on `chandra-1` with SQLAlchemy 2.0 + asyncpg.
|
|
206
|
+
- Execute ETL migration and switch production traffic.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# PRD: PostgreSQL & Chandra-1 Podman Migration
|
|
2
|
+
|
|
3
|
+
**Sprint**: `postgres-migration`
|
|
4
|
+
**Status**: Planned - Post SQLite Domain Separation
|
|
5
|
+
**Builds on**: `sqlite-domain-separation`
|
|
6
|
+
**Target version**: `0.48.0`
|
|
7
|
+
**Workflow**: [../../AGENT-WORKFLOW-GUIDE.md](../../AGENT-WORKFLOW-GUIDE.md)
|
|
8
|
+
**Parent Plan**: [../DATABASE_ARCHITECTURE_PLAN.md](../DATABASE_ARCHITECTURE_PLAN.md)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Executive Summary
|
|
13
|
+
|
|
14
|
+
Following SQLite domain separation, `kryten-webqueue` will move its application data to one
|
|
15
|
+
PostgreSQL 16+ database, `webqueue`, on `chandra-1`. The database has five logical schemas:
|
|
16
|
+
`catalog`, `queue`, `jobs`, `users`, and `tmdb`. The application will use SQLAlchemy 2.0 async
|
|
17
|
+
sessions backed by `asyncpg`, with PostgreSQL full-text search and trigram matching replacing
|
|
18
|
+
SQLite FTS5.
|
|
19
|
+
|
|
20
|
+
The cutover is a planned maintenance-window migration. SQLite remains an immutable recovery
|
|
21
|
+
snapshot for one release, but production writes accepted after cutover are repaired forward in
|
|
22
|
+
PostgreSQL; the system does not silently fall back to a divergent SQLite copy.
|
|
23
|
+
|
|
24
|
+
## 2. Decisions and Boundaries
|
|
25
|
+
|
|
26
|
+
The following decisions are fixed for this sprint:
|
|
27
|
+
|
|
28
|
+
- One PostgreSQL database named `webqueue` owns all five schemas. `tmdb` is a schema, not a
|
|
29
|
+
separate database, so catalog enrichment can use relational queries when needed.
|
|
30
|
+
- The application runs as a rootful Podman Quadlet service on `chandra-1`.
|
|
31
|
+
- PostgreSQL is host-managed on `chandra-1`. The rootful application container connects through
|
|
32
|
+
an explicitly configured host-gateway address; it must not use `localhost`.
|
|
33
|
+
- The migration uses a maintenance window and a forward-fix rollback policy. There is no
|
|
34
|
+
dual-write phase and no automatic return to SQLite after PostgreSQL accepts writes.
|
|
35
|
+
- Existing api-gate, MediaCMS, and CyTube integration contracts are unchanged. This sprint does
|
|
36
|
+
not add direct service-to-service communication paths.
|
|
37
|
+
|
|
38
|
+
## 3. Goals and Success Metrics
|
|
39
|
+
|
|
40
|
+
- Preserve all application records and identity values during migration without external
|
|
41
|
+
TMDB/OMDB calls.
|
|
42
|
+
- Remove SQLite writer starvation between independent workloads after the SQLite partitioning
|
|
43
|
+
release, then move to PostgreSQL MVCC and pooled connections.
|
|
44
|
+
- Preserve public and administrative response shapes, queue semantics, OTP single-use behavior,
|
|
45
|
+
device-key behavior, and audit history.
|
|
46
|
+
- Provide typo-tolerant search while removing FTS5 parser failures.
|
|
47
|
+
- Complete staging rehearsal and production cutover with source/destination parity, foreign-key
|
|
48
|
+
validation, application smoke tests, and no unplanned data loss.
|
|
49
|
+
|
|
50
|
+
Success is measured by zero unhandled database errors during the concurrency test suite, no
|
|
51
|
+
migration verification mismatch, successful health/browse/search/queue smoke tests, and a
|
|
52
|
+
recorded cutover checkpoint that can be restored for investigation.
|
|
53
|
+
|
|
54
|
+
## 4. Target Architecture
|
|
55
|
+
|
|
56
|
+
### 4.1 Schema Ownership
|
|
57
|
+
|
|
58
|
+
| Schema | Tables and responsibility |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `catalog` | Catalog mirror, FTS source data, categories, tags, people, studios, enrichment state, edit log, sync log, MOTD overrides. |
|
|
61
|
+
| `queue` | Queue shadow, spend requests, queue history, playlists, schedules, completions, and blackouts. |
|
|
62
|
+
| `jobs` | Job schedules, job runs, run logs, and fetch queue. |
|
|
63
|
+
| `users` | OTPs, device link codes, device API keys, watchlists, feedback, and title suggestions. |
|
|
64
|
+
| `tmdb` | Persistent TMDB dump index and index metadata. |
|
|
65
|
+
|
|
66
|
+
Schema ownership organizes privileges, migrations, and repository modules. It does not imply
|
|
67
|
+
separate transactions: a single SQLAlchemy session may atomically update related WebQueue
|
|
68
|
+
schemas. External economy and CyTube actions remain distributed operations and require
|
|
69
|
+
idempotency and reconciliation rather than a cross-system transaction.
|
|
70
|
+
|
|
71
|
+
### 4.2 Access Model
|
|
72
|
+
|
|
73
|
+
- All application repository I/O uses SQLAlchemy 2.0 async sessions and parameterized named
|
|
74
|
+
binds. Raw `asyncpg` calls and mechanical `?` to `$n` translation are out of scope.
|
|
75
|
+
- Repository methods retain their public return shapes. A request or job defines the transaction
|
|
76
|
+
boundary; helpers must not commit independently unless explicitly documented as standalone.
|
|
77
|
+
- Migrations use a dedicated migrator role and a PostgreSQL advisory lock. Application instances
|
|
78
|
+
never race to apply DDL during startup.
|
|
79
|
+
- The runtime role receives only application DML privileges. The migration role owns schema DDL.
|
|
80
|
+
The scheduled log-pruner uses a restricted maintenance role that can delete only from
|
|
81
|
+
`jobs.job_run_logs`.
|
|
82
|
+
|
|
83
|
+
### 4.3 Configuration and Secrets
|
|
84
|
+
|
|
85
|
+
`database.backend` is `sqlite` or `postgres`. The transitional SQLite configuration explicitly
|
|
86
|
+
selects `monolith` or `partitioned`; startup rejects ambiguous layouts and rejects an unmigrated
|
|
87
|
+
legacy database instead of creating empty default files.
|
|
88
|
+
|
|
89
|
+
For PostgreSQL, connection resolution is `dsn_env`, then a password-free `dsn`, then assembled
|
|
90
|
+
host/port/user/database values. `KRYTEN_WEBQUEUE_PG_PASSWORD` is the only password source.
|
|
91
|
+
Configuration files must not contain a password or a DSN with an embedded password. The
|
|
92
|
+
container receives its database settings through a root-owned, mode-0600 environment file.
|
|
93
|
+
|
|
94
|
+
### 4.4 Container Connectivity
|
|
95
|
+
|
|
96
|
+
The rootful Quadlet supplies the host-gateway mapping and configures the database host as that
|
|
97
|
+
mapping (for example, `host.containers.internal` mapped to `host-gateway`). `localhost:5432` is
|
|
98
|
+
not valid for the application container unless PostgreSQL is deliberately moved into the same
|
|
99
|
+
pod, which is not part of this sprint. Deployment verification includes a connection test from
|
|
100
|
+
inside the running application container.
|
|
101
|
+
|
|
102
|
+
## 5. PostgreSQL Design
|
|
103
|
+
|
|
104
|
+
### 5.1 Migrations and Types
|
|
105
|
+
|
|
106
|
+
PostgreSQL DDL lives in ordered, immutable migration files with a tracked schema-version table.
|
|
107
|
+
The initial migration is a clean PostgreSQL baseline; it does not replay the SQLite migration
|
|
108
|
+
history or its historic data mutations. Each foreign key is schema-qualified, and migration tests
|
|
109
|
+
validate constraints, unique indexes, cascade behavior, and application-required indexes.
|
|
110
|
+
|
|
111
|
+
SQLite timestamps are interpreted as UTC during ETL. The ETL preflight rejects unparseable
|
|
112
|
+
values rather than guessing. Columns designated as structured JSON are validated before writing
|
|
113
|
+
`jsonb`; free-form log and detail fields remain `text` unless a separate compatibility decision
|
|
114
|
+
changes their contract. Identity values are imported with `OVERRIDING SYSTEM VALUE`, then every
|
|
115
|
+
identity sequence is reset with correct empty-table semantics.
|
|
116
|
+
|
|
117
|
+
### 5.2 Search
|
|
118
|
+
|
|
119
|
+
`catalog.catalog` stores a generated English `tsvector` with title-weighted ranking and a GIN
|
|
120
|
+
index. `pg_trgm` supplies a title trigram index and a fuzzy fallback with an initial similarity
|
|
121
|
+
threshold of `0.3`. Search uses `websearch_to_tsquery` with bound parameters.
|
|
122
|
+
|
|
123
|
+
`catalog_fts` is a derived SQLite-only index. It is neither copied nor checksum-compared during
|
|
124
|
+
ETL. PostgreSQL search is rebuilt from `catalog.catalog` and verified against exact, multi-word,
|
|
125
|
+
fuzzy, and adversarial-input fixtures.
|
|
126
|
+
|
|
127
|
+
### 5.3 Retention
|
|
128
|
+
|
|
129
|
+
Only `jobs.job_run_logs` is eligible for retention deletion. The daily 04:00 UTC task deletes
|
|
130
|
+
bounded batches using an index on `logged_at`, records a metric and audit log with the deleted
|
|
131
|
+
row count, and retries on transient database failures. It never deletes `queue.spend_requests`,
|
|
132
|
+
`queue.queue_history`, `users.feedback`, `users.title_suggestions`, `catalog.item_edit_log`, or
|
|
133
|
+
records owned by other services.
|
|
134
|
+
|
|
135
|
+
## 6. ETL and Verification
|
|
136
|
+
|
|
137
|
+
The ETL reads partitioned SQLite files read-only after the source service and schedulers have
|
|
138
|
+
stopped. It creates a fresh PostgreSQL target, streams tables in foreign-key-safe order, and
|
|
139
|
+
never calls an external API. It writes only to a staging target that has passed migrations.
|
|
140
|
+
|
|
141
|
+
Verification is deterministic for every source table:
|
|
142
|
+
|
|
143
|
+
1. Compare row counts.
|
|
144
|
+
2. Compare canonical hashes over stable primary-key ordering, normalized UTC timestamps, and
|
|
145
|
+
canonical JSON representations.
|
|
146
|
+
3. Validate foreign keys, uniqueness, and sequence next values in PostgreSQL.
|
|
147
|
+
4. Rebuild and test derived search indexes.
|
|
148
|
+
5. Run application-level fixtures for browse, recently-played suppression, watchlist ordering,
|
|
149
|
+
queue history, OTP redemption, device-key authentication, jobs, and search.
|
|
150
|
+
|
|
151
|
+
The source is captured with a SQLite backup/checkpoint-safe procedure while the service is
|
|
152
|
+
stopped. Target files are created in a staging location for the SQLite split release; the source
|
|
153
|
+
is never renamed or deleted automatically. Archival requires an explicit operator action after
|
|
154
|
+
verification.
|
|
155
|
+
|
|
156
|
+
## 7. Cutover and Recovery
|
|
157
|
+
|
|
158
|
+
1. Rehearse the complete migration against a representative production snapshot and record
|
|
159
|
+
timings, hashes, and failures.
|
|
160
|
+
2. Build and verify the rootful Quadlet image and host-gateway database connectivity on
|
|
161
|
+
`chandra-1`.
|
|
162
|
+
3. Enter maintenance mode; stop WebQueue, its scheduler, and any writer that can touch the
|
|
163
|
+
SQLite files.
|
|
164
|
+
4. Capture a verified SQLite backup, run PostgreSQL migrations once with the migrator role, and
|
|
165
|
+
run the ETL into a fresh target.
|
|
166
|
+
5. Require all verification gates to pass before granting the runtime/pruner roles and starting
|
|
167
|
+
the Quadlet service.
|
|
168
|
+
6. Run health, browse, search, queue, admin, OTP, device-key, and job-log smoke tests. Monitor
|
|
169
|
+
connection-pool saturation, query latency, database errors, and scheduler health.
|
|
170
|
+
7. Preserve the SQLite snapshot read-only for one release. After PostgreSQL accepts a write,
|
|
171
|
+
recovery is a forward repair in PostgreSQL using recorded request identifiers and audit data;
|
|
172
|
+
reverting to SQLite is not an automatic rollback path.
|
|
173
|
+
|
|
174
|
+
A failed pre-cutover verification returns to the unchanged SQLite service. A post-cutover defect
|
|
175
|
+
is triaged as a forward fix; any manual recovery must explicitly account for writes accepted by
|
|
176
|
+
PostgreSQL.
|
|
177
|
+
|
|
178
|
+
## 8. Dependencies and Security
|
|
179
|
+
|
|
180
|
+
Runtime dependencies are `sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, and `greenlet>=3.0`.
|
|
181
|
+
PostgreSQL 16+ must provide `pg_trgm`; extension installation is performed by an administrator,
|
|
182
|
+
not the application runtime role.
|
|
183
|
+
|
|
184
|
+
Device-key hashes, OTPs, and session/auth behavior are preserved. TLS is required for a remote
|
|
185
|
+
PostgreSQL endpoint; the approved host-local connection uses the configured host-gateway and
|
|
186
|
+
host firewall restrictions. Backup ownership, retention, and restore testing are operational
|
|
187
|
+
requirements before production cutover.
|
|
188
|
+
|
|
189
|
+
## 9. Explicit Non-Goals
|
|
190
|
+
|
|
191
|
+
- Redis caching, vector search, recommender features, and cross-service live database joins.
|
|
192
|
+
- A dual-write migration or automatic rollback to a stale SQLite database.
|
|
193
|
+
- Raw single-row speed as the primary success metric.
|
|
194
|
+
|
|
195
|
+
## 10. Release Acceptance Criteria
|
|
196
|
+
|
|
197
|
+
- One canonical `webqueue` database contains the five approved schemas.
|
|
198
|
+
- SQLite configuration prevents accidental empty-database startup during transition.
|
|
199
|
+
- All migrations, ETL verification, and application fixture checks pass in rehearsal.
|
|
200
|
+
- The runtime uses SQLAlchemy async sessions; migrations and pruning use least-privilege roles.
|
|
201
|
+
- The Quadlet app connects to host-managed PostgreSQL without relying on container-localhost.
|
|
202
|
+
- Search, queue/payment reconciliation, retention, observability, and forward-recovery procedures
|
|
203
|
+
are documented and tested before the production cutover.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPEC — Sortie 1: Database Config + SQLAlchemy 2.0 Async Engine
|
|
2
|
+
|
|
3
|
+
**Sprint**: `postgres-migration`
|
|
4
|
+
**PRD**: [PRD-postgres-migration.md](PRD-postgres-migration.md)
|
|
5
|
+
**Depends on**: none
|
|
6
|
+
**Estimated**: 2–4 h
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Overview
|
|
11
|
+
|
|
12
|
+
Add the Postgres backend selector, DSN resolution, and **SQLAlchemy 2.0 (`asyncpg`)** engine and session factory for the unified `webqueue` database on `chandra-1`, supporting both partitioned SQLite (from Part 1) and PostgreSQL backends.
|
|
13
|
+
|
|
14
|
+
## 2. Scope and Non-Goals
|
|
15
|
+
|
|
16
|
+
**In scope**: configuration models, DSN resolution, SQLAlchemy `AsyncEngine` and `async_sessionmaker` factory, example configuration updates, dependency additions (`sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, `greenlet>=3.0`).
|
|
17
|
+
**Non-goals**: no repository port (Sortie 2), no FTS change (Sortie 3), no TMDB schema port (Sortie 4), no ETL (Sortie 5).
|
|
18
|
+
|
|
19
|
+
## 3. Requirements
|
|
20
|
+
|
|
21
|
+
- `database.backend: sqlite | postgres` (default `sqlite` during development/transitional phase).
|
|
22
|
+
- `database.postgres`: `dsn_env`/password-free `dsn`/`host`/`port`/`user`/`dbname`/`password_env`/`pool_size`/`max_overflow`.
|
|
23
|
+
- Single database `dbname = webqueue` hosting all logical schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`).
|
|
24
|
+
- DSN precedence: `dsn_env` → password-free `dsn` plus `password_env` → assembled URL using `password_env`. No secrets, password field, or password-bearing DSN is permitted in `config.json`.
|
|
25
|
+
|
|
26
|
+
## 4. Design
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# kryten_webqueue/config.py
|
|
30
|
+
class PostgresConfig(BaseModel):
|
|
31
|
+
dsn_env: str | None = None
|
|
32
|
+
dsn: str | None = None
|
|
33
|
+
host: str = "host.containers.internal"
|
|
34
|
+
port: int = 5432
|
|
35
|
+
user: str = "kryten"
|
|
36
|
+
dbname: str = "webqueue"
|
|
37
|
+
password_env: str | None = "KRYTEN_WEBQUEUE_PG_PASSWORD"
|
|
38
|
+
pool_size: int = 10
|
|
39
|
+
max_overflow: int = 20
|
|
40
|
+
|
|
41
|
+
class DatabaseConfig(BaseModel):
|
|
42
|
+
backend: Literal["sqlite", "postgres"] = "sqlite"
|
|
43
|
+
sqlite_layout: Literal["monolith", "partitioned"] = "monolith"
|
|
44
|
+
data_dir: str = "./data" # for partitioned sqlite
|
|
45
|
+
postgres: PostgresConfig = PostgresConfig()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# kryten_webqueue/catalog/db/engine.py
|
|
50
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
51
|
+
|
|
52
|
+
def create_pg_engine(config: PostgresConfig):
|
|
53
|
+
url = config.get_async_url() # postgresql+asyncpg://...
|
|
54
|
+
engine = create_async_engine(
|
|
55
|
+
url,
|
|
56
|
+
pool_size=config.pool_size,
|
|
57
|
+
max_overflow=config.max_overflow,
|
|
58
|
+
pool_pre_ping=True,
|
|
59
|
+
)
|
|
60
|
+
session_factory = async_sessionmaker(
|
|
61
|
+
bind=engine,
|
|
62
|
+
class_=AsyncSession,
|
|
63
|
+
expire_on_commit=False,
|
|
64
|
+
)
|
|
65
|
+
return engine, session_factory
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The runtime container resolves `host.containers.internal` through an explicit rootful Quadlet
|
|
69
|
+
host-gateway mapping. This is not a container-localhost connection. Configuration validation
|
|
70
|
+
rejects password-bearing `dsn` values and ambiguous SQLite layouts before startup.
|
|
71
|
+
|
|
72
|
+
## 5. Implementation Plan
|
|
73
|
+
|
|
74
|
+
- **Modify** `config.py`: add `PostgresConfig`, `DatabaseConfig` with `postgresql+asyncpg` URL builder.
|
|
75
|
+
- **Create** `kryten_webqueue/catalog/db/engine.py`.
|
|
76
|
+
- **Add dependencies** in `pyproject.toml`: `sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, `greenlet>=3.0`.
|
|
77
|
+
- **Modify** `config.example.json`: document Postgres connection block with `KRYTEN_WEBQUEUE_PG_PASSWORD` environment placeholder.
|
|
78
|
+
|
|
79
|
+
## 6. Testing Strategy
|
|
80
|
+
|
|
81
|
+
- Unit tests for `PostgresConfig.get_async_url()` testing precedence order, environment-only password substitution, and rejection of password-bearing file DSNs.
|
|
82
|
+
- Configuration loading tests for default SQLite and PostgreSQL configurations.
|
|
83
|
+
|
|
84
|
+
## 7. Acceptance Criteria
|
|
85
|
+
|
|
86
|
+
- [ ] Backend selectable; supports `sqlite` and `postgres`.
|
|
87
|
+
- [ ] SQLAlchemy 2.0 `AsyncEngine` initialized with connection pooling.
|
|
88
|
+
- [ ] Environment variable secret resolution verified without leaks.
|
|
89
|
+
- [ ] SQLite layout validation prevents an existing monolith from being silently replaced by empty partitioned files.
|
|
90
|
+
- [ ] Dependencies installed and all checks green (`black`, `ruff`, `mypy`, `pytest`).
|