chartwright 0.1.0__tar.gz → 0.2.0__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.
- {chartwright-0.1.0/chartwright.egg-info → chartwright-0.2.0}/PKG-INFO +87 -42
- {chartwright-0.1.0 → chartwright-0.2.0}/README.md +85 -40
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/__init__.py +1 -1
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/absorb.py +1 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/apply.py +80 -15
- chartwright-0.2.0/chartwright/cli.py +444 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/client.py +60 -3
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/compiler.py +219 -24
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/dashdiff.py +33 -5
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/decompile.py +316 -24
- chartwright-0.2.0/chartwright/design/__init__.py +129 -0
- chartwright-0.2.0/chartwright/design/brief.py +101 -0
- chartwright-0.2.0/chartwright/design/calibrate.py +139 -0
- chartwright-0.2.0/chartwright/design/fix.py +74 -0
- chartwright-0.2.0/chartwright/design/guidelines/chart-choice.md +31 -0
- chartwright-0.2.0/chartwright/design/guidelines/composition.md +28 -0
- chartwright-0.2.0/chartwright/design/model.py +326 -0
- chartwright-0.2.0/chartwright/design/presets.py +181 -0
- chartwright-0.2.0/chartwright/design/probe.py +61 -0
- chartwright-0.2.0/chartwright/design/redesign.py +53 -0
- chartwright-0.2.0/chartwright/design/rules.py +1097 -0
- chartwright-0.2.0/chartwright/mcp_server.py +288 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/profiles.py +96 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/resolver.py +67 -9
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/sketch.py +10 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/smoke.py +60 -3
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/spec.py +302 -44
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/testing.py +24 -0
- {chartwright-0.1.0 → chartwright-0.2.0/chartwright.egg-info}/PKG-INFO +87 -42
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/SOURCES.txt +33 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/requires.txt +1 -1
- {chartwright-0.1.0 → chartwright-0.2.0}/pyproject.toml +6 -3
- chartwright-0.2.0/tests/test_apply_rollback.py +119 -0
- chartwright-0.2.0/tests/test_calibrate.py +108 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_client.py +13 -0
- chartwright-0.2.0/tests/test_column_suggestions.py +64 -0
- chartwright-0.2.0/tests/test_cross_filters.py +84 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_decompile.py +1 -1
- chartwright-0.2.0/tests/test_design.py +471 -0
- chartwright-0.2.0/tests/test_design_arch.py +221 -0
- chartwright-0.2.0/tests/test_design_data.py +140 -0
- chartwright-0.2.0/tests/test_design_gate.py +64 -0
- chartwright-0.2.0/tests/test_design_rules_v2.py +213 -0
- chartwright-0.2.0/tests/test_docs.py +120 -0
- chartwright-0.2.0/tests/test_layout_footer.py +137 -0
- chartwright-0.2.0/tests/test_markdown_heights.py +59 -0
- chartwright-0.2.0/tests/test_mcp_server.py +162 -0
- chartwright-0.2.0/tests/test_mixed_chart.py +233 -0
- chartwright-0.2.0/tests/test_nested_tabs.py +101 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_pivot_formatting.py +50 -8
- chartwright-0.2.0/tests/test_plan_errors.py +56 -0
- chartwright-0.2.0/tests/test_preset_auth.py +179 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_range_filter.py +67 -1
- chartwright-0.2.0/tests/test_redesign.py +67 -0
- chartwright-0.2.0/tests/test_review_tier_1_2.py +214 -0
- chartwright-0.2.0/tests/test_select_filter.py +133 -0
- chartwright-0.2.0/tests/test_smoke_fit.py +78 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_spec_v2.py +1 -1
- chartwright-0.2.0/tests/test_table_formatting.py +169 -0
- chartwright-0.2.0/tests/test_table_sort.py +147 -0
- chartwright-0.1.0/chartwright/cli.py +0 -222
- chartwright-0.1.0/chartwright/mcp_server.py +0 -112
- chartwright-0.1.0/tests/test_mcp_server.py +0 -54
- {chartwright-0.1.0 → chartwright-0.2.0}/LICENSE +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/NOTICE +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/ids.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/dependency_links.txt +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/entry_points.txt +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/top_level.txt +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/setup.cfg +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_absorb.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_backup_layout.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_compiler.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_dashdiff.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_params_contract.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_profiles.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_property_roundtrip.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_sketch.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_spec.py +0 -0
- {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_time_filter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chartwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Build, verify, and maintain Apache Superset dashboards from small spec files.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/debabsah/chartwright
|
|
@@ -24,14 +24,16 @@ Requires-Dist: pyyaml>=6.0
|
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
26
26
|
Provides-Extra: mcp
|
|
27
|
-
Requires-Dist: mcp
|
|
27
|
+
Requires-Dist: mcp<3,>=1.0; extra == "mcp"
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
# Chartwright
|
|
31
31
|
|
|
32
|
-
A dashboard compiler for Apache Superset.
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
A dashboard compiler for Apache Superset. The `chartwright` command line builds
|
|
33
|
+
a dashboard from a small deterministic file, updates it from that file later,
|
|
34
|
+
and decompiles an existing dashboard back into one. Every dataset, column, and
|
|
35
|
+
metric is checked against your own Superset first. An AI can write the file for
|
|
36
|
+
you, or revise it, from a request in plain words.
|
|
35
37
|
|
|
36
38
|

|
|
37
39
|
|
|
@@ -41,14 +43,20 @@ charts, three filters, one `chartwright apply`. An AI wrote
|
|
|
41
43
|
[the request and rebuild steps](https://github.com/debabsah/chartwright/blob/main/examples/README.md).*
|
|
42
44
|
|
|
43
45
|
A dashboard that lives in a file gets the workflow code already has: review it
|
|
44
|
-
in a pull request, rebuild it identically,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
in a pull request, rebuild it identically, and bring it back after a bad
|
|
47
|
+
change. `chartwright plan` diffs the file against the live dashboard and exits
|
|
48
|
+
non-zero when they differ, so drift fails a CI check. `chartwright decompile`
|
|
49
|
+
covers the other direction, and lists anything it could not carry over.
|
|
50
|
+
|
|
51
|
+
Write the file yourself, or describe the dashboard you want and have an AI
|
|
52
|
+
write it. The same checks run either way. After the build, each chart's query
|
|
53
|
+
runs once (an error fails the build, an empty chart is named), and
|
|
54
|
+
`chartwright advise` reviews the layout for readability.
|
|
49
55
|
|
|
50
56
|
## What a spec looks like
|
|
51
57
|
|
|
58
|
+
The file is called a spec:
|
|
59
|
+
|
|
52
60
|
```json
|
|
53
61
|
{
|
|
54
62
|
"spec_version": "1",
|
|
@@ -75,8 +83,24 @@ after.
|
|
|
75
83
|
`chartwright apply` on this file signs in to your Superset, confirms the
|
|
76
84
|
`cleaned_sales_data` table and the `sales` and `order_date` columns exist,
|
|
77
85
|
builds a KPI card above a line chart, and finishes by running each chart's
|
|
78
|
-
query once
|
|
79
|
-
|
|
86
|
+
query once. A misspelled column or a missing table is a clear error naming the
|
|
87
|
+
problem, with the closest real names suggested, before anything is created.
|
|
88
|
+
|
|
89
|
+
## Creating dashboards with AI
|
|
90
|
+
|
|
91
|
+
The spec is typed, and every reference in it is checked before anything is
|
|
92
|
+
created. Whatever a model proposes has to survive the same verification your
|
|
93
|
+
own specs do.
|
|
94
|
+
|
|
95
|
+
- **Claude Code skill**: from a clone of this repo, run
|
|
96
|
+
`python install-skill.py`; the model writes the spec from your request, and
|
|
97
|
+
the tool verifies and builds it.
|
|
98
|
+
- **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
|
|
99
|
+
exposes ten tools covering the whole lifecycle, usable from any MCP client.
|
|
100
|
+
- **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
|
|
101
|
+
tool can generate valid specs.
|
|
102
|
+
- **Guardrails**: the AI proposes; the tool verifies, using your own Superset
|
|
103
|
+
login. Verification reads names (datasets, columns, metrics), not rows.
|
|
80
104
|
|
|
81
105
|
## Quick start
|
|
82
106
|
|
|
@@ -113,35 +137,47 @@ as-is.
|
|
|
113
137
|
- **Deterministic dashboards**: the same spec always produces the identical
|
|
114
138
|
dashboard. Diff it in git, review it in a PR.
|
|
115
139
|
- **Verified at every step**: every reference is checked before anything is
|
|
116
|
-
written, and every chart's query runs once
|
|
117
|
-
say what went wrong and where.
|
|
140
|
+
written, and every chart's query runs once (an error fails the build, an
|
|
141
|
+
empty chart is named); failures say what went wrong and where.
|
|
118
142
|
- **Dashboards as code, in both directions**: `chartwright decompile` turns any live
|
|
119
143
|
dashboard into a spec; `chartwright plan` shows what differs between the spec and
|
|
120
144
|
the live dashboard, ready as a CI gate; `chartwright compile` builds the import
|
|
121
145
|
bundle offline, no server needed.
|
|
122
|
-
- **
|
|
123
|
-
apply
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
146
|
+
- **Recoverable by default**: every apply backs up the previous state first,
|
|
147
|
+
and an apply that fails while importing or updating charts restores it
|
|
148
|
+
automatically. Dashboards Chartwright did not create are never overwritten;
|
|
149
|
+
to bring a hand-built one under a spec, decompile it and build it at a new
|
|
150
|
+
slug.
|
|
151
|
+
- **What a spec can express**: 15 chart types, metrics as you write them,
|
|
152
|
+
per-chart filters, a native filter bar, tabs, markdown notes, a footer shown
|
|
153
|
+
under every tab, and layouts you can draw as ASCII sketches.
|
|
129
154
|
- **Environment promotion**: specs name their data (connection, schema,
|
|
130
|
-
table), so the same file applies to dev, staging, and production
|
|
155
|
+
table), so the same file applies to dev, staging, and production when they
|
|
156
|
+
share connection names.
|
|
131
157
|
|
|
132
158
|
Every capability, with the CLI verb reference: [docs/FEATURES.md](https://github.com/debabsah/chartwright/blob/main/docs/FEATURES.md).
|
|
133
159
|
|
|
134
|
-
##
|
|
160
|
+
## The design brain
|
|
135
161
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
A spec can import perfectly and still produce a dashboard nobody can read: a
|
|
163
|
+
pie squeezed into two columns, a KPI buried under three tables, a bar chart
|
|
164
|
+
with forty category labels Superset silently drops. The design brain covers
|
|
165
|
+
size and scroll budgets, chart-choice limits, and layout composition. It
|
|
166
|
+
arrives as a brief the AI reads before authoring, and a critic that reviews
|
|
167
|
+
the result:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
chartwright brief --audience executive # the guidance, before writing a spec
|
|
171
|
+
chartwright advise spec.json --fix # the critique, with safe auto-repairs
|
|
172
|
+
chartwright redesign old-dash --profile prod # audit + repair a live dashboard
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
You stay in charge of it: suppress any rule for one chart in the spec, tune
|
|
176
|
+
the thresholds per audience or per deployment in `design.yaml`, and feed the
|
|
177
|
+
heights you drag in the UI back into the recommendations with `chartwright
|
|
178
|
+
calibrate`. Pass `--design off` and the output is byte-identical to a build
|
|
179
|
+
that never had it. The full rulebook and architecture:
|
|
180
|
+
[docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md).
|
|
145
181
|
|
|
146
182
|
## Drawing layouts as text
|
|
147
183
|
|
|
@@ -162,15 +198,19 @@ space under `K` deliberately empty. Every rule, drawn and explained:
|
|
|
162
198
|
|
|
163
199
|
## Testing and evidence
|
|
164
200
|
|
|
165
|
-
Every
|
|
166
|
-
lifecycle soak, stale-tab adversary,
|
|
167
|
-
4.1.4, 5.0.0, and 6.1.0 containers.
|
|
168
|
-
Superset's own source for every supported
|
|
169
|
-
|
|
201
|
+
Every pull request and every push to main runs the full offline suite on Linux and
|
|
202
|
+
Windows, plus the full pipeline (apply, lifecycle soak, stale-tab adversary,
|
|
203
|
+
fault injection) against real Superset 4.1.4, 5.0.0, and 6.1.0 containers.
|
|
204
|
+
Chart options are checked against Superset's own source for every supported
|
|
205
|
+
version, so a Superset change surfaces here before it reaches your dashboards.
|
|
206
|
+
|
|
207
|
+
Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md).
|
|
170
208
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
209
|
+
[docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md) records how Superset itself behaves: what its
|
|
210
|
+
importer accepts, how dashboard settings are stored, what its chart plugins
|
|
211
|
+
expect. Every behavior is cited to its source line at 4.1.4, 5.0.0, and 6.1.0.
|
|
212
|
+
Release tags are immutable, so any line of it can be checked against a fresh
|
|
213
|
+
checkout of that tag.
|
|
174
214
|
|
|
175
215
|
## Documentation
|
|
176
216
|
|
|
@@ -178,10 +218,15 @@ for every Superset behavior the tool relies on:
|
|
|
178
218
|
reference
|
|
179
219
|
- [docs/LAYOUT-GUIDE.md](https://github.com/debabsah/chartwright/blob/main/docs/LAYOUT-GUIDE.md): drawing layouts as text,
|
|
180
220
|
every rule illustrated
|
|
221
|
+
- [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md): the design rulebook, every
|
|
222
|
+
rule and threshold, and how to tune or switch them off
|
|
181
223
|
- [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md): what is tested, what it
|
|
182
224
|
caught, and how to reproduce it
|
|
183
|
-
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md):
|
|
184
|
-
|
|
225
|
+
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): how Superset behaves on import,
|
|
226
|
+
on dashboard storage, and in chart options, cited to its source line at
|
|
227
|
+
each supported release
|
|
228
|
+
- [docs/COMPARISON.md](https://github.com/debabsah/chartwright/blob/main/docs/COMPARISON.md): what Chartwright, preset-cli,
|
|
229
|
+
sup, and the Terraform provider each automate, so you can pick by the job
|
|
185
230
|
|
|
186
231
|
---
|
|
187
232
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Chartwright
|
|
2
2
|
|
|
3
|
-
A dashboard compiler for Apache Superset.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A dashboard compiler for Apache Superset. The `chartwright` command line builds
|
|
4
|
+
a dashboard from a small deterministic file, updates it from that file later,
|
|
5
|
+
and decompiles an existing dashboard back into one. Every dataset, column, and
|
|
6
|
+
metric is checked against your own Superset first. An AI can write the file for
|
|
7
|
+
you, or revise it, from a request in plain words.
|
|
6
8
|
|
|
7
9
|

|
|
8
10
|
|
|
@@ -12,14 +14,20 @@ charts, three filters, one `chartwright apply`. An AI wrote
|
|
|
12
14
|
[the request and rebuild steps](https://github.com/debabsah/chartwright/blob/main/examples/README.md).*
|
|
13
15
|
|
|
14
16
|
A dashboard that lives in a file gets the workflow code already has: review it
|
|
15
|
-
in a pull request, rebuild it identically,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
in a pull request, rebuild it identically, and bring it back after a bad
|
|
18
|
+
change. `chartwright plan` diffs the file against the live dashboard and exits
|
|
19
|
+
non-zero when they differ, so drift fails a CI check. `chartwright decompile`
|
|
20
|
+
covers the other direction, and lists anything it could not carry over.
|
|
21
|
+
|
|
22
|
+
Write the file yourself, or describe the dashboard you want and have an AI
|
|
23
|
+
write it. The same checks run either way. After the build, each chart's query
|
|
24
|
+
runs once (an error fails the build, an empty chart is named), and
|
|
25
|
+
`chartwright advise` reviews the layout for readability.
|
|
20
26
|
|
|
21
27
|
## What a spec looks like
|
|
22
28
|
|
|
29
|
+
The file is called a spec:
|
|
30
|
+
|
|
23
31
|
```json
|
|
24
32
|
{
|
|
25
33
|
"spec_version": "1",
|
|
@@ -46,8 +54,24 @@ after.
|
|
|
46
54
|
`chartwright apply` on this file signs in to your Superset, confirms the
|
|
47
55
|
`cleaned_sales_data` table and the `sales` and `order_date` columns exist,
|
|
48
56
|
builds a KPI card above a line chart, and finishes by running each chart's
|
|
49
|
-
query once
|
|
50
|
-
|
|
57
|
+
query once. A misspelled column or a missing table is a clear error naming the
|
|
58
|
+
problem, with the closest real names suggested, before anything is created.
|
|
59
|
+
|
|
60
|
+
## Creating dashboards with AI
|
|
61
|
+
|
|
62
|
+
The spec is typed, and every reference in it is checked before anything is
|
|
63
|
+
created. Whatever a model proposes has to survive the same verification your
|
|
64
|
+
own specs do.
|
|
65
|
+
|
|
66
|
+
- **Claude Code skill**: from a clone of this repo, run
|
|
67
|
+
`python install-skill.py`; the model writes the spec from your request, and
|
|
68
|
+
the tool verifies and builds it.
|
|
69
|
+
- **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
|
|
70
|
+
exposes ten tools covering the whole lifecycle, usable from any MCP client.
|
|
71
|
+
- **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
|
|
72
|
+
tool can generate valid specs.
|
|
73
|
+
- **Guardrails**: the AI proposes; the tool verifies, using your own Superset
|
|
74
|
+
login. Verification reads names (datasets, columns, metrics), not rows.
|
|
51
75
|
|
|
52
76
|
## Quick start
|
|
53
77
|
|
|
@@ -84,35 +108,47 @@ as-is.
|
|
|
84
108
|
- **Deterministic dashboards**: the same spec always produces the identical
|
|
85
109
|
dashboard. Diff it in git, review it in a PR.
|
|
86
110
|
- **Verified at every step**: every reference is checked before anything is
|
|
87
|
-
written, and every chart's query runs once
|
|
88
|
-
say what went wrong and where.
|
|
111
|
+
written, and every chart's query runs once (an error fails the build, an
|
|
112
|
+
empty chart is named); failures say what went wrong and where.
|
|
89
113
|
- **Dashboards as code, in both directions**: `chartwright decompile` turns any live
|
|
90
114
|
dashboard into a spec; `chartwright plan` shows what differs between the spec and
|
|
91
115
|
the live dashboard, ready as a CI gate; `chartwright compile` builds the import
|
|
92
116
|
bundle offline, no server needed.
|
|
93
|
-
- **
|
|
94
|
-
apply
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
- **Recoverable by default**: every apply backs up the previous state first,
|
|
118
|
+
and an apply that fails while importing or updating charts restores it
|
|
119
|
+
automatically. Dashboards Chartwright did not create are never overwritten;
|
|
120
|
+
to bring a hand-built one under a spec, decompile it and build it at a new
|
|
121
|
+
slug.
|
|
122
|
+
- **What a spec can express**: 15 chart types, metrics as you write them,
|
|
123
|
+
per-chart filters, a native filter bar, tabs, markdown notes, a footer shown
|
|
124
|
+
under every tab, and layouts you can draw as ASCII sketches.
|
|
100
125
|
- **Environment promotion**: specs name their data (connection, schema,
|
|
101
|
-
table), so the same file applies to dev, staging, and production
|
|
126
|
+
table), so the same file applies to dev, staging, and production when they
|
|
127
|
+
share connection names.
|
|
102
128
|
|
|
103
129
|
Every capability, with the CLI verb reference: [docs/FEATURES.md](https://github.com/debabsah/chartwright/blob/main/docs/FEATURES.md).
|
|
104
130
|
|
|
105
|
-
##
|
|
131
|
+
## The design brain
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
A spec can import perfectly and still produce a dashboard nobody can read: a
|
|
134
|
+
pie squeezed into two columns, a KPI buried under three tables, a bar chart
|
|
135
|
+
with forty category labels Superset silently drops. The design brain covers
|
|
136
|
+
size and scroll budgets, chart-choice limits, and layout composition. It
|
|
137
|
+
arrives as a brief the AI reads before authoring, and a critic that reviews
|
|
138
|
+
the result:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
chartwright brief --audience executive # the guidance, before writing a spec
|
|
142
|
+
chartwright advise spec.json --fix # the critique, with safe auto-repairs
|
|
143
|
+
chartwright redesign old-dash --profile prod # audit + repair a live dashboard
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
You stay in charge of it: suppress any rule for one chart in the spec, tune
|
|
147
|
+
the thresholds per audience or per deployment in `design.yaml`, and feed the
|
|
148
|
+
heights you drag in the UI back into the recommendations with `chartwright
|
|
149
|
+
calibrate`. Pass `--design off` and the output is byte-identical to a build
|
|
150
|
+
that never had it. The full rulebook and architecture:
|
|
151
|
+
[docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md).
|
|
116
152
|
|
|
117
153
|
## Drawing layouts as text
|
|
118
154
|
|
|
@@ -133,15 +169,19 @@ space under `K` deliberately empty. Every rule, drawn and explained:
|
|
|
133
169
|
|
|
134
170
|
## Testing and evidence
|
|
135
171
|
|
|
136
|
-
Every
|
|
137
|
-
lifecycle soak, stale-tab adversary,
|
|
138
|
-
4.1.4, 5.0.0, and 6.1.0 containers.
|
|
139
|
-
Superset's own source for every supported
|
|
140
|
-
|
|
172
|
+
Every pull request and every push to main runs the full offline suite on Linux and
|
|
173
|
+
Windows, plus the full pipeline (apply, lifecycle soak, stale-tab adversary,
|
|
174
|
+
fault injection) against real Superset 4.1.4, 5.0.0, and 6.1.0 containers.
|
|
175
|
+
Chart options are checked against Superset's own source for every supported
|
|
176
|
+
version, so a Superset change surfaces here before it reaches your dashboards.
|
|
177
|
+
|
|
178
|
+
Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md).
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
180
|
+
[docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md) records how Superset itself behaves: what its
|
|
181
|
+
importer accepts, how dashboard settings are stored, what its chart plugins
|
|
182
|
+
expect. Every behavior is cited to its source line at 4.1.4, 5.0.0, and 6.1.0.
|
|
183
|
+
Release tags are immutable, so any line of it can be checked against a fresh
|
|
184
|
+
checkout of that tag.
|
|
145
185
|
|
|
146
186
|
## Documentation
|
|
147
187
|
|
|
@@ -149,10 +189,15 @@ for every Superset behavior the tool relies on:
|
|
|
149
189
|
reference
|
|
150
190
|
- [docs/LAYOUT-GUIDE.md](https://github.com/debabsah/chartwright/blob/main/docs/LAYOUT-GUIDE.md): drawing layouts as text,
|
|
151
191
|
every rule illustrated
|
|
192
|
+
- [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md): the design rulebook, every
|
|
193
|
+
rule and threshold, and how to tune or switch them off
|
|
152
194
|
- [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md): what is tested, what it
|
|
153
195
|
caught, and how to reproduce it
|
|
154
|
-
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md):
|
|
155
|
-
|
|
196
|
+
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): how Superset behaves on import,
|
|
197
|
+
on dashboard storage, and in chart options, cited to its source line at
|
|
198
|
+
each supported release
|
|
199
|
+
- [docs/COMPARISON.md](https://github.com/debabsah/chartwright/blob/main/docs/COMPARISON.md): what Chartwright, preset-cli,
|
|
200
|
+
sup, and the Terraform provider each automate, so you can pick by the job
|
|
156
201
|
|
|
157
202
|
---
|
|
158
203
|
|
|
@@ -151,9 +151,14 @@ def _roundtrip_dataset_files(resolution: Resolution, client: SupersetClient) ->
|
|
|
151
151
|
return extra
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
def chart_payloads_from_bundle(bundle: bytes) -> dict[str, dict]:
|
|
154
|
+
def chart_payloads_from_bundle(bundle: bytes, dataset_ids: dict[str, int] | None = None) -> dict[str, dict]:
|
|
155
155
|
"""uuid -> ChartRestApi.put payload, from the compiled bundle's chart yamls.
|
|
156
156
|
|
|
157
|
+
With ``dataset_ids`` (dataset uuid -> id on the target), the payload also moves the
|
|
158
|
+
chart onto its spec dataset: params alone carry the new datasource, but the chart's
|
|
159
|
+
own datasource_id would stay on the old one (observed live 2026-09-25: a chart moved
|
|
160
|
+
from dataset 5 to 10 kept datasource_id 5).
|
|
161
|
+
|
|
157
162
|
Used to update pre-existing owned charts IN PLACE. Slice ids must stay
|
|
158
163
|
stable across re-applies: delete+reimport mints new ids, which invalidates
|
|
159
164
|
native-filter scopes the moment any open browser tab writes its (stale,
|
|
@@ -164,24 +169,48 @@ def chart_payloads_from_bundle(bundle: bytes) -> dict[str, dict]:
|
|
|
164
169
|
for n in zf.namelist():
|
|
165
170
|
if "/charts/" in n and n.endswith(".yaml"):
|
|
166
171
|
cy = yaml.safe_load(zf.read(n))
|
|
167
|
-
|
|
172
|
+
payload = {
|
|
168
173
|
"slice_name": cy.get("slice_name"),
|
|
169
174
|
"viz_type": cy.get("viz_type"),
|
|
170
175
|
"params": json.dumps(cy.get("params") or {}),
|
|
171
176
|
# params changed -> any stored query context is stale
|
|
172
177
|
"query_context": None,
|
|
173
178
|
}
|
|
179
|
+
ds_id = (dataset_ids or {}).get(str(cy.get("dataset_uuid")))
|
|
180
|
+
if ds_id is not None:
|
|
181
|
+
payload["datasource_id"] = ds_id
|
|
182
|
+
payload["datasource_type"] = "table"
|
|
183
|
+
out[str(cy.get("uuid"))] = payload
|
|
174
184
|
return out
|
|
175
185
|
|
|
176
186
|
|
|
187
|
+
def bundle_dataset_ids(bundle: bytes, client: SupersetClient) -> dict[str, int]:
|
|
188
|
+
"""Dataset uuid -> id on the target, for each dataset the bundle ships. Looked
|
|
189
|
+
up per table name (indexed server-side) and matched by uuid, as charts are."""
|
|
190
|
+
zf = zipfile.ZipFile(io.BytesIO(bundle))
|
|
191
|
+
wanted: dict[str, str] = {}
|
|
192
|
+
for n in zf.namelist():
|
|
193
|
+
if "/datasets/" in n and n.endswith(".yaml"):
|
|
194
|
+
dy = yaml.safe_load(zf.read(n)) or {}
|
|
195
|
+
if dy.get("uuid") and dy.get("table_name"):
|
|
196
|
+
wanted[str(dy["uuid"])] = dy["table_name"]
|
|
197
|
+
found: dict[str, int] = {}
|
|
198
|
+
for table in set(wanted.values()):
|
|
199
|
+
for d in client.find_datasets(table):
|
|
200
|
+
if str(d.get("uuid")) in wanted:
|
|
201
|
+
found[str(d["uuid"])] = d["id"]
|
|
202
|
+
return found
|
|
203
|
+
|
|
204
|
+
|
|
177
205
|
def _update_owned_charts_in_place(
|
|
178
206
|
spec: DashboardSpec, client: SupersetClient, bundle: bytes,
|
|
179
|
-
existing_by_uuid: dict[str, dict],
|
|
207
|
+
existing_by_uuid: dict[str, dict], resolution: Resolution | None = None,
|
|
180
208
|
) -> tuple[list[str], list[str]]:
|
|
181
|
-
"""PUT compiled params onto pre-existing owned charts (ids
|
|
182
|
-
Returns (updated chart names, errors)."""
|
|
209
|
+
"""PUT compiled params (and the spec dataset) onto pre-existing owned charts (ids
|
|
210
|
+
stay stable). Returns (updated chart names, errors)."""
|
|
183
211
|
owned = {str(ids.chart_uuid(spec.dashboard.slug, c.name)): c.name for c in spec.charts}
|
|
184
|
-
|
|
212
|
+
dataset_ids = {str(d.uuid): d.id for d in resolution.datasets.values()} if resolution else None
|
|
213
|
+
payloads = chart_payloads_from_bundle(bundle, dataset_ids)
|
|
185
214
|
updated, errors = [], []
|
|
186
215
|
for u, summary in existing_by_uuid.items():
|
|
187
216
|
payload = payloads.get(u)
|
|
@@ -209,6 +238,26 @@ def backup_dir_for(profile: str, slug: str) -> "Path":
|
|
|
209
238
|
return base / profile / slug
|
|
210
239
|
|
|
211
240
|
|
|
241
|
+
def write_backup(backup_dir: "Path", data: bytes) -> "Path":
|
|
242
|
+
"""Write a backup zip under a new name; never overwrite an earlier one.
|
|
243
|
+
|
|
244
|
+
Names are local time to the microsecond (``20261003T141502.123456.zip``),
|
|
245
|
+
fixed width, so they sort oldest to newest. Two applies within one second
|
|
246
|
+
used to share a name and the second overwrote the first. Exclusive create
|
|
247
|
+
makes a collision a retry, never a silent overwrite."""
|
|
248
|
+
import datetime
|
|
249
|
+
|
|
250
|
+
while True:
|
|
251
|
+
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S.%f")
|
|
252
|
+
path = backup_dir / f"{stamp}.zip"
|
|
253
|
+
try:
|
|
254
|
+
with open(path, "xb") as fh:
|
|
255
|
+
fh.write(data)
|
|
256
|
+
return path
|
|
257
|
+
except FileExistsError:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
|
|
212
261
|
def restore_bundle(zip_bytes: bytes, slug: str, client: SupersetClient) -> ApplyReport:
|
|
213
262
|
"""Restore a backup bundle COMPLETELY, not just import it. The importer
|
|
214
263
|
never overwrites existing charts (docs/CONTRACTS.md), so surviving
|
|
@@ -223,7 +272,15 @@ def restore_bundle(zip_bytes: bytes, slug: str, client: SupersetClient) -> Apply
|
|
|
223
272
|
report.import_detail = r.text[:2000]
|
|
224
273
|
return report
|
|
225
274
|
|
|
226
|
-
|
|
275
|
+
# With the dataset ids, a restore also moves each chart back onto its
|
|
276
|
+
# backed-up dataset, so its datasource_id matches the params it gets.
|
|
277
|
+
# Best effort: a failed lookup must not stop the restore itself.
|
|
278
|
+
try:
|
|
279
|
+
dataset_ids = bundle_dataset_ids(zip_bytes, client)
|
|
280
|
+
except SupersetAPIError as e:
|
|
281
|
+
dataset_ids = {}
|
|
282
|
+
report.warnings.append(f"chart datasets not restored (dataset lookup failed: {e})")
|
|
283
|
+
payloads = chart_payloads_from_bundle(zip_bytes, dataset_ids)
|
|
227
284
|
existing = client.charts_by_uuids(
|
|
228
285
|
{u: p["slice_name"] for u, p in payloads.items()})
|
|
229
286
|
restored = []
|
|
@@ -284,7 +341,6 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
|
|
|
284
341
|
if existing is not None:
|
|
285
342
|
# Last-known-good insurance before we mutate anything: the previous
|
|
286
343
|
# owned state, restorable with `chartwright restore <zip> --profile ...`.
|
|
287
|
-
import datetime
|
|
288
344
|
import os
|
|
289
345
|
|
|
290
346
|
backup_dir = backup_dir_for(profile, spec.dashboard.slug)
|
|
@@ -293,11 +349,8 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
|
|
|
293
349
|
# Zips hold dashboard/dataset metadata; gate the default tree to
|
|
294
350
|
# the owner. (No-op on Windows; custom dirs are the user's to manage.)
|
|
295
351
|
os.chmod(backup_dir.parent.parent, 0o700)
|
|
296
|
-
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
297
|
-
backup_path = backup_dir / f"{stamp}.zip"
|
|
298
352
|
backup_bytes = client.export_dashboard(existing["id"])
|
|
299
|
-
|
|
300
|
-
report.backup = str(backup_path)
|
|
353
|
+
report.backup = str(write_backup(backup_dir, backup_bytes))
|
|
301
354
|
|
|
302
355
|
def _auto_restore(reason: str) -> None:
|
|
303
356
|
"""Import failed mid-mutation: put the previous state back rather
|
|
@@ -312,11 +365,11 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
|
|
|
312
365
|
else:
|
|
313
366
|
report.warnings.append(
|
|
314
367
|
f"{reason}; auto-restore also failed ({rr.import_detail}); "
|
|
315
|
-
f"restore manually: chartwright restore {report.backup}"
|
|
368
|
+
f"restore manually: chartwright restore {report.backup} --profile {profile}"
|
|
316
369
|
)
|
|
317
370
|
except Exception as e: # noqa: BLE001 - restore is best-effort recovery
|
|
318
371
|
report.warnings.append(
|
|
319
|
-
f"{reason}; auto-restore errored ({e}); restore manually: chartwright restore {report.backup}"
|
|
372
|
+
f"{reason}; auto-restore errored ({e}); restore manually: chartwright restore {report.backup} --profile {profile}"
|
|
320
373
|
)
|
|
321
374
|
|
|
322
375
|
# Everything below mutates the instance; any failure must still return a
|
|
@@ -359,9 +412,14 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
|
|
|
359
412
|
return report
|
|
360
413
|
|
|
361
414
|
if existing_by_uuid:
|
|
362
|
-
updated, update_errors = _update_owned_charts_in_place(
|
|
415
|
+
updated, update_errors = _update_owned_charts_in_place(
|
|
416
|
+
spec, client, bundle, existing_by_uuid, resolution)
|
|
363
417
|
if update_errors:
|
|
418
|
+
# Still the import step: some charts may carry the new params
|
|
419
|
+
# and others the old. Same outcome as a connection drop here,
|
|
420
|
+
# which already restored (SupersetAPIError while stage is import).
|
|
364
421
|
report.import_detail = "; ".join(update_errors)
|
|
422
|
+
_auto_restore("updating charts in place failed after the import")
|
|
365
423
|
return report
|
|
366
424
|
if updated:
|
|
367
425
|
report.warnings.append(
|
|
@@ -403,6 +461,13 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
|
|
|
403
461
|
if report.stage in ("prepare", "import"):
|
|
404
462
|
_auto_restore(f"apply failed during {report.stage}: {e}")
|
|
405
463
|
return report
|
|
464
|
+
except Exception as e: # noqa: BLE001 - a bug mid-mutation must still restore and report
|
|
465
|
+
# Not a Superset error: most likely a bug here. Same rule as above, and
|
|
466
|
+
# the report keeps the backup path, which a traceback would lose.
|
|
467
|
+
report.import_detail = f"unexpected error during {report.stage}: {type(e).__name__}: {e}"
|
|
468
|
+
if report.stage in ("prepare", "import"):
|
|
469
|
+
_auto_restore(f"apply failed during {report.stage}: {type(e).__name__}: {e}")
|
|
470
|
+
return report
|
|
406
471
|
|
|
407
472
|
report.stage = "done"
|
|
408
473
|
report.ok = True
|