Skip to content

mahdincc/Spider

Repository files navigation

VWare — cross-supplier mobile price comparison (Iran)

Fast price discovery for the Iranian mobile market. Pick a phone, choose colour + warranty, and VWare crawls multiple Iranian retailers live to show the real, in-stock prices side by side — Persian, RTL, mobile-first.

CI python license

Lineage. This is the FastAPI 0→1 MVP that validated the price-aggregation pipeline before it grew into the full multi-tenant .NET VWare platform (separate repo). Read it as the "make it work" prototype: a deliberately small stack doing one job well, and heavily tested. 0 → 1 → scale.


The problem

Iranian shoppers compare phone prices across a dozen marketplaces — Snappshop, Digikala, HamrahTel, Farnaa and more — each with its own catalogue, stock levels and per-vendor pricing. There is no clean, fast, mobile-first way to answer "this exact model + colour + warranty, cheapest in-stock, right now." VWare does that.

What it does

  • Type-ahead search over a ~2,900-model catalogue — tokenised, order-independent, Persian and English.
  • Guided wizard — budget and use-case in, ranked recommendations out (scored on chipset gaming tier, camera, battery, brightness, …).
  • Paste-a-link — drop a product URL from another store and it resolves to the canonical model in the catalogue.
  • Live price crawl across suppliers on demand, with a 30-minute price cache.
  • Cross-supplier matching — one canonical phone anchored to many retailers' listings (brand + model + storage + RAM + colour, with a fuzzy fallback).

Screenshots

Persian, RTL, mobile-first; the UI is HTMX-rendered partials — no SPA, no build step. Two shots are worth a look: the mobile search + price table, and the auto-generated OpenAPI page at /docs. Capture recipe (2 min) in docs/screenshots/.

Suppliers

Live price crawler Search-link matched Spec source
Snappshop · Digikala · HamrahTel · Farnaa + 10 more (Kalamik, GooshiOnline, Mobile140, Zitro, HamrahSeeb, Lipak, …) GSMArena

14 supplier profiles in total: 4 with live-crawl price integration, the rest matched via search-link discovery. See docs/SCRAPING.md for how data is sourced and the rate-limiting / Terms-of-Service stance.

Architecture

The catalogue is a durable cache built once; only prices are fetched live, and even those are cached for 30 minutes.

data/dataset.xlsx
      │  scripts/probe_mobile.py     → live Snappshop API; keep in-stock models
      ▼
data/picked_models.json + api_samples/*.json
      │  app/importer.py             (+ gsmarena_catalog_*.json, digikala catalog)
      ▼
data/vware_mvp.sqlite   ── categories · brands · products · variants · guarantees
      │                     · supplier_product_links  (cross-supplier match)
      │  app/link_matcher/*          anchors 1 canonical product → many suppliers
      ▼
FastAPI + Jinja2 + HTMX  ── user picks product + variant
                              │
                              ▼   POST /api/prices
                          LIVE crawl per supplier  ──►  30-min PriceCache (SQLite)
                                                          │
                                                          ▼  price table rendered

Design calls worth noting (see PLAN.md / PLAN_UX.md):

  • Probe-then-crawl. Everything derivable is imported once into SQLite; the request path only fetches the price of the exact item asked for — never bulk-crawls.
  • 30-minute cache trades a little price staleness for a big drop in supplier load and sub-second repeat lookups.
  • HTMX over an SPA. Server-rendered partials keep the stack tiny and the mobile payload small — no client framework, no build step.

Tests

Front and centre, because correctness here is verifiable:

pytest -q
  • 351 tests pass against a seeded catalogue DB (27 files: Persian text normalisation, variant/colour extraction, spec parsing, the cross-supplier link matcher, the wizard ranker, and every HTTP endpoint with the live crawler mocked).
  • 298 of them run on every push in CI from a clean checkout (no seeded data required) — that's what the green badge above reflects. The rest are catalogue- dependent integration tests that need a seeded DB and are run locally (they skip cleanly in CI — see tests/conftest.py).

Run it

Docker (one command)

docker compose up            # app behind Caddy on http://localhost

Brings up two containers: the Uvicorn app and a Caddy reverse proxy (auto-HTTPS when you point a domain at it — see Caddyfile).

The product catalogue lives in a gitignored SQLite DB, so a fresh checkout starts empty. Seed it with python -m scripts.probe_mobile && python -m app.importer (needs access to the Iranian supplier APIs). To verify the engine without live access, run the test suite above — it needs no network.

Local (dev)

python -m venv .venv && . .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload                     # → http://127.0.0.1:8000

Interactive OpenAPI docs at /docs, health check at /health.

Production

Runs as docker compose (app + Caddy) on a low-spec Iran VPS. Because the target network is isolated from GitHub/PyPI, scripts/deploy.sh rsyncs code, runs idempotent migrations, re-imports supplier profiles, restarts the service, and smoke-tests the key routes — all driven by $VWARE_SSH_* env vars (see .env.example).

Tech stack

  • Python 3.12, FastAPI, Uvicorn
  • Jinja2 + HTMX — server-rendered, no JS framework, no build step
  • SQLite + SQLAlchemy 2 (async via aiosqlite)
  • httpx async client for the supplier crawlers
  • pytest + pytest-asyncio
  • Docker + Caddy for deployment

Project structure

app/
  main.py            FastAPI routes (search, wizard, prices, ingest)
  config.py db.py    settings + SQLAlchemy models
  crawler/           one file per supplier: snappshop, digikala, hamrahtel,
                     farnaa, gsmarena (+ base.py retry/backoff)
  link_matcher/      cross-supplier anchor matching, scoring, colour match
  services/          wizard ranking + canonical search
  text_utils.py      Persian normalisation + variant extraction
  templates/ static/ Jinja partials + Idealo-style design system
data/                JSON/xlsx seed corpus (catalog, specs, personas)
scripts/             probe, importers, idempotent migrations, deploy.sh
tests/               pytest suite (27 files)
docs/                DESIGN_SYSTEM.md, SCRAPING.md

How this was built

An agentic, spec-driven loop — not vibe coding:

  1. Spec first. PLAN.md (scope) and PLAN_UX.md (UX + pricing model) are the curated specs the build was driven from; a Persian UX walk-through (docs/user_journey1.pdf) pinned the flow before any code.
  2. Vertical slices. Development ran as ~26 phase branches (phase-aphase-z and beyond), each a thin end-to-end slice: a test, an implementation, repeat.
  3. Verify, then trust. Every supplier parser is tested offline against captured fixtures; endpoints are tested with the live crawler mocked. The AI wrote code fast; the human owned the calls that matter — probe-then-crawl over live-scrape, the 30-minute cache trade-off, HTMX over an SPA, and which suppliers earned a full crawler vs. a search-link.

Status & roadmap

Status: working demo. Search, wizard, cross-supplier matching, live pricing and error-reporting all function; 351 tests green.

Known limits / next: live prices require access to the Iranian supplier APIs; catalogue seeding is a manual step; only 4 of 14 suppliers have full live crawlers. The natural next steps — user accounts, wallet/billing, watchlists, more crawlers — are where the project graduated into the full .NET VWare platform.

License

MIT © Mahdi Aghakhani.

About

Cross-supplier mobile price comparison for the Iranian market — FastAPI + HTMX, live crawler pipeline, Dockerized. The 0→1 MVP that preceded the .NET VWare platform.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors