dda-sdk 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dda_sdk-0.0.1/.claude/skills/dda-analytics/SKILL.md +523 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/README.md +187 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/SKILL.md +60 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/avatar/avatar-color.png +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/avatar/avatar-dark.png +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/avatar/avatar-white.png +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/favicon/favicon-32.png +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/favicon/favicon-512.png +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/bi-dark-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/bi-dark.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/bi-white-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/bi-white.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-color-round.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-dark-color-round.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-dark-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-dark-round.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-dark.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-default-round.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-default.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-white-round.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/icon/icon-white.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-dark-on-white.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-dark.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-one-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-primary.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-white-color.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/logo/logo-white.svg +1 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/apple.svg +3 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/epic.svg +5 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/gog.svg +5 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/google.svg +7 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/humble.svg +3 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/meta.svg +3 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/microsoft.svg +17 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/nintendo.svg +3 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/playstation.svg +3 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/assets/platforms/steam.svg +5 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/colors_and_type.css +338 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Black.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-BlackItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Bold.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-BoldItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-ExtraBold.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-ExtraBoldItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-ExtraLight.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-ExtraLightItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Italic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Light.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-LightItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Medium.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-MediumItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-Regular.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-SemiBold.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-SemiBoldItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Mulish-VariableFont_wght.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Black.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-BlackItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Bold.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-BoldItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Italic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Light.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-LightItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Medium.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-MediumItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Regular.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-Thin.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/fonts/Roboto-ThinItalic.ttf +0 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/_card.css +41 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/_shell.html +4 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-app-icons.html +36 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-avatar-favicon.html +59 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-logo-construction.html +98 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-logo.html +49 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-pixel-motif.html +101 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/brand-platforms.html +33 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/colors-core.html +17 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/colors-inks.html +51 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/colors-secondary.html +15 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/colors-sequential.html +45 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/colors-theme-dark.html +18 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-buttons.html +40 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-chart-line.html +49 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-checkbox.html +110 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-forms.html +37 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-kpi.html +26 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-nav.html +53 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/component-table.html +29 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/spacing-elevation.html +19 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/spacing-radii.html +17 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/spacing-scale.html +24 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/type-body.html +28 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/type-display.html +29 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/preview/type-scale.html +23 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/ui_kits/data-platform/overview.html +838 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/ui_kits/diagrams/regional-pricing.html +273 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/ui_kits/marketing-site/home.html +578 -0
- dda_sdk-0.0.1/.claude/skills/indiebi-design-system/ui_kits/reports/altworks-march-2026.html +301 -0
- dda_sdk-0.0.1/.gitignore +12 -0
- dda_sdk-0.0.1/PKG-INFO +11 -0
- dda_sdk-0.0.1/README.md +104 -0
- dda_sdk-0.0.1/dda_sdk/__init__.py +44 -0
- dda_sdk-0.0.1/dda_sdk/config.py +13 -0
- dda_sdk-0.0.1/dda_sdk/docs.py +6 -0
- dda_sdk-0.0.1/dda_sdk/duckdb_connector.py +123 -0
- dda_sdk-0.0.1/dda_sdk/polars_connector.py +120 -0
- dda_sdk-0.0.1/dda_sdk/schemas/__init__.py +52 -0
- dda_sdk-0.0.1/dda_sdk/schemas/_fields.py +397 -0
- dda_sdk-0.0.1/dda_sdk/schemas/baseline.py +55 -0
- dda_sdk-0.0.1/dda_sdk/schemas/countries.py +20 -0
- dda_sdk-0.0.1/dda_sdk/schemas/currencies.py +16 -0
- dda_sdk-0.0.1/dda_sdk/schemas/engagements_per_sku.py +53 -0
- dda_sdk-0.0.1/dda_sdk/schemas/events.py +37 -0
- dda_sdk-0.0.1/dda_sdk/schemas/organizations.py +14 -0
- dda_sdk-0.0.1/dda_sdk/schemas/portals.py +26 -0
- dda_sdk-0.0.1/dda_sdk/schemas/products.py +22 -0
- dda_sdk-0.0.1/dda_sdk/schemas/sales.py +47 -0
- dda_sdk-0.0.1/dda_sdk/schemas/sales_per_sku.py +55 -0
- dda_sdk-0.0.1/dda_sdk/schemas/skus.py +33 -0
- dda_sdk-0.0.1/dda_sdk/schemas/visibility.py +37 -0
- dda_sdk-0.0.1/dda_sdk/schemas/visibility_wishlist_per_sku.py +43 -0
- dda_sdk-0.0.1/dda_sdk/schemas/wishlist_actions.py +31 -0
- dda_sdk-0.0.1/dda_sdk/schemas/wishlist_cohorts.py +29 -0
- dda_sdk-0.0.1/dda_sdk/ui.py +10 -0
- dda_sdk-0.0.1/dda_sdk/updater.py +20 -0
- dda_sdk-0.0.1/dda_sdk/versioning.py +72 -0
- dda_sdk-0.0.1/dda_sdk/workspace_sync.py +61 -0
- dda_sdk-0.0.1/docs/assets/annual_sales_dashboard_example.png +0 -0
- dda_sdk-0.0.1/docs/assets/favicon.png +0 -0
- dda_sdk-0.0.1/docs/assets/logo.svg +14 -0
- dda_sdk-0.0.1/docs/data-freshness.md +5 -0
- dda_sdk-0.0.1/docs/getting-started.md +36 -0
- dda_sdk-0.0.1/docs/index.md +27 -0
- dda_sdk-0.0.1/docs/portal-support.md +55 -0
- dda_sdk-0.0.1/docs/querying-data.md +85 -0
- dda_sdk-0.0.1/docs/schema-reference.md +376 -0
- dda_sdk-0.0.1/docs/stylesheets/extra.css +78 -0
- dda_sdk-0.0.1/docs/updating.md +29 -0
- dda_sdk-0.0.1/docs/use-with-claude.md +33 -0
- dda_sdk-0.0.1/pyproject.toml +58 -0
- dda_sdk-0.0.1/tests/__init__.py +0 -0
- dda_sdk-0.0.1/tests/schemas/__init__.py +0 -0
- dda_sdk-0.0.1/tests/schemas/test_countries.py +52 -0
- dda_sdk-0.0.1/tests/test_build_workspace_zip.py +40 -0
- dda_sdk-0.0.1/tests/test_config.py +23 -0
- dda_sdk-0.0.1/tests/test_connector.py +45 -0
- dda_sdk-0.0.1/tests/test_updater.py +46 -0
- dda_sdk-0.0.1/tests/test_version.py +7 -0
- dda_sdk-0.0.1/tests/test_versioning.py +100 -0
- dda_sdk-0.0.1/tests/test_wheel_assets.py +39 -0
- dda_sdk-0.0.1/tests/test_workspace_sync.py +94 -0
- dda_sdk-0.0.1/workspace/.claude/settings.json +14 -0
- dda_sdk-0.0.1/workspace/.env.example +3 -0
- dda_sdk-0.0.1/workspace/.gitignore +4 -0
- dda_sdk-0.0.1/workspace/CLAUDE.local.md +3 -0
- dda_sdk-0.0.1/workspace/CLAUDE.md +28 -0
- dda_sdk-0.0.1/workspace/README.md +51 -0
- dda_sdk-0.0.1/workspace/pyproject.toml +6 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dda-analytics
|
|
3
|
+
description: Use this skill whenever the user asks for charts, graphs, or data analysis using IndieBI game data. Triggers for: "show me revenue from Steam", "chart of wishlist adds", "how did [game] perform in Q1", "compare sales across platforms", "units sold last month", "what's our best performing title", "generate a report", "visualize [metric]", "which countries", "sales by country", "top countries", "geographic breakdown", "discounts", "promotions", "sales events", "what promos ran", "Steam Summer Sale", "how did the discount perform", "what was the discount", "promo impact", "sales uplift", "event performance", "what events ran", "discount history", "promo calendar", "wishlist adds", "wishlist conversions", "store page visits", "visibility data", "page impressions". Always invoke this skill before writing any data-fetching or chart code — it tells you which table to query, how to write the fetch script, and how to render the result as an IndieBI-styled chart.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DDA Analytics
|
|
7
|
+
|
|
8
|
+
This skill walks through fetching IndieBI DDA data and rendering it as an HTML chart.
|
|
9
|
+
|
|
10
|
+
## Pre-flight — Check project is initialized
|
|
11
|
+
|
|
12
|
+
Before doing anything else, verify the project is ready to run. Do this by checking whether a virtual environment exists:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
test -d .venv && echo "ok" || echo "missing"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If `.venv` is **missing**, the user hasn't run `uv sync` yet. Walk them through setup:
|
|
19
|
+
|
|
20
|
+
### 1. Check Python
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python3 --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If the command fails or returns a version below 3.12, **ask the user before proceeding**:
|
|
27
|
+
|
|
28
|
+
> "Python 3.12+ is required but doesn't seem to be installed. Should I install it for you? (This will use your system package manager.)"
|
|
29
|
+
|
|
30
|
+
Wait for confirmation before running any install command.
|
|
31
|
+
|
|
32
|
+
### 2. Check uv
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv --version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If the command fails, **ask the user before proceeding**:
|
|
39
|
+
|
|
40
|
+
> "`uv` (the Python package manager) isn't installed. Should I install it now? It's a quick one-liner: `curl -LsSf https://astral.sh/uv/install.sh | sh`"
|
|
41
|
+
|
|
42
|
+
Wait for confirmation, then run the install.
|
|
43
|
+
|
|
44
|
+
### 3. Run uv sync
|
|
45
|
+
|
|
46
|
+
Once Python and uv are available, run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Tell the user: "Project dependencies installed — you're ready to go."
|
|
53
|
+
|
|
54
|
+
If `.venv` **exists**, the project is already set up — skip this section and proceed to Step 0.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 0 — Create a project folder
|
|
59
|
+
|
|
60
|
+
Before doing anything else, create a timestamped folder for this request under `projects/` in the repo root. This folder holds every artifact produced: query scripts and the final HTML. The user can review them there at any time.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from datetime import datetime
|
|
64
|
+
import os
|
|
65
|
+
|
|
66
|
+
slug = "short-description-of-request" # e.g. "steam-revenue-jan2026"
|
|
67
|
+
folder = f"projects/{datetime.now().strftime('%Y%m%d-%H%M%S')}-{slug}"
|
|
68
|
+
os.makedirs(folder, exist_ok=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All `.py` scripts go in this folder. The final HTML goes in this folder. Never ask the user to approve the scripts before running them — just run them.
|
|
72
|
+
|
|
73
|
+
> **Read-only project rule:** This skill only ever writes inside the `projects/` folder it creates. Never create, edit, or delete any file outside of `projects/` — this means no touching `dda_sdk/`, `schemas/`, `tests/`, `.claude/`, or any other file in the repo. The SDK and schemas are the data source; treat them as read-only infrastructure.
|
|
74
|
+
>
|
|
75
|
+
> **Exception:** when handling a credentials error (see below), you must write to `.env` in the project root. That is the only file outside `projects/` you are allowed to touch.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Credential handling
|
|
80
|
+
|
|
81
|
+
When any fetch script raises a `ValueError` that mentions "Missing credentials", the user hasn't set up their `.env` yet. Handle it like this:
|
|
82
|
+
|
|
83
|
+
1. **Tell the user what happened** — explain that the SDK needs a User ID and SAS token to access the data lake, and they haven't been set up yet.
|
|
84
|
+
|
|
85
|
+
2. **Ask for the credentials:**
|
|
86
|
+
> "To connect to IndieBI data, I need two things from your IndieBI contact:
|
|
87
|
+
> - **User ID** (looks like `u-xxxxxxx`)
|
|
88
|
+
> - **SAS Token** (starts with `sp=rl&st=...`)
|
|
89
|
+
>
|
|
90
|
+
> Please paste them here and I'll save them to `.env` for you."
|
|
91
|
+
|
|
92
|
+
3. **Save to `.env`** in the project root. If `.env` doesn't exist, create it from scratch using the format in `.env.example`:
|
|
93
|
+
```
|
|
94
|
+
USER_ID=<value the user gave>
|
|
95
|
+
SAS_TOKEN=<value the user gave>
|
|
96
|
+
```
|
|
97
|
+
If `.env` already exists but is missing these keys, append them. Never overwrite other keys already in the file.
|
|
98
|
+
|
|
99
|
+
4. **Retry the fetch script** — run it again. If it works, continue with the rest of the workflow. If it fails for a different reason, debug that separately.
|
|
100
|
+
|
|
101
|
+
### Invalid or expired SAS token
|
|
102
|
+
|
|
103
|
+
When any fetch script raises a `ValueError` that mentions "Azure authentication failed" or "invalid or expired", the SAS token in `.env` is either wrong or has expired.
|
|
104
|
+
|
|
105
|
+
1. **Tell the user what happened:**
|
|
106
|
+
> "The connection to IndieBI failed — your SAS token appears to be invalid or expired."
|
|
107
|
+
|
|
108
|
+
2. **Ask for a new token:**
|
|
109
|
+
> "Could you share a fresh SAS Token? (It starts with `sp=rl&st=...` and comes from your IndieBI contact.) I'll update `.env` for you."
|
|
110
|
+
|
|
111
|
+
3. **Update `.env`** — replace the existing `SAS_TOKEN` value. Never touch any other key in the file.
|
|
112
|
+
|
|
113
|
+
4. **Retry the fetch script.** If it works, continue. If it fails again, tell the user and ask them to double-check the token they provided.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Step 1 — Understand the request and clarify scope
|
|
118
|
+
|
|
119
|
+
Extract three things:
|
|
120
|
+
|
|
121
|
+
- **Metric**: what to measure (revenue, units, wishlist adds, impressions, visits…)
|
|
122
|
+
- **Filters**: platform (Steam, GOG…), date range, country, product/SKU
|
|
123
|
+
- **Granularity**: daily, weekly, monthly, or a single total
|
|
124
|
+
|
|
125
|
+
### Clarify before proceeding
|
|
126
|
+
|
|
127
|
+
**If the request is vague** — e.g. "show me how we're doing" or "what does the data say?" with no clear metric — ask what they want to measure before writing any code. A quick one-line question saves a lot of wasted work.
|
|
128
|
+
|
|
129
|
+
**If dimensions are unspecified** — date range, platforms, or products — don't silently default to "all". Instead, ask:
|
|
130
|
+
|
|
131
|
+
> "You didn't specify [date range / platforms / products]. Should I fetch data across all available [dates / platforms / titles], or do you want to narrow it down?"
|
|
132
|
+
|
|
133
|
+
Use judgment about which dimensions actually need clarifying:
|
|
134
|
+
|
|
135
|
+
- **Date range**: Almost always worth asking if omitted — "all time" can be a huge query and may not be what the user expects.
|
|
136
|
+
- **Platform**: Ask if the data covers multiple portals and the request doesn't imply "all" (e.g. "compare across platforms" implies all; "show me sales" does not).
|
|
137
|
+
- **Product/SKU**: Ask if there are multiple titles and the request could reasonably mean one specific game.
|
|
138
|
+
|
|
139
|
+
If the user's phrasing clearly implies "all" (e.g. "show me all platforms", "across everything", "full year"), skip the clarification and proceed. The goal is to avoid wasted compute on huge queries when the user actually had something specific in mind — not to interrogate them on every request.
|
|
140
|
+
|
|
141
|
+
Ask at most one clarifying message covering all unresolved dimensions and vague intent at once. Then proceed.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Step 2 — Map to table and columns
|
|
146
|
+
|
|
147
|
+
### Metric → column
|
|
148
|
+
|
|
149
|
+
| User says | Column | Table |
|
|
150
|
+
| --------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
151
|
+
| revenue / sales / gross revenue | `gross_sales` (USD) | `sales_per_sku` or `sales` |
|
|
152
|
+
| net revenue | `net_sales_approx` (USD) | same |
|
|
153
|
+
| units sold | `units_sold_directly` | same |
|
|
154
|
+
| returns (units) | `units_returned` | same |
|
|
155
|
+
| returns (revenue) | `gross_returned` | same |
|
|
156
|
+
| media engagement / clicks | `actions` | `engagements_per_sku` |
|
|
157
|
+
| list of promo events / promo calendar | `event_name`, `date_from`, `date_to`, `discount`, `promo_length`, `event_status` | `events` — one row per event, no deduplication needed |
|
|
158
|
+
| promo performance / sales uplift score | `sales_uplift_score`, `click_through_rate`, `conversion_rate` | `events` |
|
|
159
|
+
| wishlist adds / removals | `adds`, `deletes` | `wishlist_actions` or `visibility_wishlist_per_sku` |
|
|
160
|
+
| wishlist conversions / purchases | `purchases_and_activations`, `gifts`, `total_conversions` | `wishlist_actions` or `wishlist_cohorts` |
|
|
161
|
+
| store page visits / impressions | `non_owner_visits`, `non_owner_impressions` | `visibility` or `visibility_wishlist_per_sku` |
|
|
162
|
+
|
|
163
|
+
Use `sales_per_sku` when you need SKU-level detail or want `human_name`. Use `sales` when aggregating across SKUs of the same product.
|
|
164
|
+
|
|
165
|
+
### Currency
|
|
166
|
+
|
|
167
|
+
All monetary columns (`net_sales`, `gross_sales`, `gross_returned`, and the `baseline_*`/`uplift_*` sales columns) are denominated in **USD**, unless a column's description states otherwise. The `currency_code` on `countries` and the `currencies` dimension table describe a country's *local* currency for reference/display only (e.g. showing a currency's name or symbol) — do not use them to interpret the denomination of `net_sales`, `gross_sales`, or any other monetary metric.
|
|
168
|
+
|
|
169
|
+
### Geography → country breakdown
|
|
170
|
+
|
|
171
|
+
Both `sales_per_sku` and `sales` have a `country_code` column (ISO 3166-1 alpha-3, e.g. `"USA"`, `"GBR"`, `"DEU"`). This is how you get individual country data — **not** by parsing `portal_platform_region`.
|
|
172
|
+
|
|
173
|
+
Join with `countries` to get human-readable names and regions:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import polars as pl
|
|
177
|
+
from dda_sdk import DuckDBClient, SalesPerSkuSchema, CountriesSchema
|
|
178
|
+
|
|
179
|
+
with DuckDBClient() as client:
|
|
180
|
+
sales_df = client.query(
|
|
181
|
+
SalesPerSkuSchema,
|
|
182
|
+
"""
|
|
183
|
+
SELECT *
|
|
184
|
+
FROM sales_per_sku
|
|
185
|
+
WHERE date >= '2026-01-01'
|
|
186
|
+
AND date <= '2026-05-15'
|
|
187
|
+
""",
|
|
188
|
+
)
|
|
189
|
+
countries_df = client.query(CountriesSchema, "SELECT * FROM countries")
|
|
190
|
+
|
|
191
|
+
# Join and aggregate
|
|
192
|
+
by_country = (
|
|
193
|
+
sales_df
|
|
194
|
+
.join(countries_df.select(["country_code", "country_name", "region"]), on="country_code", how="left")
|
|
195
|
+
.group_by(["country_code", "country_name", "region"])
|
|
196
|
+
.agg(pl.col("units_sold_directly").sum(), pl.col("gross_sales").sum())
|
|
197
|
+
.sort("units_sold_directly", descending=True)
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
> **`portal_platform_region` vs `country_code`**: The region segment in `portal_platform_region` (e.g. `"PS America"`, `"Nintendo Europe/Australia"`, `"Global"`) is the **platform's own regional grouping** — not individual countries. Always use `country_code` when the user asks about countries, country rankings, or geographic breakdowns. Use `portal_platform_region` only when filtering by storefront or platform.
|
|
202
|
+
|
|
203
|
+
Note: some portals (e.g. Steam when reporting globally) may aggregate all sales under a single `country_code` or leave it empty. If `country_code` values look sparse for a portal, mention it to the user.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### Platform → filter
|
|
208
|
+
|
|
209
|
+
`portal_platform_region` is a composite like `'Steam:PC:Global'` (colon-separated: portal:platform:region).
|
|
210
|
+
|
|
211
|
+
**Always look up the portals table first** — don't hardcode filter strings. This ensures you use the exact values present in the user's data and catch the region ambiguity. The `display_portal` column has the human-friendly name to use in chart labels and report headers (e.g. `"Steam"` instead of `"steam"`).
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from dda_sdk import DuckDBClient, PortalsSchema
|
|
215
|
+
|
|
216
|
+
with DuckDBClient() as client:
|
|
217
|
+
portals_df = client.query(PortalsSchema, "SELECT * FROM portals")
|
|
218
|
+
|
|
219
|
+
# Find rows matching the portal the user mentioned
|
|
220
|
+
steam_rows = portals_df.filter(portals_df["portal"] == "steam")
|
|
221
|
+
# steam_rows["portal_platform_region"].to_list() → ["Steam:PC:Global"]
|
|
222
|
+
# steam_rows["display_portal"].to_list() → ["Steam"] ← use this in chart labels
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Then decide:
|
|
226
|
+
|
|
227
|
+
- **One matching row** → use `portal_platform_region == 'Steam:PC:Global'` (exact match, no LIKE needed)
|
|
228
|
+
- **Multiple rows** (e.g. Nintendo has several regions) → ask the user: "Nintendo has these regions: [list]. Do you want all of them, or a specific one?"
|
|
229
|
+
|
|
230
|
+
Build the SQL filter from the exact value(s) returned:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# Single portal
|
|
234
|
+
ppr = "Steam:PC:Global"
|
|
235
|
+
sql_filter = f"portal_platform_region = '{ppr}'"
|
|
236
|
+
|
|
237
|
+
# Multiple portals
|
|
238
|
+
pprs = ["Nintendo:Switch:Americas", "Nintendo:Switch:Europe"]
|
|
239
|
+
in_list = ", ".join(f"'{p}'" for p in pprs)
|
|
240
|
+
sql_filter = f"portal_platform_region IN ({in_list})"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Date filters
|
|
244
|
+
|
|
245
|
+
Use ISO dates: `date >= '2026-01-01' AND date <= '2026-01-31'`
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Promotions & Discount Events
|
|
250
|
+
|
|
251
|
+
When the user asks about discounts, promos, or sales events, use `events` — it has **one row per event** (no deduplication needed).
|
|
252
|
+
|
|
253
|
+
**Key columns:**
|
|
254
|
+
|
|
255
|
+
- `event_id` — unique identifier per event
|
|
256
|
+
- `event_name` — human-readable name (e.g. "Steam Summer Sale 2026")
|
|
257
|
+
- `date_from` / `date_to` — event start and end
|
|
258
|
+
- `discount` — discount depth as a 0–1 float (e.g. `0.40` = 40% off); multiply by 100 to display as %
|
|
259
|
+
- `promo_length` — event duration in days
|
|
260
|
+
- `event_status` — `"finished"` or `"ongoing"`, or `None`
|
|
261
|
+
- `click_through_rate` — non-owner visits to impressions ratio (null if no impressions)
|
|
262
|
+
- `conversion_rate` — units sold directly to non-owner visits ratio (null if no visits)
|
|
263
|
+
- `sales_uplift_score` — total sales / baseline sales; above 1.0 = positive uplift (null if baseline is zero)
|
|
264
|
+
|
|
265
|
+
**Example — promo calendar:**
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
import polars as pl
|
|
269
|
+
from dda_sdk import DuckDBClient, EventsSchema
|
|
270
|
+
|
|
271
|
+
with DuckDBClient() as client:
|
|
272
|
+
events_df = client.query(
|
|
273
|
+
EventsSchema,
|
|
274
|
+
"SELECT * FROM events WHERE event_status = 'finished'",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Already one row per event — sort and display
|
|
278
|
+
calendar = (
|
|
279
|
+
events_df
|
|
280
|
+
.with_columns(
|
|
281
|
+
(pl.col("discount") * 100).round(0).alias("discount_pct")
|
|
282
|
+
)
|
|
283
|
+
.sort("date_from", descending=True)
|
|
284
|
+
)
|
|
285
|
+
print(calendar)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Example — join event data with sales for promo impact:**
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
import polars as pl
|
|
292
|
+
from dda_sdk import DuckDBClient, EventsSchema, SalesSchema
|
|
293
|
+
|
|
294
|
+
with DuckDBClient() as client:
|
|
295
|
+
events_df = client.query(EventsSchema, "SELECT * FROM events")
|
|
296
|
+
sales_df = client.query(SalesSchema, "SELECT * FROM sales")
|
|
297
|
+
|
|
298
|
+
# Expand events to daily rows, then join to daily sales
|
|
299
|
+
promo_sales = (
|
|
300
|
+
events_df
|
|
301
|
+
.with_columns(
|
|
302
|
+
pl.date_ranges(pl.col("date_from"), pl.col("date_to"), interval="1d").alias("date")
|
|
303
|
+
)
|
|
304
|
+
.explode("date")
|
|
305
|
+
.join(
|
|
306
|
+
sales_df.select(["date", "regional_product_id", "portal_platform_region",
|
|
307
|
+
"gross_sales", "units_sold_directly"]),
|
|
308
|
+
on=["date", "regional_product_id", "portal_platform_region"],
|
|
309
|
+
how="inner",
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
> **Note on `discount` values**: `1.0` = 100% off, `0.0` = no discount. Some portals may report `0.0` during a promo if they don't expose discount depth — use `event_name` and `date_from`/`date_to` to identify the event in that case.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Wishlist & Visibility
|
|
319
|
+
|
|
320
|
+
**Wishlist adds/removes at product level** → use `wishlist_actions`. It has `adds`, `deletes`, `purchases_and_activations`, `gifts`, and `is_promo_sale` per day per product per portal per country.
|
|
321
|
+
|
|
322
|
+
**Wishlist cohort analysis** (how quickly do wishlisted users convert?) → use `wishlist_cohorts`. Group by `month_cohort` to see conversion rates over time.
|
|
323
|
+
|
|
324
|
+
**Store page impressions and visits** → use `visibility`. It has `non_owner_visits`, `non_owner_impressions`, `owner_visits`, `owner_impressions`, broken down by `visit_source` (`"Direct Navigation"` or `"Visits from Impressions"`), `page_category`, and `page_feature`.
|
|
325
|
+
|
|
326
|
+
**SKU-level visibility + wishlist combined** → use `visibility_wishlist_per_sku`. Useful when you need `human_name` or want to see data per individual SKU rather than per product.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Step 3 — Check available tables, then fetch
|
|
331
|
+
|
|
332
|
+
Not all tables are provisioned for every user. When the requested metric uses a table you're not sure about, check first:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from dda_sdk import DuckDBClient
|
|
336
|
+
with DuckDBClient() as client:
|
|
337
|
+
tables = client._conn.execute("SHOW TABLES").fetchall()
|
|
338
|
+
print([t[0] for t in tables])
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
If a table isn't listed, use mock data or tell the user it isn't in their plan.
|
|
342
|
+
|
|
343
|
+
Use `DuckDBClient` for straightforward fetching. Pass the schema that matches the table you're reading — it validates the result. **Always use `SELECT *`** — partial column selects break schema validation (the schema needs all required fields present).
|
|
344
|
+
|
|
345
|
+
**Push filters into the query, never fetch-all-then-filter.** The data lake is large — always pass date ranges, portal, product, and any other known filters directly in the `WHERE` clause (or as `.filter()` before `.collect()` for `PolarsClient`). Never fetch the full table and then filter in Polars. If the user says "January 2026", that goes into `WHERE date >= '2026-01-01' AND date <= '2026-01-31'`, not into a Polars `.filter()` after fetching everything. Aggregate in Polars after fetching, but filter before.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
import polars as pl
|
|
349
|
+
from dda_sdk import DuckDBClient, SalesPerSkuSchema
|
|
350
|
+
|
|
351
|
+
with DuckDBClient() as client:
|
|
352
|
+
df = client.query(
|
|
353
|
+
SalesPerSkuSchema,
|
|
354
|
+
"""
|
|
355
|
+
SELECT *
|
|
356
|
+
FROM sales_per_sku
|
|
357
|
+
WHERE date >= '2026-01-01'
|
|
358
|
+
AND date <= '2026-01-31'
|
|
359
|
+
AND portal_platform_region = 'Steam:PC:Global'
|
|
360
|
+
""",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Aggregate in Polars after fetching
|
|
364
|
+
monthly = (
|
|
365
|
+
df.group_by(pl.col("date").dt.truncate("1mo"))
|
|
366
|
+
.agg(pl.col("gross_sales").sum())
|
|
367
|
+
.sort("date")
|
|
368
|
+
)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Alternatively use `PolarsClient` for larger date ranges (lazy evaluation reads only what you need):
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
import polars as pl
|
|
375
|
+
from dda_sdk import PolarsClient, SalesPerSkuSchema
|
|
376
|
+
|
|
377
|
+
with PolarsClient() as client:
|
|
378
|
+
df = (
|
|
379
|
+
client.scan_table(SalesPerSkuSchema)
|
|
380
|
+
.filter(pl.col("date").is_between(pl.date(2026, 1, 1), pl.date(2026, 1, 31)))
|
|
381
|
+
.filter(pl.col("portal_platform_region").str.starts_with("Steam"))
|
|
382
|
+
.group_by(pl.col("date").dt.truncate("1mo"))
|
|
383
|
+
.agg(pl.col("gross_sales").sum())
|
|
384
|
+
.sort("date")
|
|
385
|
+
.collect()
|
|
386
|
+
)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Save the script as `<folder>/fetch.py` (using the folder created in Step 0) and run it. Print the result as JSON or a simple table so you can read the values and embed them in the chart.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Step 4 — Design and generate the HTML chart
|
|
394
|
+
|
|
395
|
+
Read the design system skill before writing any HTML: `.claude/skills/indiebi-design-system/SKILL.md`
|
|
396
|
+
|
|
397
|
+
Use the `ui_kits/data-platform/overview.html` file in the design system as a reference for card layouts, chart styling, axis labels, and component patterns.
|
|
398
|
+
|
|
399
|
+
### Choose the right chart type
|
|
400
|
+
|
|
401
|
+
Don't default to a bar chart. Look at what the data is actually saying and pick the form that makes it easiest to read:
|
|
402
|
+
|
|
403
|
+
| Data shape | Best chart |
|
|
404
|
+
| ------------------------------------------------- | ---------------------------------------------------------------- |
|
|
405
|
+
| One metric over time (single platform) | **Line chart** — shows trend and momentum clearly |
|
|
406
|
+
| One metric over time, comparing 2–4 platforms | **Multi-line chart** or **grouped bar chart** |
|
|
407
|
+
| Distribution across categories at a point in time | **Bar chart** (horizontal if labels are long, vertical if short) |
|
|
408
|
+
| Part-to-whole (platform share, revenue breakdown) | **Stacked bar** or **donut/pie** (pie only if ≤5 slices) |
|
|
409
|
+
| Ranking (top countries, top titles) | **Horizontal bar chart** — easiest to scan a ranked list |
|
|
410
|
+
| Two metrics at once (e.g. revenue vs. units) | **Dual-axis line** or **bar + line combo** |
|
|
411
|
+
| A single aggregate number the user asked for | **Big number + context line** — no chart needed |
|
|
412
|
+
|
|
413
|
+
When in doubt: if it's "over time" → line. If it's "across categories" → bar. If it's "share of total" → stacked or pie.
|
|
414
|
+
|
|
415
|
+
### Decide whether KPI tiles add value
|
|
416
|
+
|
|
417
|
+
KPI tiles (the big number cards above the chart) are useful when they answer a question the chart alone doesn't — like the total, the change vs. prior period, or the single most important takeaway. But they're not always the right call:
|
|
418
|
+
|
|
419
|
+
- **Include KPIs when**: the user asked a summary question ("how much did we make?"), or when the top-line number isn't obvious from the chart shape alone.
|
|
420
|
+
- **Skip KPIs when**: the chart already tells the full story (e.g. a simple ranking of 5 countries), or they'd just duplicate what's on the axes.
|
|
421
|
+
|
|
422
|
+
When you do include KPIs, pick the ones that are actually meaningful for the specific request — don't default to total / peak / average every time. Think about what would be most useful:
|
|
423
|
+
|
|
424
|
+
| Request type | Useful KPIs |
|
|
425
|
+
| ------------------------ | ----------------------------------------------------------------------- |
|
|
426
|
+
| Annual/period revenue | Total, best month, MoM change in last month |
|
|
427
|
+
| Platform comparison | #1 platform + its share, combined total |
|
|
428
|
+
| Country ranking | Top country, top country's % of total, number of countries contributing |
|
|
429
|
+
| Trend / growth | Starting value, ending value, total % change over period |
|
|
430
|
+
| Event / promo impact | Avg discount %, event count, best-performing event name |
|
|
431
|
+
| Promo calendar / history | Events run, avg promo length, time since last promo |
|
|
432
|
+
|
|
433
|
+
### Visual rules (always apply)
|
|
434
|
+
|
|
435
|
+
1. **Import the CSS**: `<link rel="stylesheet" href="../../.claude/skills/indiebi-design-system/colors_and_type.css">`
|
|
436
|
+
(adjust relative path based on where the HTML lives)
|
|
437
|
+
2. **Dark theme by default**: `background: var(--theme-00)`, cards on `var(--theme-02)`
|
|
438
|
+
3. **Bar colors**:
|
|
439
|
+
- Single-platform: `fill: var(--primary-lavender); opacity: 0.75`, peak bar in `var(--primary-orange)`
|
|
440
|
+
- Multi-platform: use portal brand colors — `var(--portal-steam)`, `var(--portal-gog)`, `var(--portal-nintendo)`, etc. Never use generic colors when platforms are being compared.
|
|
441
|
+
4. **Axis labels**: 9px monospace, `rgba(255,255,255,.35)`
|
|
442
|
+
5. **Platform icons**: use `assets/platforms/<name>.svg` from the design system whenever a storefront is named. Render as `<img>` with `filter: brightness(0) invert(1)` so they appear white on dark backgrounds. Never use emoji or text-only labels for storefronts.
|
|
443
|
+
|
|
444
|
+
### SVG charts — baking in data
|
|
445
|
+
|
|
446
|
+
All data values go directly into the SVG markup — no JavaScript needed. For bar/line charts:
|
|
447
|
+
|
|
448
|
+
- Set `max_value` to a round number slightly above your actual max (e.g. if max is $1.14M, use $1.2M) so bars don't touch the top
|
|
449
|
+
- `chart_height = 150` (y goes from 30 at top to 180 at baseline)
|
|
450
|
+
- `bar_height = value / max_value * chart_height`
|
|
451
|
+
- `y = 180 - bar_height`
|
|
452
|
+
- Grid lines at 25%, 50%, 75%, 100% of max
|
|
453
|
+
|
|
454
|
+
For line charts, compute `cx` and `cy` for each point and connect with a `<polyline>` or `<path>`.
|
|
455
|
+
|
|
456
|
+
Write the HTML to `<folder>/chart.html`, then open it:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
open <folder>/chart.html
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Dimension tables
|
|
465
|
+
|
|
466
|
+
The following tables are **dimension tables** — they serve as lookup/filter tables for the fact tables (`sales`, `sales_per_sku`, `events`, `wishlist_actions`, `visibility`, etc.). Each has a single unique primary key column you can use to join or filter.
|
|
467
|
+
|
|
468
|
+
| Table | Primary key | Use to get |
|
|
469
|
+
| ----- | ----------- | ---------- |
|
|
470
|
+
| `countries` | `country_code` | `country_name`, `region`, `currency_code` (country's local currency, reference/display only — see [Currency](#currency)) |
|
|
471
|
+
| `currencies` | `currency_code` | `currency_name`, `symbol` |
|
|
472
|
+
| `organizations` | `organization_id` | `organization_name` |
|
|
473
|
+
| `portals` | `portal_platform_region` | `portal`, `display_portal`, `platform`, `store`, `region`, `latest_date` |
|
|
474
|
+
| `products` | `regional_product_id` | `product_name`, `product_type`, `custom_group` |
|
|
475
|
+
| `skus` | `unique_sku_id` | `human_name`, `sku_type`, `base_sku_id`, `release_date`, `product_type` |
|
|
476
|
+
|
|
477
|
+
Dimension tables can also be joined to each other. For example, `currencies` joins to `countries` on `currency_code` to enrich country rows with currency symbols.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Table relationships
|
|
482
|
+
|
|
483
|
+
**The join rule:** when the same column name appears in two schema files, those tables can be joined on that column. No separate foreign-key docs needed — the shared name is the contract.
|
|
484
|
+
|
|
485
|
+
Key shared columns and what they unlock:
|
|
486
|
+
|
|
487
|
+
| Shared column | Tables that have it | What the join adds |
|
|
488
|
+
| ------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------- |
|
|
489
|
+
| `country_code` | `sales_per_sku`, `sales` ↔ `countries` | `country_name`, `region`, `currency_code` |
|
|
490
|
+
| `currency_code` | `countries` ↔ `currencies` | `currency_name`, `symbol` |
|
|
491
|
+
| `regional_product_id` | `sales_per_sku`, `sales`, `events` ↔ `products` | `product_name`, `product_type` |
|
|
492
|
+
| `unique_sku_id` | `sales_per_sku`, `engagements_per_sku` ↔ `skus` | `human_name`, `sku_type`, `product_type`, `regional_product_id` |
|
|
493
|
+
| `date` + `regional_product_id` + `portal_platform_region` | `visibility`, `wishlist_actions` ↔ `sales` | Join traffic/wishlist data to daily sales |
|
|
494
|
+
| `portal_platform_region` | sales, events, wishlist, visibility tables ↔ `portals` | `portal`, `display_portal`, `platform`, `region` as separate columns |
|
|
495
|
+
| `organization_id` | `sales_per_sku`, `sales` ↔ `organizations` | `organization_name` |
|
|
496
|
+
|
|
497
|
+
When you're not sure whether two tables share a column, read both schema files and compare the field names — if a name matches, join on it.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## All available tables
|
|
502
|
+
|
|
503
|
+
Before writing any query, read the schema file for the table you plan to use — it is the authoritative source of column names and types. Column names change; this skill does not auto-update.
|
|
504
|
+
|
|
505
|
+
| Schema class | SQL table name | Schema file |
|
|
506
|
+
| ------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
507
|
+
| `SalesPerSkuSchema` | `sales_per_sku` | `dda_sdk/schemas/sales_per_sku.py` |
|
|
508
|
+
| `SalesSchema` | `sales` | `dda_sdk/schemas/sales.py` |
|
|
509
|
+
| `EngagementsPerSkuSchema` | `engagements_per_sku` | `dda_sdk/schemas/engagements_per_sku.py` |
|
|
510
|
+
| `EventsSchema` | `events` | `dda_sdk/schemas/events.py` |
|
|
511
|
+
| `VisibilitySchema` | `visibility` | `dda_sdk/schemas/visibility.py` |
|
|
512
|
+
| `VisibilityWishlistPerSkuSchema`| `visibility_wishlist_per_sku` | `dda_sdk/schemas/visibility_wishlist_per_sku.py` |
|
|
513
|
+
| `WishlistActionsSchema` | `wishlist_actions` | `dda_sdk/schemas/wishlist_actions.py` |
|
|
514
|
+
| `WishlistCohortsSchema` | `wishlist_cohorts` | `dda_sdk/schemas/wishlist_cohorts.py` |
|
|
515
|
+
| `BaselineSchema` | `baseline` | `dda_sdk/schemas/baseline.py` |
|
|
516
|
+
| `ProductsSchema` | `products` | `dda_sdk/schemas/products.py` |
|
|
517
|
+
| `SkusSchema` | `skus` | `dda_sdk/schemas/skus.py` |
|
|
518
|
+
| `CountriesSchema` | `countries` | `dda_sdk/schemas/countries.py` — join to sales tables on `country_code` for geographic breakdowns |
|
|
519
|
+
| `CurrenciesSchema` | `currencies` | `dda_sdk/schemas/currencies.py` — join to `countries` on `currency_code` |
|
|
520
|
+
| `OrganizationsSchema` | `organizations` | `dda_sdk/schemas/organizations.py` |
|
|
521
|
+
| `PortalsSchema` | `portals` | `dda_sdk/schemas/portals.py` — PK: `portal_platform_region`; use `display_portal` for report labels |
|
|
522
|
+
|
|
523
|
+
Import any schema: `from dda_sdk import DuckDBClient, SalesPerSkuSchema, PortalsSchema`
|