loopable / platform /harness /tables.py
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
bf8519f verified
Raw
History Blame Contribute Delete
5.73 kB
"""harness/tables.py β€” a topic-backed TABLE for the grid (CG-2).
The grid's first table (customers) is built from `modules.customer_data.pool()`: one row per
customer, the whole book shipped as component args, filtered client-side. That works at 1,550
rows and does not work at line grain β€” `sales_lines` is 201,558 rows / ~89 MB.
This module builds the SECOND table: rows come from `semantic.store_rows`, so the fetch is
WINDOWED and the filtering is SQL. Its whole job is to hand the grid a payload in the same
host-neutral shape the React component already speaks, plus the one thing a windowed table must
carry that a whole-book table never needed:
counts = {matched, total, shown, windowed}
`matched` and `total` come from their own queries over the FULL scope; `shown` is the size of
this window. The component renders "showing SHOWN of MATCHED (filtered from TOTAL)" from those
numbers and NEVER from `rows.length` β€” which is the difference between an honest window and a
silent `[:N]` wearing a total ([[no-unverifiable-aggregates]]).
The field CONTRACT is derived from the topic, not hand-written: labels and types come from the
model's dims and metrics, so a new metric cannot arrive as an untyped, unlabelled column and the
grid can never disagree with the semantic layer about what a column means.
"""
from harness import semantic as _sem
#: Topic-backed tables the grid can open. `key` is the storage key its saved views live under β€”
#: separate from the customer table's, so the two tables' views can never collide.
TOPIC_TABLES = {
"sales_lines": {
"label": "Sales lines",
"storage_key": "sales_lines_table_workspace",
# the columns worth defaulting to visible, in order; everything else stays available
"default_visible": ["date", "order_partner", "product", "category", "state", "agent",
"team", "revenue", "units", "margin"],
# a line-grain table is unusable sorted by nothing in particular
"default_sorts": [{"colId": "date", "dir": "desc"}],
},
}
#: how many rows one window carries. Small enough that the payload stays a few hundred KB at
#: ~440 B/row, large enough to scroll. The honest counts make the window visible rather than
#: silent, so this is a performance knob and not a correctness one.
PAGE_ROWS = 200
def _labels(topic):
"""colId -> human label, taken from the MODEL (dim labels, metric labels)."""
t = _sem.topics()[topic]
out = {}
for key, d in ((t.get("store") or {}).get("dims") or {}).items():
out[key] = d.get("label") or key.replace("_", " ").title()
out[f"{key}_id"] = f"{out[key]} id"
out["date"] = "Date"
for k, m in _sem.metrics().items():
if m.get("topic") == topic:
out[k] = m.get("label") or k
return out
def table_fields(topic):
"""The grid field contract for a topic, derived from the semantic model.
Every column is `source='odoo'` β€” read-only. A line is a fact from the ERP; unlike the
customer table there is no overlay stratum here (notes/tags hang off a CUSTOMER, not off an
individual order line), so the two-strata split simply does not arise.
"""
cfg = TOPIC_TABLES[topic]
cols = _sem.store_columns(topic, grain="row")
labels = _labels(topic)
order = [k for k in cfg["default_visible"] if k in cols]
order += [k for k in cols if k not in order]
return [{
"key": k,
"label": labels.get(k, k.replace("_", " ").title()),
"type": cols[k]["type"],
"source": "odoo",
"default": k in cfg["default_visible"],
} for k in order]
def table_payload(topic, config=None, offset=0, limit=None, team_id=None,
date_from=None, date_to=None):
"""Fetch ONE window of a topic-backed table plus its scope-wide counts and totals.
`config` is a grid ViewConfig (filters / filterConj / sorts / search) β€” the same shape the
customer table persists, so a saved view means the same thing on either table.
Returns the component payload: {fields, rows, counts, aggregates, storageKey}.
"""
if topic not in TOPIC_TABLES:
raise _sem.ModelError(f"no topic-backed table registered for {topic!r}")
cfg = TOPIC_TABLES[topic]
config = config or {}
limit = PAGE_ROWS if limit is None else limit
got = _sem.store_rows(
topic,
filter_tree=config.get("filters") or [],
filter_conj=config.get("filterConj") or "and",
search=config.get("search") or None,
sorts=config.get("sorts") or cfg["default_sorts"],
member_ids=config.get("memberPids") or None,
limit=limit, offset=offset,
team_id=team_id, date_from=date_from, date_to=date_to,
)
rows = []
for i, r in enumerate(got["rows"]):
# `pid` is the grid's row identity contract (selection, detail, overlay all key on it).
# At line grain that identity is the order LINE id, which store_rows returns as _rid.
row = {k: v for k, v in r.items() if k != "_rid"}
row["pid"] = r["_rid"]
rows.append(row)
return {
"fields": table_fields(topic),
"rows": rows,
# THE honest window. `matched`/`total` are scope-wide queries; `shown` is this window.
# A caller must never recompute `matched` from len(rows).
"counts": {
"shown": len(rows),
"matched": got["row_count"],
"total": got["total_count"],
"windowed": True,
},
"aggregates": got["aggregates"], # over the whole FILTERED scope, not the window
"storageKey": cfg["storage_key"],
"label": cfg["label"],
"window": got["window"],
}