File size: 8,548 Bytes
c14ceee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """harness/views.py β saved views + spawned dashboards: the store and the RE-RUNNER (OM-3 viewer).
The Analyst's save_view/compose_dashboard tools persist chart SPECS + their semantic QUERY (never
rows) into data/store/views.json. This module is the read side: load the specs, re-execute each
view's governed query against the tenant store at render time, and hand the page a fresh artifact
dict in exactly the shape `_render_analyst_artifact` already renders. The viewer therefore shows
LIVE (store-fresh) numbers, not the numbers from whenever the view was saved β same discipline as
every dashboard (a saved view is a query, not a snapshot).
Legacy note: views saved before 2026-07-11 carry no 'query' (only a session-scoped result_id) and
cannot be re-run β run_view raises a readable error the page surfaces per view.
"""
import json
import time
import harness.semantic as SEM
from harness.tools import VIEWS_PATH
_QUERY_KEYS = ("topic", "measures", "group_by", "grain", "date_from", "date_to",
"team_id", "filters", "sort", "limit", "exclude_services")
def load():
if VIEWS_PATH.exists():
return json.loads(VIEWS_PATH.read_text(encoding="utf-8"))
return {"views": {}, "dashboards": {}}
def _save(d):
VIEWS_PATH.parent.mkdir(parents=True, exist_ok=True)
VIEWS_PATH.write_text(json.dumps(d, indent=1), encoding="utf-8")
def views():
return load().get("views", {})
def dashboards():
return load().get("dashboards", {})
def delete_view(name):
d = load()
d.get("views", {}).pop(name, None)
for dash in d.get("dashboards", {}).values():
dash["views"] = [v for v in dash.get("views", []) if v != name]
_save(d)
def delete_dashboard(name):
d = load()
d.get("dashboards", {}).pop(name, None)
_save(d)
# ------------------------------------------------------------------ UPDATE (owner directive
# 2026-07-12: the Analyst can CREATE **and UPDATE** each workbook in the workspace)
_QUERY_PATCH_KEYS = ("measures", "group_by", "grain", "date_from", "date_to", "team_id",
"filters", "sort", "limit", "exclude_services", "topic")
_SPEC_PATCH_KEYS = ("title", "kind", "x", "y", "series", "metric",
"y2", "size", "value", "facet", "columns", "transforms")
def update_view(name, changes):
"""Patch a saved view's chart spec and/or query, VALIDATED BY RE-EXECUTION before saving β
an update that cannot run does not land. `changes` may contain spec keys (title/kind/x/y/
series/metric) and/or a `query` dict of query-key patches (a None value REMOVES the key)."""
d = load()
v = d.get("views", {}).get(name)
if not v:
raise SEM.ModelError(f"unknown view {name!r} (use list_workspace)")
spec = dict(v.get("chart") or {})
q = dict(spec.get("query") or {})
if not q:
raise SEM.ModelError(f"view {name!r} carries no query β rebuild it instead")
changes = dict(changes or {})
qpatch = changes.pop("query", None) or {}
bad = [k for k in changes if k not in _SPEC_PATCH_KEYS]
bad += [k for k in qpatch if k not in _QUERY_PATCH_KEYS]
if bad:
raise SEM.ModelError(f"unknown patch keys {bad} (spec: {list(_SPEC_PATCH_KEYS)}; "
f"query: {list(_QUERY_PATCH_KEYS)})")
for k, val in qpatch.items():
if val is None:
q.pop(k, None)
else:
q[k] = val
res = _run_query(q) # validation: it must RUN
new_spec = {**spec, **changes, "query": q}
rows = res["rows"]
if new_spec.get("transforms"): # the transform chain must replay too
import harness.transforms as TR
rows, _ = TR.apply({**res, "query": q}, new_spec["transforms"], run_query=_run_query)
cols = set(rows[0]) if rows else set()
for ref in ("x", "y", "series", "y2", "size", "value", "facet"):
if new_spec.get(ref) and cols and new_spec[ref] not in cols:
raise SEM.ModelError(f"{ref}={new_spec[ref]!r} not in the patched result columns "
f"{sorted(cols)} β patch {ref} too")
d["views"][name] = {"chart": new_spec,
"saved_at": time.strftime("%Y-%m-%d %H:%M")}
_save(d)
return {"updated": name, "row_count": res["row_count"],
"query": q, "note": "patched query re-ran successfully before saving"}
def update_dashboard(name, views_list=None, new_name=None):
"""Update a workbook (dashboard): recompose its views (order = display order) and/or rename
it. Renames follow through to scheduled reports that reference it."""
d = load()
dash = d.get("dashboards", {}).get(name)
if dash is None:
raise SEM.ModelError(f"unknown dashboard {name!r} (use list_workspace)")
if views_list is not None:
missing = [v for v in views_list if v not in d.get("views", {})]
if missing:
raise SEM.ModelError(f"unknown views {missing} β save_view them first")
dash["views"] = list(views_list)
dash["updated_at"] = time.strftime("%Y-%m-%d %H:%M")
if new_name and new_name != name:
if new_name in d.get("dashboards", {}) or new_name in d.get("views", {}):
raise SEM.ModelError(f"{new_name!r} already exists β pick another name")
d["dashboards"][new_name] = d["dashboards"].pop(name)
try: # follow the rename into schedules
import harness.routines as R
rt = R.load()
if name in rt.get("reports", {}):
rt["reports"][new_name] = rt["reports"].pop(name)
R._save(rt)
except Exception:
pass
name = new_name
_save(d)
return {"dashboard": name, "views": d["dashboards"][name]["views"]}
def _run_query(q):
return SEM.store_query(**{k: q.get(k) for k in _QUERY_KEYS if q.get(k) is not None})
def run_view(name):
"""Re-execute one saved view's governed query β then REPLAY its recorded transform chain β
and return a fresh artifact dict ({'chart': {..., 'rows': [...]}}, {'table': {...}} or
{'kpi': {...}}) for the house renderer."""
v = views().get(name)
if not v:
raise SEM.ModelError(f"unknown view {name!r}")
spec = v.get("chart") or {}
q = spec.get("query")
if not q:
raise SEM.ModelError(f"view {name!r} was saved without its query (pre-2026-07-11) β "
"ask the Analyst to rebuild and re-save it")
res = _run_query(q)
if spec.get("kind") == "kpi" or (spec.get("metric") and spec.get("kind") != "table"):
metric = spec.get("metric") or spec.get("y") # KPI-shaped view
val = (res["rows"][0].get(metric) if res["rows"] else None)
kpi = {"metric": metric, "value": val or 0}
cq = spec.get("compare_query")
if cq:
prev_rows = _run_query(cq)["rows"]
prev = prev_rows[0].get(metric) if prev_rows else None
if prev:
kpi["delta_pct"] = ((val or 0) - prev) / abs(prev)
return {"kpi": kpi}
rows = res["rows"]
if spec.get("transforms"): # a saved view replays its transforms
import harness.transforms as TR
rows, _ = TR.apply({**res, "query": q}, spec["transforms"], run_query=_run_query)
if spec.get("kind") == "table":
return {"table": {**{k: spec.get(k) for k in ("kind", "title", "columns")},
"rows": rows}}
return {"chart": {**{k: spec.get(k) for k in ("kind", "x", "y", "series", "title",
"y2", "size", "value", "facet")},
"rows": rows}}
def run_dashboard(name):
"""All of a dashboard's views, freshly re-queried. Returns [(view_name, artifact|None, err)]."""
dash = dashboards().get(name)
if dash is None:
raise SEM.ModelError(f"unknown dashboard {name!r}")
out = []
for vn in dash.get("views", []):
try:
out.append((vn, run_view(vn), None))
except Exception as e:
out.append((vn, None, str(e)[:200]))
return out
def store_freshness():
"""Newest sync checkpoint across entities β the staleness footer ('data as of β¦')."""
try:
import harness.datastore as DS
ts = [s.get("updated") for s in DS.status().values() if s.get("updated")]
return max(ts) if ts else None
except Exception:
return None
|