Files
goods/ingestion/opengoods/jobs/update_off.py
T
novaalphastrikeomegaz663 044c870df7
CI / Go (api) (pull_request) Failing after 22s
CI / Python (ingestion) (pull_request) Successful in 14s
CI / Migrations (postgres) (pull_request) Failing after 18s
feat(ingestion): harden OFF ingestion for bulk seeding
- Add retry/backoff (429 + 5xx, Retry-After aware) to the OFF adapter so
  transient API errors no longer abort a run.
- Clamp bounded text fields (serving_size, net_content_unit,
  country_of_origin) to their column widths in transform; long OFF values
  previously raised StringDataRightTruncation and rolled back the batch.
- Load each record inside a savepoint (load_record_safe) so one malformed
  source record is skipped instead of aborting the whole import; jobs now
  report an errored count.
- Tests for retry behaviour, serving_size clamping, and per-record isolation.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 06:55:56 +00:00

71 lines
2.5 KiB
Python

"""Incremental Open Food Facts update.
Fetches products modified since the persisted watermark, loads them, then
advances the watermark to the newest ``last_modified_t`` processed so the next
run only sees what changed.
Usage:
python -m opengoods.jobs.update_off --max-pages 5
python -m opengoods.jobs.update_off --since 1700000000 # override watermark
"""
from __future__ import annotations
import argparse
import sys
import psycopg
from opengoods.adapters.openfoodfacts import SOURCE_NAME, OpenFoodFactsAdapter
from opengoods.etl.load import default_dsn, ensure_source, load_record_safe
from opengoods.etl.state import get_watermark, set_watermark
from opengoods.etl.transform import transform
def run(args: argparse.Namespace) -> int:
adapter = OpenFoodFactsAdapter(min_interval=args.min_interval)
loaded = skipped = errored = 0
high_watermark = 0
with psycopg.connect(args.dsn, autocommit=False) as conn:
source_id = ensure_source(conn)
since = args.since if args.since is not None else get_watermark(conn, SOURCE_NAME)
high_watermark = since
for raw in adapter.fetch_modified_since(
since, page_size=args.page_size, max_pages=args.max_pages
):
high_watermark = max(high_watermark, int(raw.get("last_modified_t") or 0))
rec = transform(raw)
if rec is None:
skipped += 1
continue
if load_record_safe(conn, rec, source_id, raw):
loaded += 1
else:
errored += 1
set_watermark(
conn,
SOURCE_NAME,
high_watermark,
stats={"loaded": loaded, "skipped": skipped, "errored": errored, "since": since},
)
conn.commit()
print(
f"since={since} loaded={loaded} skipped={skipped} "
f"errored={errored} watermark={high_watermark}"
)
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Incremental OFF update")
parser.add_argument("--since", type=int, default=None, help="override watermark (unix ts)")
parser.add_argument("--page-size", type=int, default=100, help="search page size")
parser.add_argument("--max-pages", type=int, default=10, help="max pages to scan")
parser.add_argument("--min-interval", type=float, default=4.0, help="API throttle seconds")
parser.add_argument("--dsn", default=default_dsn(), help="PostgreSQL DSN")
return run(parser.parse_args(argv))
if __name__ == "__main__":
sys.exit(main())