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/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]