rung is a cost-ranked web-scraping framework: extract authoritative rosters, scrape each entity's own
site for its locations, diff those against the roster, and snapshot each location's live catalog.
Domain-specific catalogs plug in via the rung.plugins entry point; this repo is the generic core.
Start with ARCHITECTURE.md for the module map and dependency edges.
- Stage 1 — rosters:
search-states→find-lists→scrape-states(generic HTML/PDF/CSV/ArcGIS/Socrata extractors; opt-in--renderbrowser and--aiLLM fallbacks).seed-companiesderives entities;recondetects each one's platform + homepage. - Stage 2 — entity sites:
scrape-company-storesroutes each entity through the access engine (access.py) →company_stores; thendedupe-stores→compare-stores. - Stage 3 — listings:
scrape-menussnapshots each handled location's catalog intostore_products, routed to a per-platform rung supplied by the plugin overlay.--max-age-hours Nfreshness-gates re-scrapes. - Fleet:
workeris the distributed entrypoint (one process per egress IP).
- Two packages (public core + private plugin). The public core ships Stage-1 extraction, the
access engine, the queue, persistence, normalization, the CLI, and the plugin seam, and runs on its
own — plug-in stages resolve to registry stubs until an overlay registers via
rung.plugins. The core imports nothing from any overlay; the boundary is test-enforced (tests/test_import_layering.py). Seedocs/publish_split_design.md. - Access-method engine (
access.py): every target is reachable several ways at different cost;run_targetruns the cheapest that works, persists the winner per target, and re-walks only on failure / a cheaper untried rung / a governed staleness re-explore. - Work queue (
queue.py,jobstable): stages enqueue then claim their own work (FOR UPDATE SKIP LOCKED), so concurrent runs partition targets.
- Python ≥ 3.13, deps via
uv:uv run <cmd>,uv run pytest,uv tool run ruff. - Postgres (psycopg3 raw SQL, no ORM).
DATABASE_URLenv; default = a local Postgres dev container. Runruff check→ty→pytest(coverage floor) before every commit; the same gate runs in CI (.github/workflows/ci.yml).
- Coding standards: modern type syntax (
X | None,list[…]); LBYL overtry/exceptexcept at the CLI boundary and untrusted-external-data boundaries;pathlib.Pathwith explicitencoding="utf-8". CLI commands useprint()for user-facing output. - All HTTP goes through
http.make_session()(an AST guard intests/test_http.pyenforces it — never construct a bare session). - Normalized fields: each
store_productsrow keeps its raw platform shape and standardized fields stamped at scrape time (canonical category/type, lineage facet, per-variant size→grams and price-per-unit, folded attribute maps); theproducts_normalizedVIEW is the cross-platform surface. - Keep
ARCHITECTURE.md/README.md/docs/in sync with code changes.