chartwright 0.1.0__py3-none-any.whl
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/__init__.py +7 -0
- chartwright/absorb.py +93 -0
- chartwright/apply.py +409 -0
- chartwright/cli.py +222 -0
- chartwright/client.py +227 -0
- chartwright/compiler.py +601 -0
- chartwright/dashdiff.py +208 -0
- chartwright/decompile.py +625 -0
- chartwright/ids.py +21 -0
- chartwright/mcp_server.py +112 -0
- chartwright/profiles.py +143 -0
- chartwright/resolver.py +196 -0
- chartwright/sketch.py +180 -0
- chartwright/smoke.py +124 -0
- chartwright/spec.py +597 -0
- chartwright/testing.py +31 -0
- chartwright-0.1.0.dist-info/METADATA +189 -0
- chartwright-0.1.0.dist-info/RECORD +23 -0
- chartwright-0.1.0.dist-info/WHEEL +5 -0
- chartwright-0.1.0.dist-info/entry_points.txt +3 -0
- chartwright-0.1.0.dist-info/licenses/LICENSE +202 -0
- chartwright-0.1.0.dist-info/licenses/NOTICE +5 -0
- chartwright-0.1.0.dist-info/top_level.txt +1 -0
chartwright/smoke.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Post-import smoke: exercise each chart via POST /api/v1/chart/data with a
|
|
2
|
+
query context constructed from the chart's own spec (imported charts carry no
|
|
3
|
+
saved query_context, so the GET variant won't work).
|
|
4
|
+
|
|
5
|
+
Semantic: data-level sanity: the chart's references are valid on the live
|
|
6
|
+
instance AND a representative query for its fields returns rows. It does not
|
|
7
|
+
replicate each viz's post-processing (binning, pivoting, normalization); that
|
|
8
|
+
stays the renderer's job.
|
|
9
|
+
|
|
10
|
+
Pass = HTTP 200 AND non-empty result payload. Empty = warning naming the
|
|
11
|
+
chart, not a silent pass.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from .client import SupersetClient
|
|
19
|
+
from .compiler import _metric_payload
|
|
20
|
+
from .resolver import Resolution
|
|
21
|
+
from .spec import DashboardSpec
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SmokeResult:
|
|
26
|
+
chart: str
|
|
27
|
+
ok: bool
|
|
28
|
+
warning: bool
|
|
29
|
+
detail: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _filters_payload(chart) -> list[dict]:
|
|
33
|
+
return [{"col": f.column, "op": f.op, "val": f.value} for f in chart.filters]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _time_axis(column: str, grain: str | None) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"columnType": "BASE_AXIS",
|
|
39
|
+
"label": column,
|
|
40
|
+
"sqlExpression": column,
|
|
41
|
+
"expressionType": "SQL",
|
|
42
|
+
"timeGrain": grain or "P1D",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _query_for(chart, spec: DashboardSpec) -> dict:
|
|
47
|
+
slug = spec.dashboard.slug
|
|
48
|
+
q: dict = {
|
|
49
|
+
"filters": _filters_payload(chart),
|
|
50
|
+
"extras": {"time_grain_sqla": getattr(chart, "time_grain", None) or "P1D"},
|
|
51
|
+
"time_range": "No filter",
|
|
52
|
+
"row_limit": getattr(chart, "row_limit", None) or 1000,
|
|
53
|
+
"columns": [],
|
|
54
|
+
"metrics": [],
|
|
55
|
+
"orderby": [],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def metric(m: str):
|
|
59
|
+
return _metric_payload(m, slug, chart.name)
|
|
60
|
+
|
|
61
|
+
t = chart.type
|
|
62
|
+
if t == "big_number_total":
|
|
63
|
+
q["metrics"] = [metric(chart.metric)]
|
|
64
|
+
elif t == "big_number_trend":
|
|
65
|
+
q["metrics"] = [metric(chart.metric)]
|
|
66
|
+
q["columns"] = [_time_axis(chart.time_column, chart.time_grain)]
|
|
67
|
+
elif t in ("timeseries_line", "timeseries_bar", "timeseries_area", "timeseries_scatter"):
|
|
68
|
+
q["metrics"] = [metric(m) for m in chart.metrics]
|
|
69
|
+
q["columns"] = [_time_axis(chart.time_column, chart.time_grain)] + (
|
|
70
|
+
[chart.groupby] if chart.groupby else []
|
|
71
|
+
)
|
|
72
|
+
elif t == "bar":
|
|
73
|
+
q["metrics"] = [metric(m) for m in chart.metrics]
|
|
74
|
+
q["columns"] = [chart.x_column] + ([chart.groupby] if chart.groupby else [])
|
|
75
|
+
elif t == "pie":
|
|
76
|
+
q["metrics"] = [metric(chart.metric)]
|
|
77
|
+
q["columns"] = [chart.groupby]
|
|
78
|
+
elif t == "table":
|
|
79
|
+
if chart.columns:
|
|
80
|
+
q["columns"] = chart.columns
|
|
81
|
+
else:
|
|
82
|
+
q["columns"] = chart.groupby or []
|
|
83
|
+
q["metrics"] = [metric(m) for m in (chart.metrics or [])]
|
|
84
|
+
elif t == "pivot_table":
|
|
85
|
+
q["metrics"] = [metric(m) for m in chart.metrics]
|
|
86
|
+
q["columns"] = [*chart.rows, *chart.columns]
|
|
87
|
+
elif t == "heatmap":
|
|
88
|
+
q["metrics"] = [metric(chart.metric)]
|
|
89
|
+
q["columns"] = [chart.x_column, chart.y_column]
|
|
90
|
+
elif t == "histogram":
|
|
91
|
+
# data sanity: the raw column has values; binning is the renderer's job
|
|
92
|
+
q["columns"] = [chart.column] + ([chart.groupby] if chart.groupby else [])
|
|
93
|
+
elif t == "funnel":
|
|
94
|
+
q["metrics"] = [metric(chart.metric)]
|
|
95
|
+
q["columns"] = [chart.groupby]
|
|
96
|
+
elif t == "treemap":
|
|
97
|
+
q["metrics"] = [metric(chart.metric)]
|
|
98
|
+
q["columns"] = list(chart.groupby)
|
|
99
|
+
return q
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def smoke_chart(chart, spec: DashboardSpec, resolution: Resolution, client: SupersetClient) -> SmokeResult:
|
|
103
|
+
ds = resolution.for_chart(chart.dataset)
|
|
104
|
+
ctx = {
|
|
105
|
+
"datasource": {"id": ds.id, "type": "table"},
|
|
106
|
+
"queries": [_query_for(chart, spec)],
|
|
107
|
+
"result_format": "json",
|
|
108
|
+
"result_type": "full",
|
|
109
|
+
}
|
|
110
|
+
r = client.chart_data(ctx)
|
|
111
|
+
if r.status_code != 200:
|
|
112
|
+
return SmokeResult(chart.name, False, False, f"HTTP {r.status_code}: {r.text[:500]}")
|
|
113
|
+
try:
|
|
114
|
+
result = r.json()["result"]
|
|
115
|
+
rows = sum(len(q.get("data") or []) for q in result)
|
|
116
|
+
except Exception as e: # noqa: BLE001 - malformed body is a failure, whatever the shape
|
|
117
|
+
return SmokeResult(chart.name, False, False, f"unparseable chart/data response: {e}")
|
|
118
|
+
if rows == 0:
|
|
119
|
+
return SmokeResult(chart.name, True, True, "query succeeded but returned 0 rows")
|
|
120
|
+
return SmokeResult(chart.name, True, False, f"{rows} rows")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def smoke(spec: DashboardSpec, resolution: Resolution, client: SupersetClient) -> list[SmokeResult]:
|
|
124
|
+
return [smoke_chart(c, spec, resolution, client) for c in spec.charts]
|