pyproject.toml uses range pins (>=) for all dependencies; without a
lockfile, a fresh `pip install .` on a different day could pull
materially different versions of fastapi, sqlalchemy, httpx, etc.
For a production-shaped service that's a reproducibility risk —
especially since we don't run a CI pipeline that would catch
"works on yesterday's container, fails on today's."
requirements.lock pins every transitive dep (60 packages) to the
exact versions running in the test container today. Dockerfile is
updated so both stages install from the lockfile first, then install
the project itself with --no-deps:
pip install -r requirements.lock
pip install --no-deps .
That way pyproject.toml's range pins document our compatible
upper-and-lower bounds, but the lockfile is what actually gets
installed on every build.
To bump deps later: bump pyproject.toml ranges, rebuild a fresh
venv, `pip freeze` it, save back to requirements.lock.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five fixes uncovered by actually running the suite in docker-compose.test.yml:
1. (real prod bug) PATCH /api/settings/digest mutated principal.user which
require_token had loaded in a now-closed session — the commit on the
handler's session persisted nothing. Re-fetch the user via the active
session before writing.
2. Portable PK type. SQLite only auto-fills `INTEGER PRIMARY KEY`; plain
BIGINT requires explicit values. Define a `_PK` alias of
`BigInteger().with_variant(Integer(), "sqlite")` and use it for all 10
autoincrement primary keys in app/models.py. No prod-schema change
(MariaDB still gets BIGINT).
3. job_lifecycle's MariaDB GET_LOCK / RELEASE_LOCK is now gated behind
`dialect.name == "mysql"`, so the test SQLite engine doesn't trip on
the missing function. Single-process test runs can't race themselves.
4. tests/test_news_window.py seeded Headline rows without `fingerprint`,
which is NOT NULL — added an `fp-{title}` value per row.
5. tests/test_email_digest_job.py now also patches `llm_configured` to
True so the job doesn't short-circuit on the missing API key.
6. (test container hygiene) Drop `COPY tests ./tests` from the test stage
in the Dockerfile — .dockerignore excludes `tests/` (correct: prod
image must not bake tests), and docker-compose.test.yml bind-mounts
./tests at run time anyway.
Suite now: 198 passed, 5 skipped, 1 pre-existing failure
(test_default_groups_present — Phase G dropped the "pie" group from
config/default.toml but the assertion wasn't updated; unrelated to this
branch).
Adds a `test` stage to the Dockerfile (prod deps + pytest + aiosqlite via
the `dev` extras, never shipped) and a docker-compose.test.yml that runs
it under its own Compose project name (`cassandra-test`). The project-name
isolation matters because this host runs prod — a wrong `compose up` would
otherwise recreate the live `app` container; namespaced project means the
test container can't touch any prod container/network/volume.
Tests use an in-memory aiosqlite DB (per tests/conftest.py) so the
container has no MariaDB / Redis dependency and nothing on the prod DB
is observed or mutated.
Also adds aiosqlite to dev extras — tests have always implicitly needed
it (the conftest pins DATABASE_URL to sqlite+aiosqlite:///:memory:); the
declaration was just missing.
Usage:
docker compose -f docker-compose.test.yml run --rm test
docker compose -f docker-compose.test.yml run --rm test pytest -k unsubscribe
Containerised macro-strategy dashboard: 4-panel web UI (indicators,
portfolio, flash news, AI strategic log), MariaDB store, hourly
ingestion jobs, OpenRouter-backed AI analysis.
Ports the four prototype scripts in the parent dir (market_pulse,
flash_news, trading212, strategic_log) into async services backed by a
persistent DB and served via FastAPI + Jinja2 + HTMX. APScheduler runs
as a separate compose service for crash-safety and easier restarts.
Portfolio composition + position names come live from Trading 212;
news per-ticker headlines reuse those names. Tone (NOVICE/INTERMEDIATE/
PRO) and analysis style (DRY/SPECULATIVE) are env-configurable and
stored on each log row so historical entries show what produced them.
Default model is deepseek/deepseek-v4-flash (overridable via env).
Light/dark theme toggle, sans-serif for prose surfaces, monospace for
data. Bearer-token auth, OpenRouter monthly cost cap, RSS feeds auto-
disabled on consecutive failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>