Skip to content

Retail DC demo — Shoprite-style data generator + 18-model dbt pipeline - #2

Open
imbilawork wants to merge 13 commits into
mainfrom
feat/retail-dc-demo
Open

Retail DC demo — Shoprite-style data generator + 18-model dbt pipeline#2
imbilawork wants to merge 13 commits into
mainfrom
feat/retail-dc-demo

Conversation

@imbilawork

Copy link
Copy Markdown
Contributor

Summary

Adds a second analytics schema (analytics_retail_dc) alongside SAFEX to show what the BI stack looks like against a large-retailer inventory footprint.

  • Data generator (scripts/generate_retail_dc.py): 8 DCs, 2,493 SKUs across 18 categories, 80 SA-flavoured suppliers (Tiger Brands, AVI, Clover, Unilever SA, P&G, Distell, SAB + 3 own-label), 820 stores across 6 Shoprite-group banners, 183k pallet positions, 553k movements over 60 days with Pareto-weighted SKU velocity. Deterministic (seed=42).
  • 18 dbt models under dbt/models/dc/: 8 staging + 10 marts covering DC utilisation, stock cover days, expiry risk, category mix, store/DC flow, supplier inbound, top movers.
  • COPY loader (scripts/load_movements.py): bulk-loads the 553k movement rows via psycopg2.copy_expert (dbt seed would take ~1 hour over Atlantic latency).
  • Superset bootstrap (scripts/superset_bootstrap.py): idempotent REST-API driver that registers the Neon DB, creates 10 datasets, builds charts, and wires them into a "Retail DC — Operations Overview" starter dashboard.

Row counts

table rows
dim_sku 2,493
dim_store 820
fct_pallet_stock 183,320
fct_stock_movements 553,800
fct_stock_cover_days 18,122
fct_store_dc_flow 107,819

Stock on hand: R14.0 bn. 28-day outbound: R25.8 bn. Cover-days bucket split: 22% stockout risk / 21% healthy / 16% buffer / 31% overstock / 9% non-moving.

Depends on

Test plan

  • python3 scripts/generate_retail_dc.py regenerates all CSVs deterministically
  • dbt seed --select path:seeds/dc loads 7 seeds (~180s for pallet_stock over Neon)
  • scripts/load_movements.py COPYs 553k rows cleanly
  • dbt run --select path:models/dc builds 18 models green
  • dbt test --select path:models/dc passes 8 source tests
  • scripts/superset_bootstrap.py registers DB + datasets + dashboard against a running Superset

🤖 Generated with Claude Code

imbilawork and others added 13 commits April 23, 2026 10:43
- Bump pyjwt pin to >=2.10.1,<3.0 (closes Dependabot alert #1 — PyJWT 2.9.0
  accepts unknown `crit` header extensions, a high-severity issue for any
  service validating JWTs from untrusted parties).
- Add sql/neon-init.sql — one-time CREATE SCHEMA for superset_meta and
  analytics. Kept out of dbt/seeds/ where dbt would try to load it as
  tabular data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/setup-env.sh: reads a Neon pooled URI (stdin, secrets.txt, or
  interactive prompt), derives the direct endpoint by stripping `-pooler`,
  generates the three required secrets with openssl, writes a mode-600 .env.
- docs/quickstart.md: eight-step sequence from bare MacBook to a running
  Superset with seeded SilverGro marts. Includes dbt setup, dataset
  registration, and dashboard authoring hints.
- README: point at the quickstart instead of duplicating setup steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- add flask-cors: ENABLE_CORS=True requires the dep; without it superset-init
  crashes on ModuleNotFoundError before metadata migrations run.
- add flask-appbuilder>=4.5.4: FAB 4.5.0 emits int JWT subjects; pyjwt>=2.10
  rejects those on decode, breaking every authenticated API call after login.
- add TCP keepalives to SQLALCHEMY_ENGINE_OPTIONS: superset init creates
  hundreds of permission-view rows in one transaction; Neon's proxy silently
  drops idle connections mid-transaction without keepalives (observed:
  SSL SYSCALL error: EOF after ~17 min into init).
- celery beat --schedule=/tmp/celerybeat-schedule: default schedule path is
  /app which is read-only for the superset user, beat restart-loops otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eline

Populates a second analytics schema (analytics_retail_dc) alongside the SAFEX
commodity model. Demonstrates what the BI stack looks like against a large-
retailer inventory footprint:

  - 8 DCs (Cilmor, Canelands, Basfour, Montague Gardens, Brackenfell Liquor,
    Midrand, East London, Bloemfontein) with realistic capacity 14k-65k pallets
  - 2,493 SKUs across 18 categories / 122 subcategories
  - 80 SA-flavoured suppliers (Tiger Brands, AVI, Clover, Unilever SA, etc.)
    + three own-label flags (Ritebrand, Oh My Goodness!, Simple Truth)
  - 820 stores across 6 banners with province-weighted distribution
  - 183k current pallet positions (util 52-72% across DCs)
  - 553k movements over 60 days with Pareto-weighted SKU velocity
    (top 20% of SKUs do ~80% of outbound volume)

scripts/generate_retail_dc.py — deterministic (seed=42), standard-library only.
Dims land as dbt seeds; fct_pallet_stock regenerates locally (gitignored);
fct_stock_movements loads via psycopg2 COPY (scripts/load_movements.py) because
dbt seed would take an hour for 550k rows over Neon's latency.

dbt models in dbt/models/dc/: 8 staging views + 10 marts covering DC
utilisation, stock cover days, expiry risk, category mix, store/DC flow,
supplier inbound and top movers.

scripts/superset_bootstrap.py — idempotent API driver: registers the Neon DB,
creates datasets, builds charts and wires them into a "Retail DC — Operations
Overview" starter dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single self-contained HTML file covering the whole stack in one place:
architecture diagram (SVG), both dbt domains + row counts, local setup,
retail DC demo bring-up, GCP Cloud Run deploy, the four first-run bring-up
gotchas, operations (refresh / redeploy / teardown), and a reference table
of env vars, deployed services and repo layout.

No external dependencies. Open in a browser to read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dbt-project-yml schema override concatenates with profile target,
so the actual landing schema for SAFEX models is analytics_analytics.
Added a callout flagging the issue + the fix path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub secret-scanning flagged a Postgres connection-string-shaped
placeholder; it was docs/example values, not real credentials, but the
document also carried real infrastructure identifiers (Neon endpoint,
Upstash endpoint, Cloud Run URL, project number) that should not be in
a public-repo doc. Replaced each with angle-bracket placeholders and
reworded the pooled-URI example so it no longer parses as a valid URL.

No actual credentials were ever committed. This changes HEAD only; the
previous commit 8bab97f still contains the identifiers in its diff
until either the alert is dismissed as a false positive, or the branch
is rewritten with git filter-repo and force-pushed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the Global External HTTPS LB + Serverless NEG workaround for
custom domains in africa-south1 (where managed run domain-mappings returns
501 UNIMPLEMENTED). Covers:

- why native domain-mappings is blocked
- SVG of the request path: browser → DNS → anycast IP → forwarding rule →
  HTTPS proxy → URL map → backend service → serverless NEG → Cloud Run
- 7-command gcloud provisioning sequence
- DNS record to add (Cloudflare grey-cloud)
- three gotchas: portName on backend service with Serverless NEG;
  cert stuck at PROVISIONING behind Cloudflare proxy; and the 501
  UNIMPLEMENTED on africa-south1 that started this whole dance
- verify + teardown

All placeholder values; no real anycast IP, hostname, or project number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Complete inventory of what the GCP deploy creates, with IAM graph and
day-two material:

- resource inventory table: every resource in nth-bi-stack, by layer
- IAM graph SVG: principals → identities → roles/resources
- Secret Manager table: each of the 8 secrets with purpose + consumer
- Cloud Run service flags table with the reasoning behind every value
  (CPU/memory/concurrency/min-max/timeout)
- cost expectations at demo scale (~$60-70/mo, itemised)
- observability: where logs / metrics / audit live + a gcloud logging tail
- teardown: surgical cleanup of the bi-stack resources vs full project
  deletion

All placeholder values; no real identifiers or credentials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The custom domain (Global HTTPS LB) and min-instances=1 are the two
costs that make the stack ~$60/mo at idle. Neither is necessary for
dev/testing, but both are nice when showing to a customer.

- Add gcp/deploy-lb.sh: idempotent, 7-resource LB provisioning. Takes
  GCP_PROJECT + BI_DOMAIN env vars. Prints the anycast IP at the end.
- Add gcp/teardown-lb.sh: reverse-order delete for clean removal.
- Default gcp/deploy-web.sh to --min-instances=0 (was 1). Override with
  BI_MIN_INSTANCES=1 for an always-warm instance ahead of a demo.
- Update docs/overview.html:
    - new "scripted provisioning + teardown" subsection for the LB
    - extended cost table showing both configurations side by side
    - callout explaining the default shape and how to flip to warm+LB

Steady-state cost on default settings: ~$0-2/mo. For a demo, two
commands get to ~$60/mo polish: deploy-lb.sh + BI_MIN_INSTANCES=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old string-splitting code:
  port=int(REDIS_URL.rsplit(":", 1)[-1].split("/")[0])
crashes on ValueError when the URL has a query component, e.g.
  rediss://default:pw@host:6379?ssl_cert_reqs=CERT_REQUIRED
because the "port" piece becomes "6379?ssl_cert_reqs=CERT_REQUIRED".

Upstash + python-redis require ssl_cert_reqs for TLS connections, so
that query param is mandatory for the Cloud Run deploy. Caused a web
startup crash the moment Cloud Run picked up a new secret version with
the param appended.

Fix: use urllib.parse.urlparse. Also enables ssl=True + ssl_cert_reqs
on RESULTS_BACKEND automatically when the URL scheme is rediss://.
Local docker-compose (redis://redis:6379/0) behaves identically.

Fifth first-run bring-up bug — should have caught this when I added the
Upstash ssl_cert_reqs query param, didn't, cost us a surprise 503 the
next day when Cloud Run re-read the secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-click links into Cloud Run, logs, monitoring, IAM, secrets,
Artifact Registry, billing, org policies, and load balancer lists.
Saves the tab-hunt every time something needs checking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added 'Neon compute topology — read replica split' subsection under Data
model. Covers:

- why split: offload dashboard SELECTs from the primary that takes
  Superset metadata writes + dbt builds
- URI-to-endpoint mapping (metadata → primary direct, analytics →
  replica direct, dbt → primary pooled)
- Neon-specific note: replicas share credentials with the primary;
  endpoint URL is a literal string-swap of the ep- prefix
- snippet: swap via one gcloud secrets versions add + one PUT against
  Superset's database API

Also flagged the change in the Secret Manager inventory: ANALYTICS_DB_URI
now points at the read replica, not the primary.

Companion to the runtime change: ANALYTICS_DB_URI secret bumped to v2
with the replica URI; Superset's stored 'Analytics (Neon)' database
record updated via API to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant