lql-cli 0.6.0__tar.gz → 0.8.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.
- {lql_cli-0.6.0 → lql_cli-0.8.0}/PKG-INFO +13 -1
- {lql_cli-0.6.0 → lql_cli-0.8.0}/README.md +12 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/pyproject.toml +1 -1
- lql_cli-0.8.0/src/lql/__init__.py +1 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/datasets.py +72 -3
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/instructions.py +20 -4
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/preview.py +93 -7
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/workspaces.py +64 -3
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/output.py +21 -1
- {lql_cli-0.6.0 → lql_cli-0.8.0}/uv.lock +1 -1
- lql_cli-0.6.0/src/lql/__init__.py +0 -1
- {lql_cli-0.6.0 → lql_cli-0.8.0}/.claude/settings.local.json +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/.gitignore +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/examples/agent-traces.jsonl +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/package-lock.json +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/_group.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/_opts.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/api.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/cli.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/__init__.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/annotations.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/auth.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/buckets.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/edits.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/evals.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/highlights.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/issues.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/reports.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/skills.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/spec.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/tui.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/commands/update.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/config.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/sessions.py +0 -0
- {lql_cli-0.6.0 → lql_cli-0.8.0}/src/lql/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lql-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: lql — CLI for the Liquid DataViewer platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/Liquid4All/lql
|
|
6
6
|
Author: Liquid AI
|
|
@@ -166,11 +166,23 @@ lql preview <file.jsonl|file.json> Local file: each line/object is a row
|
|
|
166
166
|
lql preview <dataset-id> Platform dataset (fetched & paged lazily)
|
|
167
167
|
lql preview <org/name> --hf HuggingFace repo: sync to DataViewer, then view
|
|
168
168
|
lql preview <src> -c <field> Force field(s) as conversations (repeatable)
|
|
169
|
+
lql preview <src> -f "col=value" Filter rows (repeatable, AND); local & platform
|
|
169
170
|
lql preview <src> -n <N> Page size when paging a platform dataset
|
|
170
171
|
lql preview <src> --offset N Start at row index N
|
|
171
172
|
lql preview <src> --title "<title>" Title shown in the viewer header
|
|
172
173
|
```
|
|
173
174
|
|
|
175
|
+
**Filtering (`--filter`/`-f`).** Show only matching rows — works on local files and
|
|
176
|
+
platform datasets (platform filtering runs server-side). Repeatable; filters AND
|
|
177
|
+
together; string match is case-insensitive. Operators: `=`, `!=`, `~` (contains),
|
|
178
|
+
`>`, `<`, `>=`, `<=`.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
lql preview <dataset-id> -f "domain=telecom"
|
|
182
|
+
lql preview data.jsonl -f "reward>=0.8" -f "split=test" # both must hold
|
|
183
|
+
lql preview <dataset-id> -f "model~lfm" # contains
|
|
184
|
+
```
|
|
185
|
+
|
|
174
186
|
**HuggingFace datasets (`--hf`).** `lql preview org/name --hf` syncs the repo
|
|
175
187
|
into a DataViewer workspace, then opens it. You pick the target workspace from
|
|
176
188
|
an interactive list (or pass `--workspace <id>`; `--split` defaults to `train`).
|
|
@@ -150,11 +150,23 @@ lql preview <file.jsonl|file.json> Local file: each line/object is a row
|
|
|
150
150
|
lql preview <dataset-id> Platform dataset (fetched & paged lazily)
|
|
151
151
|
lql preview <org/name> --hf HuggingFace repo: sync to DataViewer, then view
|
|
152
152
|
lql preview <src> -c <field> Force field(s) as conversations (repeatable)
|
|
153
|
+
lql preview <src> -f "col=value" Filter rows (repeatable, AND); local & platform
|
|
153
154
|
lql preview <src> -n <N> Page size when paging a platform dataset
|
|
154
155
|
lql preview <src> --offset N Start at row index N
|
|
155
156
|
lql preview <src> --title "<title>" Title shown in the viewer header
|
|
156
157
|
```
|
|
157
158
|
|
|
159
|
+
**Filtering (`--filter`/`-f`).** Show only matching rows — works on local files and
|
|
160
|
+
platform datasets (platform filtering runs server-side). Repeatable; filters AND
|
|
161
|
+
together; string match is case-insensitive. Operators: `=`, `!=`, `~` (contains),
|
|
162
|
+
`>`, `<`, `>=`, `<=`.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
lql preview <dataset-id> -f "domain=telecom"
|
|
166
|
+
lql preview data.jsonl -f "reward>=0.8" -f "split=test" # both must hold
|
|
167
|
+
lql preview <dataset-id> -f "model~lfm" # contains
|
|
168
|
+
```
|
|
169
|
+
|
|
158
170
|
**HuggingFace datasets (`--hf`).** `lql preview org/name --hf` syncs the repo
|
|
159
171
|
into a DataViewer workspace, then opens it. You pick the target workspace from
|
|
160
172
|
an interactive list (or pass `--workspace <id>`; `--split` defaults to `train`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -10,7 +10,7 @@ from .._group import AliasGroup
|
|
|
10
10
|
|
|
11
11
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
12
12
|
from ..api import ApiClient
|
|
13
|
-
from ..output import print_error, print_json, print_table
|
|
13
|
+
from ..output import print_error, print_grouped_tables, print_json, print_table
|
|
14
14
|
from ..util import q
|
|
15
15
|
|
|
16
16
|
app = typer.Typer(help="Manage datasets", cls=AliasGroup, no_args_is_help=True)
|
|
@@ -21,6 +21,23 @@ def _truncate(v: object, n: int = 80) -> str:
|
|
|
21
21
|
return s[: n - 3] + "..." if len(s) > n else s
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _workspace_index(client: ApiClient) -> dict:
|
|
25
|
+
"""Map workspace id -> {"name", "team_name"}. Returns {} if lookups fail."""
|
|
26
|
+
try:
|
|
27
|
+
teams = {t.get("id"): (t.get("name") or "") for t in client.get("/v1/teams").json()}
|
|
28
|
+
except Exception:
|
|
29
|
+
teams = {}
|
|
30
|
+
try:
|
|
31
|
+
out = {}
|
|
32
|
+
for w in client.get("/v1/workspaces").json():
|
|
33
|
+
name = w.get("display_name") or w.get("name") or ""
|
|
34
|
+
tname = teams.get(w.get("team_id")) or ("(unknown team)" if w.get("team_id") else "Personal")
|
|
35
|
+
out[w.get("id")] = {"name": name, "team_name": tname}
|
|
36
|
+
return out
|
|
37
|
+
except Exception:
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
24
41
|
@app.command("list")
|
|
25
42
|
def list_datasets(
|
|
26
43
|
workspace: Annotated[Optional[str], typer.Option("--workspace", help="Filter by workspace ID")] = None,
|
|
@@ -28,6 +45,10 @@ def list_datasets(
|
|
|
28
45
|
Optional[str],
|
|
29
46
|
typer.Option("--search", "-s", help="Filter by case-insensitive substring of name / HF repo"),
|
|
30
47
|
] = None,
|
|
48
|
+
flat: Annotated[
|
|
49
|
+
bool,
|
|
50
|
+
typer.Option("--flat", "-f", help="Flat table instead of the default workspace-grouped view"),
|
|
51
|
+
] = False,
|
|
31
52
|
json_out: JsonOpt = False,
|
|
32
53
|
profile: ProfileOpt = None,
|
|
33
54
|
api_url: ApiUrlOpt = None,
|
|
@@ -47,13 +68,61 @@ def list_datasets(
|
|
|
47
68
|
or s in (d.get("name") or "").lower()
|
|
48
69
|
or s in (d.get("hf_repo_id") or "").lower()
|
|
49
70
|
]
|
|
71
|
+
|
|
72
|
+
ws_index = _workspace_index(client)
|
|
73
|
+
|
|
74
|
+
def _ws_info(d: dict) -> dict:
|
|
75
|
+
return ws_index.get(d.get("workspace_id")) or {}
|
|
76
|
+
|
|
77
|
+
# Enrich each dataset with resolved names (additive, keeps --json useful).
|
|
78
|
+
for d in items:
|
|
79
|
+
info = _ws_info(d)
|
|
80
|
+
d["workspace_name"] = info.get("name") or ""
|
|
81
|
+
d["team_name"] = info.get("team_name") or ""
|
|
82
|
+
|
|
83
|
+
if json_out:
|
|
84
|
+
print_json(items)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
def _row(d: dict) -> list:
|
|
88
|
+
return [
|
|
89
|
+
d.get("id") or "",
|
|
90
|
+
d.get("display_name") or d.get("name") or "",
|
|
91
|
+
d.get("row_count") if d.get("row_count") is not None else "",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def _ws_label(d: dict) -> str:
|
|
95
|
+
info = _ws_info(d)
|
|
96
|
+
wid = d.get("workspace_id") or ""
|
|
97
|
+
name = info.get("name") or "(unknown workspace)"
|
|
98
|
+
team = info.get("team_name") or ""
|
|
99
|
+
head = f"{name} · {team}" if team else name
|
|
100
|
+
return f"{head} ({wid})" if wid else head
|
|
101
|
+
|
|
102
|
+
if not flat:
|
|
103
|
+
groups: dict[str, list] = {}
|
|
104
|
+
labels: dict[str, str] = {}
|
|
105
|
+
for d in sorted(items, key=lambda d: (d.get("display_name") or d.get("name") or "").lower()):
|
|
106
|
+
info = _ws_info(d)
|
|
107
|
+
# Sort sections by team, then workspace name, so a team's datasets cluster.
|
|
108
|
+
key = (info.get("team_name") or "~", info.get("name") or "~", d.get("workspace_id") or "")
|
|
109
|
+
groups.setdefault(key, []).append(d)
|
|
110
|
+
labels[key] = _ws_label(d)
|
|
111
|
+
order = sorted(groups)
|
|
112
|
+
print_grouped_tables(
|
|
113
|
+
["ID", "Name", "Rows"],
|
|
114
|
+
[(labels[k], [_row(d) for d in groups[k]]) for k in order],
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
50
118
|
print_table(
|
|
51
|
-
["ID", "Name", "Workspace", "Rows"],
|
|
119
|
+
["ID", "Name", "Workspace", "Team", "Rows"],
|
|
52
120
|
[
|
|
53
121
|
[
|
|
54
122
|
d.get("id") or "",
|
|
55
123
|
d.get("display_name") or d.get("name") or "",
|
|
56
|
-
d.get("workspace_id") or "",
|
|
124
|
+
d.get("workspace_name") or d.get("workspace_id") or "",
|
|
125
|
+
d.get("team_name") or "",
|
|
57
126
|
d.get("row_count") if d.get("row_count") is not None else "",
|
|
58
127
|
]
|
|
59
128
|
for d in items
|
|
@@ -38,6 +38,13 @@ All commands accept --json for stable JSON output to stdout.
|
|
|
38
38
|
Errors always go to stderr as: { "error": "message", "code": "slug" }
|
|
39
39
|
Data always goes to stdout.
|
|
40
40
|
|
|
41
|
+
AGENTS: always pass --json and parse that. It is the stable contract — a flat,
|
|
42
|
+
unnested structure carrying full IDs plus resolved names (e.g. team_name,
|
|
43
|
+
workspace_name, parent_name on list results). The default human output is
|
|
44
|
+
Rich-rendered tables that may be grouped into sections and have truncated cells
|
|
45
|
+
(IDs shown as `8b45ab54-…`); do NOT parse it. Table-only display flags like
|
|
46
|
+
--flat / --group never affect --json output.
|
|
47
|
+
|
|
41
48
|
Exit codes:
|
|
42
49
|
0 success
|
|
43
50
|
1 usage / validation error
|
|
@@ -52,7 +59,7 @@ Pagination: --limit N --offset N on list commands.
|
|
|
52
59
|
|
|
53
60
|
A workspace is the top-level container for datasets, spec docs, and members.
|
|
54
61
|
|
|
55
|
-
lql workspaces list [--search <text>]
|
|
62
|
+
lql workspaces list [--search <text>] [--flat] # grouped by team by default; --flat for a single table; --search filters by name / slug substring
|
|
56
63
|
lql workspaces create <name>
|
|
57
64
|
lql workspaces show <id>
|
|
58
65
|
lql workspaces update <id> --name <new-name>
|
|
@@ -63,7 +70,7 @@ A workspace is the top-level container for datasets, spec docs, and members.
|
|
|
63
70
|
|
|
64
71
|
## Datasets
|
|
65
72
|
|
|
66
|
-
lql datasets list [--workspace <id>] [--search <text>] # --search filters by name / HF repo substring
|
|
73
|
+
lql datasets list [--workspace <id>] [--search <text>] [--flat] # grouped by workspace (with team) by default; --flat for a single table; --search filters by name / HF repo substring
|
|
67
74
|
lql datasets show <id>
|
|
68
75
|
lql datasets create --workspace <id> --hf-repo <org/repo> [--name <display>] [--split <split>]
|
|
69
76
|
lql datasets create --workspace <id> --hf-bucket <org/bucket> --key <path-or-glob> [--name <display>]
|
|
@@ -100,8 +107,17 @@ DataViewer workspace (you pick one interactively, or pass --workspace <id>;
|
|
|
100
107
|
--split defaults to train) and reused on later previews (dedup by repo+split).
|
|
101
108
|
|
|
102
109
|
Options: -c/--column (field(s) to treat as conversations; default auto-detect,
|
|
103
|
-
repeatable), -n/--limit (page size when
|
|
104
|
-
(start row index), --title, --hf, --split,
|
|
110
|
+
repeatable), -f/--filter (filter rows; see below), -n/--limit (page size when
|
|
111
|
+
paging a platform dataset), --offset (start row index), --title, --hf, --split,
|
|
112
|
+
--workspace, --profile, --api-url.
|
|
113
|
+
|
|
114
|
+
Filtering: -f/--filter "col<op>value" shows only matching rows — works on local
|
|
115
|
+
files and platform datasets (server-side for platform). Repeatable; filters AND
|
|
116
|
+
together; string compare is case-insensitive. Operators: = (eq), != (ne),
|
|
117
|
+
~ (contains), >, <, >=, <=.
|
|
118
|
+
|
|
119
|
+
lql preview <dataset-id> -f "domain=telecom" -f "reward>=0.8"
|
|
120
|
+
lql preview data.jsonl -f "model~lfm"
|
|
105
121
|
|
|
106
122
|
Navigation: two modes toggled with m — pager (one sample at a time; ←/→ or
|
|
107
123
|
n/b switch samples, ↑/↓/j/k scroll) and scroll (all samples; n/b jump between
|
|
@@ -759,6 +759,67 @@ def _choose_workspace(client: ApiClient, tui_mod) -> Optional[str]:
|
|
|
759
759
|
return choice
|
|
760
760
|
|
|
761
761
|
|
|
762
|
+
# --------------------------------------------------------------------------
|
|
763
|
+
# Row filtering (--filter "col<op>value")
|
|
764
|
+
# --------------------------------------------------------------------------
|
|
765
|
+
|
|
766
|
+
# Maps each CLI symbol to the platform filter API's operator name (the same
|
|
767
|
+
# names work server-side and locally). _parse_filters picks the earliest operator
|
|
768
|
+
# (longest on a tie), so list order doesn't affect correctness.
|
|
769
|
+
_FILTER_OPS = [(">=", "gte"), ("<=", "lte"), ("!=", "ne"), ("~", "contains"), ("=", "eq"), (">", "gt"), ("<", "lt")]
|
|
770
|
+
_NUMERIC_OPS = {"gt": lambda c, v: c > v, "lt": lambda c, v: c < v, "gte": lambda c, v: c >= v, "lte": lambda c, v: c <= v}
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _parse_filters(specs: Optional[List[str]]) -> List[tuple]:
|
|
774
|
+
"""Parse ['col=value', 'reward>=0.5', 'name~kod'] → [(col, op, value), ...].
|
|
775
|
+
|
|
776
|
+
Splits on the EARLIEST operator (longest on a tie, so 'reward>=5' is gte not
|
|
777
|
+
gt), keeping operator chars in the value intact (e.g. 'q=a>b' → col 'q', value
|
|
778
|
+
'a>b'). Rejects an empty column or value."""
|
|
779
|
+
out: List[tuple] = []
|
|
780
|
+
for spec in specs or []:
|
|
781
|
+
chosen = None # (index, symbol, op_name)
|
|
782
|
+
for sym, op in _FILTER_OPS:
|
|
783
|
+
i = spec.find(sym)
|
|
784
|
+
if i > 0 and (chosen is None or i < chosen[0] or (i == chosen[0] and len(sym) > len(chosen[1]))):
|
|
785
|
+
chosen = (i, sym, op)
|
|
786
|
+
if chosen is None:
|
|
787
|
+
print_error(
|
|
788
|
+
f"Invalid --filter '{spec}'. Use col=value, col!=value, col~text, or col>/</>=/<= N.",
|
|
789
|
+
"bad_filter",
|
|
790
|
+
)
|
|
791
|
+
raise typer.Exit(1)
|
|
792
|
+
i, sym, op = chosen
|
|
793
|
+
col, val = spec[:i].strip(), spec[i + len(sym):].strip()
|
|
794
|
+
if not col or not val:
|
|
795
|
+
print_error(f"Invalid --filter '{spec}': both a column and a value are required.", "bad_filter")
|
|
796
|
+
raise typer.Exit(1)
|
|
797
|
+
out.append((col, op, val))
|
|
798
|
+
return out
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _cell_matches(cell: object, op: str, val: str) -> bool:
|
|
802
|
+
if op == "contains":
|
|
803
|
+
return cell is not None and val.lower() in str(cell).lower()
|
|
804
|
+
if op in ("eq", "ne"):
|
|
805
|
+
equal = cell is not None and str(cell).strip().lower() == val.strip().lower()
|
|
806
|
+
return equal if op == "eq" else not equal
|
|
807
|
+
try:
|
|
808
|
+
return _NUMERIC_OPS[op](float(cell), float(val)) # gt/lt/gte/lte
|
|
809
|
+
except (TypeError, ValueError):
|
|
810
|
+
return False
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _row_matches(row: object, filters: List[tuple]) -> bool:
|
|
814
|
+
"""Client-side predicate (local files). A non-dict row can't match a column
|
|
815
|
+
filter. All filters AND together."""
|
|
816
|
+
if not filters:
|
|
817
|
+
return True
|
|
818
|
+
if not isinstance(row, dict):
|
|
819
|
+
return False
|
|
820
|
+
return all(_cell_matches(row.get(col), op, val) for col, op, val in filters)
|
|
821
|
+
|
|
822
|
+
|
|
762
823
|
# --------------------------------------------------------------------------
|
|
763
824
|
# Command
|
|
764
825
|
# --------------------------------------------------------------------------
|
|
@@ -772,6 +833,13 @@ def preview(
|
|
|
772
833
|
] = None,
|
|
773
834
|
limit: Annotated[int, typer.Option("--limit", "-n", help="Page size when paging a platform dataset")] = 25,
|
|
774
835
|
offset: Annotated[int, typer.Option("--offset", help="Start at this row index")] = 0,
|
|
836
|
+
filter_: Annotated[
|
|
837
|
+
Optional[List[str]],
|
|
838
|
+
typer.Option(
|
|
839
|
+
"--filter", "-f",
|
|
840
|
+
help="Filter rows: 'col=value', 'col!=value', 'col~text' (contains), or 'col>/</>=/<= N'. Repeatable (AND).",
|
|
841
|
+
),
|
|
842
|
+
] = None,
|
|
775
843
|
title: Annotated[Optional[str], typer.Option("--title", help="Title shown in the viewer header")] = None,
|
|
776
844
|
hf: Annotated[
|
|
777
845
|
bool, typer.Option("--hf", help="Treat SOURCE as a HuggingFace repo (org/name); sync it to DataViewer, then view")
|
|
@@ -801,13 +869,20 @@ def preview(
|
|
|
801
869
|
print_error("The terminal viewer requires 'textual'. Install it: pip install textual", "missing_textual")
|
|
802
870
|
raise typer.Exit(1)
|
|
803
871
|
|
|
872
|
+
filters = _parse_filters(filter_)
|
|
804
873
|
local_path = Path(source)
|
|
805
874
|
is_local = (not hf) and local_path.exists() and local_path.is_file()
|
|
806
875
|
|
|
807
|
-
# Local file → load whole, view immediately.
|
|
876
|
+
# Local file → load whole (filter client-side), view immediately.
|
|
808
877
|
if is_local:
|
|
878
|
+
rows = _load_local(local_path)
|
|
879
|
+
if filters:
|
|
880
|
+
rows = [r for r in rows if _row_matches(r, filters)]
|
|
881
|
+
if not rows:
|
|
882
|
+
print_error("No rows match the filter(s).", "no_match")
|
|
883
|
+
raise typer.Exit(3)
|
|
809
884
|
tui_mod.run(
|
|
810
|
-
tui_mod.RowSource(initial=
|
|
885
|
+
tui_mod.RowSource(initial=rows),
|
|
811
886
|
title or local_path.name,
|
|
812
887
|
forced_cols=column or None,
|
|
813
888
|
start_idx=max(0, offset),
|
|
@@ -834,11 +909,17 @@ def preview(
|
|
|
834
909
|
view_title = title or f"dataset {source}"
|
|
835
910
|
|
|
836
911
|
page_size = limit if limit and limit > 0 else 25
|
|
912
|
+
api_filters = [{"column": col, "operator": op, "value": val} for col, op, val in filters]
|
|
837
913
|
|
|
838
914
|
def _fetch_page(off: int, lim: int) -> List[object]:
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
915
|
+
params = {"limit": str(lim), "offset": str(offset + off)}
|
|
916
|
+
if api_filters:
|
|
917
|
+
# Server-side filtering via the same endpoint `eval samples` uses.
|
|
918
|
+
data = client.post(
|
|
919
|
+
f"/v1/datasets/{q(dataset_id)}/rows/filter", json={"filters": api_filters}, params=params
|
|
920
|
+
).json()
|
|
921
|
+
else:
|
|
922
|
+
data = client.get(f"/v1/datasets/{q(dataset_id)}/rows", params=params).json()
|
|
842
923
|
return _normalize_loaded(data)
|
|
843
924
|
|
|
844
925
|
# Fetch the first page up front (with feedback) so the viewer opens already
|
|
@@ -857,8 +938,13 @@ def preview(
|
|
|
857
938
|
if first:
|
|
858
939
|
break
|
|
859
940
|
if not first:
|
|
860
|
-
|
|
861
|
-
|
|
941
|
+
if filters:
|
|
942
|
+
msg, code = "No rows match the filter(s).", "no_match"
|
|
943
|
+
elif hf:
|
|
944
|
+
msg, code = f"Dataset returned no rows for split '{split}'.", "empty_dataset"
|
|
945
|
+
else:
|
|
946
|
+
msg, code = "Dataset returned no rows.", "empty_dataset"
|
|
947
|
+
print_error(msg, code)
|
|
862
948
|
raise typer.Exit(3)
|
|
863
949
|
row_source = tui_mod.RowSource(initial=first, fetch_page=_fetch_page, page_size=page_size)
|
|
864
950
|
tui_mod.run(
|
|
@@ -7,7 +7,7 @@ from .._group import AliasGroup
|
|
|
7
7
|
|
|
8
8
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
9
9
|
from ..api import ApiClient
|
|
10
|
-
from ..output import print_json, print_table
|
|
10
|
+
from ..output import print_grouped_tables, print_json, print_table
|
|
11
11
|
from ..util import q
|
|
12
12
|
|
|
13
13
|
app = typer.Typer(help="Manage workspaces", cls=AliasGroup, no_args_is_help=True)
|
|
@@ -15,12 +15,28 @@ members_app = typer.Typer(help="Manage workspace members", cls=AliasGroup, no_ar
|
|
|
15
15
|
app.add_typer(members_app, name="members")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _ws_name(w: dict) -> str:
|
|
19
|
+
return w.get("display_name") or w.get("name") or ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _team_names(client: ApiClient) -> dict:
|
|
23
|
+
"""Map team id -> team name. Returns {} if teams can't be fetched."""
|
|
24
|
+
try:
|
|
25
|
+
return {t.get("id"): (t.get("name") or "") for t in client.get("/v1/teams").json()}
|
|
26
|
+
except Exception:
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
@app.command("list")
|
|
19
31
|
def list_workspaces(
|
|
20
32
|
search: Annotated[
|
|
21
33
|
Optional[str],
|
|
22
34
|
typer.Option("--search", "-s", help="Filter by case-insensitive substring of name / slug"),
|
|
23
35
|
] = None,
|
|
36
|
+
flat: Annotated[
|
|
37
|
+
bool,
|
|
38
|
+
typer.Option("--flat", "-f", help="Flat table instead of the default team-grouped view"),
|
|
39
|
+
] = False,
|
|
24
40
|
json_out: JsonOpt = False,
|
|
25
41
|
profile: ProfileOpt = None,
|
|
26
42
|
api_url: ApiUrlOpt = None,
|
|
@@ -37,9 +53,54 @@ def list_workspaces(
|
|
|
37
53
|
or s in (w.get("name") or "").lower()
|
|
38
54
|
or s in (w.get("slug") or "").lower()
|
|
39
55
|
]
|
|
56
|
+
|
|
57
|
+
team_names = _team_names(client)
|
|
58
|
+
name_by_id = {w.get("id"): _ws_name(w) for w in items}
|
|
59
|
+
# Enrich each workspace with resolved names (additive, keeps --json useful).
|
|
60
|
+
for w in items:
|
|
61
|
+
w["team_name"] = team_names.get(w.get("team_id")) or ""
|
|
62
|
+
w["parent_name"] = name_by_id.get(w.get("parent_id")) or ""
|
|
63
|
+
|
|
64
|
+
if json_out:
|
|
65
|
+
print_json(items)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
def _team_label(w: dict) -> str:
|
|
69
|
+
name, tid = w.get("team_name"), w.get("team_id")
|
|
70
|
+
if name:
|
|
71
|
+
return f"{name} ({tid})"
|
|
72
|
+
if tid:
|
|
73
|
+
return f"(unknown team) ({tid})"
|
|
74
|
+
return "Personal"
|
|
75
|
+
|
|
76
|
+
if not flat:
|
|
77
|
+
groups: dict[str, list] = {}
|
|
78
|
+
for w in sorted(items, key=lambda w: _ws_name(w).lower()):
|
|
79
|
+
groups.setdefault(_team_label(w), []).append(w)
|
|
80
|
+
# Named teams first (alphabetical), then unknown teams, then Personal last.
|
|
81
|
+
def _rank(label: str) -> tuple:
|
|
82
|
+
if label == "Personal":
|
|
83
|
+
return (2, label)
|
|
84
|
+
if label.startswith("(unknown team)"):
|
|
85
|
+
return (1, label)
|
|
86
|
+
return (0, label.lower())
|
|
87
|
+
|
|
88
|
+
order = sorted(groups, key=_rank)
|
|
89
|
+
print_grouped_tables(
|
|
90
|
+
["ID", "Name", "Parent"],
|
|
91
|
+
[
|
|
92
|
+
(label, [[w.get("id") or "", _ws_name(w), w.get("parent_name") or ""] for w in groups[label]])
|
|
93
|
+
for label in order
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
40
98
|
print_table(
|
|
41
|
-
["ID", "Name"],
|
|
42
|
-
[
|
|
99
|
+
["ID", "Name", "Parent", "Team"],
|
|
100
|
+
[
|
|
101
|
+
[w.get("id") or "", _ws_name(w), w.get("parent_name") or "", _team_label(w)]
|
|
102
|
+
for w in items
|
|
103
|
+
],
|
|
43
104
|
json_out,
|
|
44
105
|
items,
|
|
45
106
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sys
|
|
3
|
-
from typing import List, Sequence
|
|
3
|
+
from typing import List, Sequence, Tuple
|
|
4
4
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
from rich.table import Table
|
|
@@ -24,6 +24,26 @@ def print_table(headers: Sequence[str], rows: Sequence[Sequence[str]], is_json:
|
|
|
24
24
|
_console.print(table)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def print_grouped_tables(
|
|
28
|
+
headers: Sequence[str],
|
|
29
|
+
groups: Sequence[Tuple[str, Sequence[Sequence[str]]]],
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Print one table per group, each preceded by a bold group header.
|
|
32
|
+
|
|
33
|
+
`groups` is a sequence of (label, rows) pairs. JSON callers should print
|
|
34
|
+
the underlying data directly rather than going through this helper.
|
|
35
|
+
"""
|
|
36
|
+
for label, rows in groups:
|
|
37
|
+
_console.print(f"[bold]{label}[/bold] ({len(rows)})")
|
|
38
|
+
table = Table(show_header=True, header_style="bold")
|
|
39
|
+
for h in headers:
|
|
40
|
+
table.add_column(str(h))
|
|
41
|
+
for row in rows:
|
|
42
|
+
table.add_row(*[str(c) for c in row])
|
|
43
|
+
_console.print(table)
|
|
44
|
+
_console.print()
|
|
45
|
+
|
|
46
|
+
|
|
27
47
|
def print_error(message: str, code: str) -> None:
|
|
28
48
|
# Compact, machine-readable; always to stderr (matches the TS contract).
|
|
29
49
|
sys.stderr.write(json.dumps({"error": message, "code": code}) + "\n")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|