Working demonstration · fresh run

Every product, every price, one verified store.

A production scraper for Mariano's that collects the full catalogue for a configured store, checks the store before and after the run, validates every record against the required 22-field schema and only then publishes. Built to run on Azure twice a week without anyone watching it.

Mariano's 
Unique valid records · this run
0
 
01 · Location accuracy

The right store, checked twice.

The store is resolved by its Kroger location ID and checked against the configured chain and ZIP before any product is collected and again after the last one. Every price below came from a request carrying that store's ID; a mismatch stops the run before publication.

✓ verified before run ✓ verified after run

      
02 · Category completeness

All categories from the CSV, broken down by subcategory.

Unique records per category from Marianos_Category_URLs.csv. Click a category to see its subcategories.

03 · Field completeness

22 fields, in the required order.

Share of records with a value in each field. Empty strings, never nulls. Fields the rules require to stay empty at the scraper stage are marked, and so are fields the source does not provide.

04 · The data

Browse the records.

A stratified sample from the run, exactly as written to JSON.

ProductWeightPriceUPCStore brand
One record, raw JSON
Download

Structured JSON + run report

The sample file uses all 22 fields in the required order. The report holds total and per-category counts, duplicates, field completeness, runtime, request counts and the validation result.

↓ sample.json ↓ report.json

05 · Validation & failure protection

Nothing is published until it passes.

Every run is validated as a whole before it can replace the last good dataset. A blocked, thin, partial or wrong-store run is kept for diagnosis and the previous good file stays where the pipeline reads it.

Silent-blocking detection

A blocked category is never an empty one.

  • Every response is checked for status, JSON type, expected keys, pagination and product IDs.
  • 401/403 stop the run; 429/5xx get paced retries with backoff and Retry-After.
  • Canary products, seeded from the first good run, are looked up before collection.
  • The share of products carrying a store price shows the store context held for the whole run.
  • Category and total yields are compared with the last good run; a fall over 40% / 20% blocks publication.
Interruption recovery · real log

Stopped mid-run, resumed, nothing fetched twice.

06 · Architecture on Azure

A scheduled job, not a server to babysit.

One Bicep template and one command install everything in your subscription. No secrets in code, no keys in config: a managed identity reads Key Vault, pulls the image and writes to Blob Storage.

ScheduleMon + Thu 07:00 UTCcron 0 7 * * 1,4 Container Apps Jobpython -m marianos_scraper run --alllock → verify → collect → validateparallelism 1 · retry 1 · 4 h timeout Kroger Public APILocations + Products, OAuth2 Key Vaultclient id + secret Blob Storagelatest/ runs/ work/ state/versioning + soft delete Log AnalyticsJSON logs per eventalert on run_held_back Your pipelinelatest/<store>/products.json Managed identityACR pull · KV readBlob write

Lock

Blob lease per store. A second start exits; runs never overlap.

Verify store

Location ID, chain and ZIP must match before collection.

Collect

Category → subcategory → term units, paced, retried, each one checkpointed.

Verify again

Same store check after the last unit.

Validate

Schema, duplicates, yields, canaries, price context, completion.

Publish

Only a passing run replaces latest/.

Technologies & paid services
Language
Python 3.12, requests
Data source
Kroger Public API (free, official)
Proxies
None
Paid APIs
None
Compute
Azure Container Apps Job
Storage
Azure Blob Storage
Secrets
Azure Key Vault
Infra as code
Bicep + deploy.ps1
Tests
pytest, offline
Estimated monthly cost (1 store, 2 runs/week)USD
Total
Source boundary. The official API does not expose the price-per-measure text, deal wording (volume or digital) or the Coupons page. These fields stay empty strings, as the scraper rules require when a value is not available, and nothing is calculated to fill them.