Skip to content

rielas/chaotic-shop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chaotic Shop

CI Python 3.13+ uv Ruff License: MIT

A small, deterministic mock storefront for stress-testing web scrapers and crawlers. Each request returns a real HTML page rendered with FastHTML, but the layout, element ids, copy, and form fields drift — predictably — based on two knobs you turn at startup. Reproduce the same drift twice and your scraper has a bug; let the drift escalate and you'll find where it breaks.


Why?

Realistic scrapers fail in messy, non-obvious ways:

  • A category page silently renames a class name.
  • A product detail injects an extra form field on Tuesdays.
  • The "Buy now" button becomes "Checkout now! (only 12 items left!)".
  • Two adjacent DOM sections swap places.

Chaotic Shop reproduces those failure modes on demand. The catalog is generated from a small word list × a product id, so the same parameters always produce the same site — making bugs reproducible and reviews boring.

Features

  • Deterministic, parameterised chaos — same env vars, byte-identical site.
  • Two orthogonal chaos dimensions: category-level (shared by all products in a category) and product-level (unique per product).
  • A typed, dependency-free domain model (Catalog, Product, Skeleton) that can be exercised from a notebook without spinning up a server.
  • Server-rendered with FastHTML + Pico CSS — no JS build pipeline.
  • Fully type-checked with ty and linted/formatted with ruff.
  • Containerised, with a multi-stage uv-powered Dockerfile.

Architecture

src/
├── chaotic_shop/         # web layer (FastHTML)
│   ├── __init__.py       # app + entrypoint (uvicorn-importable as chaotic_shop:app)
│   ├── config.py         # Settings dataclass, env-loaded, frozen
│   ├── components.py     # header / breadcrumbs
│   ├── routes.py         # HTTP handlers (registered on import)
│   └── product_page.py   # product detail renderer
└── core/                 # domain — no web dependencies
    ├── __init__.py       # Catalog + Product (TypedDict)
    ├── names.py          # static word lists
    └── skeleton.py       # chaos mutation engine

The boundary between core/ and chaotic_shop/ is strict — core/ doesn't import from FastHTML, Starlette, or anything web-related, so the domain model is trivially unit-testable.

Chaos engine

core.skeleton defines four orthogonal mutation kinds, dispatched through match statements:

Mutation Effect
ReorderSections Swap two adjacent sections of the product page.
AddSectionId Inject a stable DOM id on one section, derived from the category.
ChangeText Rewrite a call-to-action label (Checkout / Submit Review).
FlipFlag Toggle an optional form field (last name, email).

choose_mutation() rolls a weighted lottery seeded from (category_index, product_id, step) — so a single product always lands on the same mutation sequence, but increasing chaos exposes more variants.

Quickstart

Requires Python 3.13+ and uv.

uv sync
uv run chaotic_shop

Open http://localhost:5001. Increase chaos:

CATEGORY_CHAOS=5 PRODUCT_CHAOS=2 uv run chaotic_shop

Configuration

All settings come from environment variables, parsed by Settings.from_env():

Variable Default Effect
CATEGORY_CHAOS 0 Mutations shared by every product in a category. Increases inter-category drift.
PRODUCT_CHAOS 0 Mutations unique per product. Increases intra-category drift.
NUMBER_OF_PRODUCTS 1000 Size of the catalog.
REVIEW_DELAY_SECONDS 4.0 Latency injected into POST /submit_review (for testing scraper timeouts).
CHECKOUT_DELAY_SECONDS 4.0 Latency injected into POST /checkout.
LOG_LEVEL INFO Standard library logging level.

Patterns:

# No drift at all — useful as a control.
CATEGORY_CHAOS=0 PRODUCT_CHAOS=0 uv run chaotic_shop

# Cross-category drift only — every product in "Compact" looks consistent,
# but "Compact" and "Advanced" differ.
CATEGORY_CHAOS=4 PRODUCT_CHAOS=0 uv run chaotic_shop

# Heavy intra-category drift — every product looks different even within
# the same category.
CATEGORY_CHAOS=2 PRODUCT_CHAOS=8 uv run chaotic_shop

Docker

docker build -t chaotic-shop .
docker run --rm -p 8000:5001 \
  -e CATEGORY_CHAOS=5 -e PRODUCT_CHAOS=2 -e NUMBER_OF_PRODUCTS=1000 \
  chaotic-shop

Server at http://localhost:8000.

Development

uv sync                  # install runtime + dev dependencies
uv run pytest            # 52 tests, ~8 seconds
uv run ruff check        # lint
uv run ruff format       # auto-format
uv run ty check src tests  # type-check

Design notes

A few choices worth calling out:

  • No global RNG. Every random draw uses a fresh random.Random(seed) instance so the catalog never mutates process-global state — which means tests can run in parallel and imports stay side-effect-free.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors