Files
goods/ingestion/opengoods/jobs/import_bypos.py
T
rosemariejebbjtxbfp a75318b811
CI / Go (api) (pull_request) Successful in 54s
CI / Python (ingestion) (pull_request) Successful in 19s
CI / Migrations (postgres) (pull_request) Successful in 27s
feat(api): Redis read cache for product details and search
Stage-0 caching from docs/scalability.md. The Go API now caches hot
product-detail and search-result reads in Redis with a fail-open,
epoch-versioned scheme; Python ingestion bumps the epoch after a write
run to invalidate the cache globally in O(1).

- api/internal/cache: fail-open Cache (GetJSON/SetJSON) namespaced by an
  epoch counter (og:cache:epoch); disabled when Redis is unconfigured.
- store: ProductByID/ProductByGTIN (24h TTL) and SearchProducts (1h TTL)
  read-through the cache via WithCache.
- ingestion: bump_cache_epoch() called after update_off/seed_off/
  import_bypos/dedup commits when rows changed; best-effort, never fails
  a run. Adds redis dependency.

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

81 lines
2.6 KiB
Python

"""Import products collected by the bypos-collector tool into OpenGoods.
The ``tools/bypos-collector`` program writes one product per line (JSONL). This
job reads such a file, transforms each ``hit`` record into the internal product
shape, and upserts it into the database under the ``bypos中心库`` source with
field-level provenance.
Usage::
python -m opengoods.jobs.import_bypos --input products.jsonl
python -m opengoods.jobs.import_bypos --input products.jsonl --limit 500
Re-running is safe: products are upserted by GTIN and the source's MSRP/
provenance rows are refreshed rather than duplicated.
"""
from __future__ import annotations
import argparse
import gzip
import json
import sys
from collections.abc import Iterator
import psycopg
from opengoods.adapters.bypos import transform_bypos
from opengoods.cache import bump_cache_epoch
from opengoods.etl.load import default_dsn, ensure_bypos_source, load_bypos_record_safe
def _read_jsonl(path: str) -> Iterator[dict]:
opener = gzip.open if path.endswith(".gz") else open
with opener(path, "rt", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
yield json.loads(line)
except json.JSONDecodeError:
continue
def run(args: argparse.Namespace) -> int:
loaded = skipped = errored = 0
with psycopg.connect(args.dsn, autocommit=False) as conn:
source_id = ensure_bypos_source(conn)
yielded = 0
for raw in _read_jsonl(args.input):
if args.limit and yielded >= args.limit:
break
rec = transform_bypos(raw)
if rec is None:
skipped += 1
continue
yielded += 1
if load_bypos_record_safe(conn, rec, source_id, raw):
loaded += 1
else:
errored += 1
conn.commit()
if loaded:
bump_cache_epoch()
print(f"loaded={loaded} skipped={skipped} errored={errored}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Import bypos-collector JSONL into OpenGoods")
parser.add_argument(
"--input", required=True, help="path to a bypos-collector JSONL (.jsonl or .jsonl.gz)"
)
parser.add_argument("--limit", type=int, default=0, help="max hit records to load (0 = all)")
parser.add_argument("--dsn", default=default_dsn(), help="PostgreSQL DSN")
return run(parser.parse_args(argv))
if __name__ == "__main__":
sys.exit(main())