Shopify Image Audit
A Lighthouse-based image audit tool for Shopify stores. Produces per-image scores, role assignments, optimisation recommendations, and a before/after comparison workflow that proves image-optimisation ROI to paying customers.
Designed for the 99–199 € audit-on-demand business model: run the audit, deliver a customer-ready HTML report, optionally compare live metrics after the customer implements the recommendations.
Quickstart
End-user install (PyPI)
# Recommended: pipx creates an isolated env
pipx install shopify-image-audit
# Alternative: pip into a venv
python -m venv .venv && source .venv/bin/activate
pip install shopify-image-audit
The PDF renderer (audit report --pdf) requires native libraries
(libpango, libcairo, libgdk-pixbuf). On Linux:
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0
The lighthouse Node CLI is required for audit run <url>. Install with
npm i -g lighthouse (or pass --lhr <file> to use a pre-existing report).
The binary can be overridden via --lighthouse-bin PATH or $LIGHTHOUSE_BIN;
see docs/integrations/LIGHTHOUSE.md for
the full install guide and CI recipes.
Developer install (from source)
git clone https://github.com/xopsio/shopify-image-audit.git
cd shopify-image-audit
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
# Verify the install
audit version
pytest -q # 830 tests
Shopify stores (OAuth, preferred)
Log in once per store — the token is stored encrypted in tokens.json
(system keyring), so no manual --access-token is needed afterwards:
audit shopify login mystore.myshopify.com
audit shopify inventory mystore.myshopify.com -o inventory.json
# Many stores in one go (v0.16.2+): log in via the same stores.json
# that batch consumes, then audit them all
audit shopify login --stores-file stores.json
audit shopify batch --stores-file stores.json -o batch.json
CLI commands
The tool ships with a single Typer app. Run audit --help for the full list;
below are the high-value entry points.
audit run <url> — full Lighthouse + audit pipeline
audit run https://kauppa.myshopify.com --device mobile --runs 3 \
--out-dir artifacts
# -> artifacts/lhr_run1.json, audit_result.json (schema-compliant)
audit baseline <lhr.json> --save baseline.json — capture baseline
audit baseline fixtures/before_after/before_lcp.json \
--save baseline.json \
--url https://demo.myshopify.com
# -> Baseline saved to baseline.json
audit compare <baseline> <current> — before/after (file or URL)
# File vs file (offline)
audit compare baseline.json fixtures/before_after/after_lcp.json \
-o comparison.html --json comparison.json
# File vs live URL (fetches via PageSpeed Insights API)
audit compare baseline.json https://demo.myshopify.com \
--strategy mobile --api-key YOUR_KEY \
-o comparison.html
# HTML report includes a per-image delta table (bytes, score, status per image).
# PDF export via --pdf flag.
Exit codes: 0 success, 2 invalid args, 10 backend failure.
audit report <audit_result.json> — HTML or PDF report
audit report baseline.json -o report.html # HTML
audit report baseline.json -o report.pdf --pdf # PDF (WeasyPrint)
audit measure <url> — live PageSpeed metrics only
audit measure https://demo.myshopify.com --strategy mobile
# -> JSON metrics to stdout (or --output metrics.json)
audit shopify <login|auth|inventory|batch> — Shopify Admin API
# Preferred (v0.15+): OAuth login, token persisted encrypted in
# tokens.json — no manual tokens needed
audit shopify login mystore.myshopify.com
audit shopify auth mystore.myshopify.com # verify the stored token
audit shopify inventory mystore.myshopify.com -o inventory.json
# One-shot login for many stores, then batch-audit them (v0.16.2+)
audit shopify login --stores-file stores.json
audit shopify batch --stores-file stores.json -o batch.json
Read-only scopes required (read_products, read_themes, read_shop).
Manual tokens still work via --access-token / $SHOPIFY_ACCESS_TOKEN.
See docs/integrations/SHOPIFY_ADMIN.md for token acquisition and
docs/integrations/SHOPIFY_OAUTH.md for the OAuth flow.
audit score <audit_input.json> --ranker {heuristic|ml}
audit score extracted.json # default: heuristic
audit score extracted.json --ranker ml # weighted feature ensemble
Full reference: docs/spec/cli_v0_1.md.
Scoring algorithms
The pipeline assigns each image a role, a score (0–100), and a
recommendation. Two rankers ship, switchable via --ranker ml:
| Ranker | Formula | Use case |
|---|---|---|
heuristic (default) |
bytes per displayed pixel (bpp) + LCP penalty | fast, predictable baseline |
ml |
weighted ensemble: f_size, f_density, f_format, f_dim_match + LCP strictness | richer signal, more honest scoring |
Both produce the same output contract (role + score + recommendation). The ML
ranker is a hand-coded feature ensemble, not a statistical model — see
src/audit/ranker_ml.py for the design rationale
(no model deps, deterministic, fully explainable via ml_features()).
Configuration
Repeated options can be set once in
~/.config/shopify-image-audit/config.toml (or
$XDG_CONFIG_HOME/shopify-image-audit/config.toml):
[defaults]
device = "mobile" # run / baseline / schedule add
strategy = "mobile" # measure / compare
parallel = 4 # shopify batch / schedule run-all (0 = unlimited)
[pagespeed]
api_key = "AIza..." # or --api-key / $PAGESPEED_API_KEY
[report]
brand_color = "#ff6b35" # report / compare branding
Precedence: CLI flag > env var > config > default. A broken config warns and falls back to defaults — it never blocks a run. Full reference (13 keys / 5 sections) in CONTRIBUTING §5b.
Architecture
src/
├── audit/ # scoring + reporting
│ ├── models.py # Pydantic v2 schemas (AuditResult, ComparisonResult, ImageDelta)
│ ├── parser.py # Lighthouse / fixture JSON parser
│ ├── ranker_heuristic.py # default ranker (bpp-based)
│ ├── ranker_ml.py # opt-in ML-style ranker (weighted ensemble)
│ └── report.py # HTML/PDF report renderer (split into _render_* funcs)
├── core/ # core algorithms
│ ├── image_extractor.py # LHR image audit extraction
│ ├── image_signals.py # shared displayed_area, assign_role, _safe_int
│ └── baseline_manager.py # save/load baselines + compare() + per-image matching
├── engine/ # orchestration + CLI
│ ├── cli.py # Typer app (run, measure, baseline, compare, shopify, ...)
│ ├── cli_helpers/ # extracted CLI helpers (validators, dispatchers, table, errors)
│ └── audit_orchestrator.py # run_audit() pipeline
└── integrations/ # external APIs
├── pagespeed_api.py # PageSpeed Insights (measure + fetch_lighthouse_json)
└── shopify_admin.py # Shopify Admin API (auth, products, theme_assets)
src/audit/schemas/audit_result.schema.json # JSON Schema contract (validated by tests)
tests/ # 830 tests, single-writer (ZCode)
docs/examples/ # live demo report + comparison JSON
docs/integrations/ # Shopify Admin API token guide
The codebase is governed by a single ZCode agent (see
docs/governance.md v1.3).
Testing
pytest -q # 830 tests, single-writer discipline
pytest --cov=src --cov-report=term # ~91% coverage
ruff check src/ tests/ # 0 violations
The CI workflow runs pytest -q + ruff check on Python 3.11 and 3.12 for
every PR. Branch protection on main requires both checks to pass before
merge. See .github/workflows/ci.yml.
Customer deliverables (Phase 1)
- Customer report template —
docs/CUSTOMER_REPORT_TEMPLATE.md - Onboarding workflow —
docs/CUSTOMER_ONBOARDING.md - Example audit report (Nordic Lifestyle demo store, LCP 4200ms → 1800ms) —
docs/examples/demo_audit_report.html - Example comparison data —
docs/examples/demo_comparison.json
Roadmap
- ✅ Sprint 1 — v0.1.0 baseline (parser, ranker, orchestrator, CLI, HTML report, 103 tests)
- ✅ Sprint 2 — before/after workflow, customer docs, ML ranker, live URL compare, CI, governance cleanup (276 tests)
- ✅ Sprint 3 — PDF export, per-image deltas, Shopify Admin API, v0.2.0 release prep (390 tests)
- ✅ Sprint 4 — Branded reports, ROI-ranked recs, audit history, v0.3.0 (489 tests)
- Branded report templates (--brand-logo, --brand-color)
- ROI-ranked recommendations (ComparisonRecommendation model)
- Audit history + trend view (HistoryStore,
audit history list/show)
- ✅ Sprint 5 — Snapshot tests, CLI coverage, error decorator wiring, history diff, v0.4.0 (546 tests)
- Snapshot testing infrastructure (syrupy) for HTML renderers
- CLI coverage for all 10 commands
- Error decorator wiring + consistency pass (RuntimeError → exit 10)
audit history diffwith stable entry-idsCHANGELOG.mdand--cov-fail-under=85CI gate
- ✅ Sprint 6 — Coverage close-out, test isolation, multi-store batch, observability, v0.5.0 (606 tests)
tests/test_table_snapshots.py(Rich Console captures)- Zero CWD-relative writes in tests
audit shopify batch --stores-filefor multi-store inventoryengine._loggingwith 6 structured log hooksCONTRIBUTING.md,--cov-fail-under=90
- ✅ Sprint 7 — Scheduled re-audit, dependency hygiene, PageSpeed cache, v0.6.0 (642 tests)
audit schedule list/add/remove/run-all+ crontab runbook- Dependabot + SLSA build-provenance attestation
- PageSpeed response cache (
PAGESPEED_CACHE_TTL) - Report footer version drift fixed
- ✅ Sprint 8 — UX polish, test architecture, shared run_parallel, v0.7.0 (665 tests)
- "Did you mean: X?" suggestions on every typo site
tests/conftest.py+tests/__init__.py(single source of truth forREPO_ROOT+ fixtures)- Shared
run_parallelhelper (batch + scheduler share it) audit schedule run-all --parallel(deferred from Sprint 7)[pdf]extra — WeasyPrint no longer required by defaultdocs/tutorial.mdwalkthrough
- ✅ Sprint 9 — Doc hygiene, wheel packaging, security polish, v0.7.1 (672 tests)
- JSON schema ships inside the wheel (
importlib.resources-readable) PAGESPEED_API_KEYenv var + API-key redaction in error messagesschedules.jsonwritten with0600permissions- Issue templates (bug report + feature request)
- Doc hygiene: canonical env-var reference, drift-free test counts
- JSON schema ships inside the wheel (
- ✅ Sprint 10 — Type safety + SBOM, v0.8.0 (672 tests)
- Mypy CI-gate, zero
type: ignorecomments ImageDictTypedDict contract across the pipeline- CycloneDX SBOM in release artifacts (complements SLSA provenance)
- Mypy CI-gate, zero
- ✅ Sprint 11 — User-side TOML config, v0.9.0 (695 tests)
~/.config/shopify-image-audit/config.toml(13 keys / 5 sections)- Precedence: CLI flag > env var > config > default
- Broken config warns + falls back — never blocks a run
- ✅ Sprint 12 — Repo hygiene + format gate, v0.9.1 (695 tests)
ruff formatapplied across the whole repo (53 files)ruff format --checkadded to CI (no future drift)- 4 Dependabot PRs merged (rich constraint widened, GitHub Actions bumps)
- ✅ Sprint 13 — Strict mypy, v0.10.0 (695 tests)
mypy --strictclean across 30 source files (zero ignore comments)ParamSpec+TypeVarpreserves decorator signatures- 26 mechanical annotation fixes (
dict[str, Any],list[Any], return-type annotations)
- ✅ Sprint 14 — ImageDelta schema, v0.10.1 (698 tests)
ImageDelta.before/.afterareImageItem(wasdict[str, Any])- Closes the Sprint 13 strict-mypy follow-up — no more
cast()inbaseline_manager.py extra="forbid"onImageItemnow protects against future drift
- ✅ Sprint 15 — TypedDict hierarchy, v0.11.0 (705 tests)
LighthouseJson+AuditEntry+Categories+PerformanceCategoryCachedPageSpeedResponse(envelope) +CachedEntry(cache)run_audit()parsesLighthouseJsondirectly
- ✅ Sprint 16 — Lighthouse install validation, v0.12.0 (715 tests)
--lighthouse-bin PATHflag +$LIGHTHOUSE_BINenv override- 10-minute per-run timeout (no more infinite hangs)
docs/integrations/LIGHTHOUSE.md(install, CI recipes, troubleshooting)- 6/6
_run_lighthousetesting gap closed (+ 4 resolver tests)
- ✅ Sprint 17 — Rich Progress bar, v0.13.0 (721 tests)
- Live progress bar for
audit shopify batchandaudit schedule run-all on_donecallback inrun_parallel(per-item observer hook)- Transient bar erases on completion; per-store post-mortem lines remain the canonical output
- Live progress bar for
- ✅ Sprint 18 — Shopify Admin API TypedDicts, v0.14.0 (726 tests)
- Typed contracts for
shop/products/themes/theme_assets - Slim client-side output shapes typed separately
- All
total=False—.get(...)-based reads and partial mocks work
- Typed contracts for
- ✅ Sprint 19 — Shopify Admin OAuth, v0.15.0 (752 tests)
- ✅ Sprint 20 — Token encryption, v0.16.0 (780 tests)
tokens.jsonencrypted at rest with Fernet (AES-128-CBC + HMAC)- Key stored in system keyring (macOS Keychain / Windows Credential Manager / Linux Secret Service) — zero prompts on desktop
$SHOPIFY_AUDIT_TOKENS_DISABLED=1opt-out for CIaudit shopify login <store>automates the manual custom-app flow- Embedded loopback HTTP server with CSRF state (constant-time check)
- Tokens persisted to
tokens.json(mode 0600) docs/integrations/SHOPIFY_OAUTH.mdcovers setup + troubleshooting- Closes the long-deferred "OAuth flow" roadmap item
- ✅ Sprint 21 — Batch auto-token from TokensStore, v0.16.1 (785 tests)
audit shopify batchfalls back totokens.jsonwhen astores-fileentry has noaccess_token- Credential-free
stores.jsonshareable across a team - Explicit
access_tokenstill wins (backwards compatible)
- ✅ Sprint 22 — Multi-store batch login, v0.16.2 (797 tests)
audit shopify login --stores-file stores.jsonauthorises all stores in one run- Per-store failures reported in a summary; exit 2 if any failed
- Closes the last OAuth follow-up deferred since Sprint 19
- ✅ Sprint 23 — Lighthouse mobile preset regression, v0.16.3 (800 tests)
- v0.16.2 broke
audit run --device mobilewith--preset=mobile(Lighthouse 13 rejects it) - Mobile now uses the Lighthouse default config; desktop still
passes
--preset=desktop TestRunLighthouseCmdShapepins the exactsubprocess.runcmd list to prevent regressions
- v0.16.2 broke
- ✅ Sprint 24 — Audit-truth & redirect detection, v0.16.4 (812 tests)
- Empty-image audits no longer claim "All images look well optimised" — they surface "No images were extracted" instead
- Redirects (e.g. Shopify storefront password -> /password) are
caught via
finalUrland fail with exit 10 + a clear error - No
audit_result.jsonis written for off-target audits - Docs:
docs/integrations/LIGHTHOUSE.mdLimitations section
- ✅ Sprint 25 — Authenticated Lighthouse, v0.17.0 (825 tests)
audit run --storefront-password <pwd>(or$SHOPIFY_STOREFRONT_PASSWORD) authenticates password- protected Shopify storefronts- POSTs the
/passwordform once, threads the_shopify_essentialcookie into Lighthouse via--extra-headers - Closes the long-standing "authenticated storefronts are not supported" limitation from v0.16.4
- ✅ Sprint 26 — Image extractor network-requests fallback, v0.17.1 (830 tests)
- When
image-elementsandresource-summaryare both empty, the extractor now falls back tonetwork-requestsfiltered byresourceType == "Image" - Restores image discovery for older fixtures and certain dynamic pages that v0.17.0 still missed
- When
Further reading
docs/spec/cli_v0_1.md— full CLI specificationdocs/governance.md— ownership + workflowdocs/runbook/measurement_protocol.md— how LCP/CLS/INP are measured deterministicallydocs/SPRINT_1_COMPLETE.md— what shipped in Sprint 1docs/SPRINT_3_PLAN.md— Sprint 3 breakdown (all done)docs/SPRINT_4_PLAN.md— Sprint 4 breakdown (all done)docs/SPRINT_5_PLAN.md— Sprint 5 breakdown (all done)QA_CHECKLIST.md— quality gates
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shopify_image_audit-0.17.1.tar.gz.
File metadata
- Download URL: shopify_image_audit-0.17.1.tar.gz
- Upload date:
- Size: 161.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca16a926fee96effb3ef0828740b8ad641d5eb0edb17e4e7b3adb3272e0ce234
|
|
| MD5 |
0f34a9fc6149f82ab2836a71c2afd228
|
|
| BLAKE2b-256 |
ded28fdfbfeb922f4ba7e531c9118dc1ad34abcf2fefe8bd90636857c1bb1148
|
Provenance
The following attestation bundles were made for shopify_image_audit-0.17.1.tar.gz:
Publisher:
release.yml on xopsio/shopify-image-audit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shopify_image_audit-0.17.1.tar.gz -
Subject digest:
ca16a926fee96effb3ef0828740b8ad641d5eb0edb17e4e7b3adb3272e0ce234 - Sigstore transparency entry: 2336188908
- Sigstore integration time:
-
Permalink:
xopsio/shopify-image-audit@f41692d4fcc408e5c6d14506ee93649ca5a5dc98 -
Branch / Tag:
refs/tags/v0.17.1 - Owner: https://github.com/xopsio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f41692d4fcc408e5c6d14506ee93649ca5a5dc98 -
Trigger Event:
push
-
Statement type:
File details
Details for the file shopify_image_audit-0.17.1-py3-none-any.whl.
File metadata
- Download URL: shopify_image_audit-0.17.1-py3-none-any.whl
- Upload date:
- Size: 116.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c26f359c98c6259434402b5cccf0e142155e89e9fb6df1ba00f7795416e8a865
|
|
| MD5 |
1c94bca38f7005d76df923b57f29e186
|
|
| BLAKE2b-256 |
314fcffa9bd92798ee387b8d7d90ba561cc3b9cf8409966fd92b438d150163d5
|
Provenance
The following attestation bundles were made for shopify_image_audit-0.17.1-py3-none-any.whl:
Publisher:
release.yml on xopsio/shopify-image-audit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shopify_image_audit-0.17.1-py3-none-any.whl -
Subject digest:
c26f359c98c6259434402b5cccf0e142155e89e9fb6df1ba00f7795416e8a865 - Sigstore transparency entry: 2336188956
- Sigstore integration time:
-
Permalink:
xopsio/shopify-image-audit@f41692d4fcc408e5c6d14506ee93649ca5a5dc98 -
Branch / Tag:
refs/tags/v0.17.1 - Owner: https://github.com/xopsio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f41692d4fcc408e5c6d14506ee93649ca5a5dc98 -
Trigger Event:
push
-
Statement type: