Skip to content

feat(ingest): persist every source through the repository layer (#36) - #47

Merged
selvarajuv merged 6 commits into
mainfrom
persist-pg
Oct 13, 2026
Merged

selvarajuv merged 6 commits into
mainfrom
persist-pg

Conversation

@Alexcchip

@Alexcchip Alexcchip commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Every ingestion source now writes to Postgres through the repository layer. Weather and flights gained normalizers and a runner; the hotel parser stopped writing its own SQL.

Refs #36, not Closes: flight_arrivals is wired and tested but needs OpenSky and RapidAPI credentials to populate. The other two criteria are met.

Type of change

  • feat — new feature
  • fix — bug fix
  • docs — documentation only
  • refactor — code change that neither fixes a bug nor adds a feature
  • perf — performance improvement
  • test — adding or updating tests
  • chore / build / ci — tooling, deps, or pipeline changes

Changes

  • repository/ gains weather, flights and hotel repositories. upsert_rows is the shared primitive, repositories never commit, the caller owns the transaction.
  • upsert_rows collapses duplicate keys within a batch, and stamps captured_at only where the column exists. Both were crashes.
  • Weather moved from backend/src/data to foresight/ingest. Behaviour fix: Open-Meteo's Time() is local midnight expressed in UTC, so every reading landed a day early east of Greenwich. weather_date joins stay_date, so this mis-attributed every weather feature by a night.
  • New ingest/flights.py, per-flight rows to daily counts, and ingest/external.py, a runner taking --source weather|opensky|aerodatabox.
  • The hotel parser no longer calls create_engine or to_sql, which was the last adapter writing SQL directly. A separate persist() writes through the repository.
  • upsert_hotel and upsert_room_type let a reload correct base_rate and coordinates. Coordinates coalesce, so a loader that omits them does not erase them.
  • docs/external-signals.md covers the contract, both CLIs and the known gaps.

Database migrations

  • No schema changes
  • Includes an Alembic migration (just migrate-create "...")
  • Migration runs cleanly (just migrate) and rolls back (just migrate-down)

b9719be9df40 adds natural keys for the external tables, c97a8a991e88 for the hotel tables. Verified up, down, up, and from scratch on an empty database.

Checklist

  • Commits follow Conventional Commits
  • Lint passes (just lint) and code is formatted (just format)
  • Pre-commit hooks run clean (just hooks installed) — pre-commit run --all-files passes ruff and ruff-format; every commit checked against the pinned conventional-pre-commit==4.1.0
  • Updated docs / README / .env.example where relevant — new docs/external-signals.md, plus ingest/ added to the README structure. No new env var
  • Tested locally (just dev) and verified the API at http://localhost:8000/docs — booted via uvicorn rather than compose, since Postgres is local here. /health 200, /docs 200, no startup errors. No route changed, but the parser gained an import edge into repository/ worth proving

How to test

  1. just migrate
  2. cd backend && uv run python -m foresight.ingest.external --source weather --city Dublin --lat 53.3498 --lon -6.2603. Expect 16 new, then 16 unchanged on a re-run.
  3. uv run --group ml pytest -q. Expect 195 passing. Bare just test fails on a missing statsmodels, which is pre-existing: it lives in the ml group and CI installs it.

Notes for reviewers

  • Overlaps feat: api to serve hotel signal data #45, which also adds repository/hotel.py with create_hotel and create_room_type. Different functions so the merge is additive, but whoever lands second resolves the file. Those versions commit inside the repository while this path commits in the caller, which is worth settling as a team.
  • daily_performance.rate is null on every row this writes. It is the training target in compare_models.py, and the export's unmapped ARR column could fill it, but that is a modelling decision rather than a persistence one.

@Alexcchip Alexcchip linked an issue Oct 13, 2026 that may be closed by this pull request
2 of 3 tasks
Alexcchip and others added 6 commits October 13, 2026 13:41
Named unique constraints on weather, flight_arrivals, competitor_rates,
hotels, room_types and daily_performance so re-running an ingest updates
rows instead of duplicating them.

Co-authored-by: Vichu Selvaraju <vichuselvaraju@gmail.com>
get_session commits once per request on success and rolls back on error.
Routes depend on it with scope="function" so the commit runs before the
response is sent. The widget repository flushes instead of committing.
…hotels

upsert_rows is the shared primitive: it collapses duplicate keys in a
batch, touches a row only when a value changed, and returns new/updated/
unchanged counts. The events upsert moves from ingest/ into repository/.

Co-authored-by: heatherlarange <heatherlarange01@gmail.com>
Co-authored-by: Vichu Selvaraju <vichuselvaraju@gmail.com>
Open-Meteo weather moves into ingest/ and fixes dates landing a day early
east of Greenwich. Per-flight rows collapse into daily counts. A runner
fetches one source per run. The Open-Meteo client is created on first use,
and climate projections live in scripts/.

Co-authored-by: Vichu Selvaraju <vichuselvaraju@gmail.com>
The parser no longer writes SQL. rate is room revenue / rooms sold (the
client's ARR) until listed-rate history arrives; unsold nights have no
rate. The CLI that opens a session lives in scripts/load_hotel_data.py.

Co-authored-by: Vichu Selvaraju <vichuselvaraju@gmail.com>
ingest/ is part of the services layer; only scripts/ open sessions.

Co-authored-by: Vichu Selvaraju <vichuselvaraju@gmail.com>
@selvarajuv
selvarajuv merged commit 846e2f7 into main Oct 13, 2026
4 checks passed
@Jrodrigo06
Jrodrigo06 deleted the persist-pg branch October 15, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persist Ingested Data Into Postgres

2 participants