lql-cli 0.3.0__tar.gz → 0.4.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.3.0 → lql_cli-0.4.0}/PKG-INFO +15 -3
- {lql_cli-0.3.0 → lql_cli-0.4.0}/README.md +14 -2
- {lql_cli-0.3.0 → lql_cli-0.4.0}/pyproject.toml +1 -1
- lql_cli-0.4.0/src/lql/__init__.py +1 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/instructions.py +6 -1
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/preview.py +191 -32
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/tui.py +38 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/uv.lock +1 -1
- lql_cli-0.3.0/src/lql/__init__.py +0 -1
- {lql_cli-0.3.0 → lql_cli-0.4.0}/.claude/settings.local.json +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/.gitignore +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/examples/agent-traces.jsonl +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/package-lock.json +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/_opts.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/api.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/cli.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/__init__.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/annotations.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/auth.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/buckets.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/datasets.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/edits.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/evals.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/highlights.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/issues.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/reports.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/skills.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/spec.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/update.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/workspaces.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/config.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/output.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/sessions.py +0 -0
- {lql_cli-0.3.0 → lql_cli-0.4.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.4.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
|
|
@@ -149,18 +149,30 @@ multimodal content (text/image/audio), ShareGPT `{from, value}`, native OpenAI
|
|
|
149
149
|
`tool_calls`, plus `<think>` reasoning blocks, `<|tool_call_start|>…<|tool_call_end|>`
|
|
150
150
|
/ Python / XML / JSON tool calls, tool results, tool-definition tables, and code.
|
|
151
151
|
|
|
152
|
-
Works on a local `.jsonl`/`.json` file
|
|
153
|
-
nothing to forward over SSH — it's just
|
|
152
|
+
Works on a local `.jsonl`/`.json` file, a platform dataset ID, or — with `--hf`
|
|
153
|
+
— a HuggingFace repo. No browser, and nothing to forward over SSH — it's just
|
|
154
|
+
the terminal.
|
|
154
155
|
|
|
155
156
|
```
|
|
156
157
|
lql preview <file.jsonl|file.json> Local file: each line/object is a row
|
|
157
158
|
lql preview <dataset-id> Platform dataset (fetched & paged lazily)
|
|
159
|
+
lql preview <org/name> --hf HuggingFace repo: sync to DataViewer, then view
|
|
158
160
|
lql preview <src> -c <field> Force field(s) as conversations (repeatable)
|
|
159
161
|
lql preview <src> -n <N> Page size when paging a platform dataset
|
|
160
162
|
lql preview <src> --offset N Start at row index N
|
|
161
163
|
lql preview <src> --title "<title>" Title shown in the viewer header
|
|
162
164
|
```
|
|
163
165
|
|
|
166
|
+
**HuggingFace datasets (`--hf`).** `lql preview org/name --hf` syncs the repo
|
|
167
|
+
into a DataViewer workspace, then opens it. You pick the target workspace from
|
|
168
|
+
an interactive list (or pass `--workspace <id>`; `--split` defaults to `train`).
|
|
169
|
+
Already-synced repos are reused — no duplicate, instant re-open.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
lql preview tatsu-lab/alpaca --hf
|
|
173
|
+
lql preview org/name --hf --split validation --workspace <id>
|
|
174
|
+
```
|
|
175
|
+
|
|
164
176
|
**Navigation** — two modes, toggle with `m` / `Tab`:
|
|
165
177
|
|
|
166
178
|
- **pager** (default): one sample at a time · `←/→` or `n`/`b` switch samples · `↑/↓`/`j`/`k`/PgUp-Dn scroll
|
|
@@ -134,18 +134,30 @@ multimodal content (text/image/audio), ShareGPT `{from, value}`, native OpenAI
|
|
|
134
134
|
`tool_calls`, plus `<think>` reasoning blocks, `<|tool_call_start|>…<|tool_call_end|>`
|
|
135
135
|
/ Python / XML / JSON tool calls, tool results, tool-definition tables, and code.
|
|
136
136
|
|
|
137
|
-
Works on a local `.jsonl`/`.json` file
|
|
138
|
-
nothing to forward over SSH — it's just
|
|
137
|
+
Works on a local `.jsonl`/`.json` file, a platform dataset ID, or — with `--hf`
|
|
138
|
+
— a HuggingFace repo. No browser, and nothing to forward over SSH — it's just
|
|
139
|
+
the terminal.
|
|
139
140
|
|
|
140
141
|
```
|
|
141
142
|
lql preview <file.jsonl|file.json> Local file: each line/object is a row
|
|
142
143
|
lql preview <dataset-id> Platform dataset (fetched & paged lazily)
|
|
144
|
+
lql preview <org/name> --hf HuggingFace repo: sync to DataViewer, then view
|
|
143
145
|
lql preview <src> -c <field> Force field(s) as conversations (repeatable)
|
|
144
146
|
lql preview <src> -n <N> Page size when paging a platform dataset
|
|
145
147
|
lql preview <src> --offset N Start at row index N
|
|
146
148
|
lql preview <src> --title "<title>" Title shown in the viewer header
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
**HuggingFace datasets (`--hf`).** `lql preview org/name --hf` syncs the repo
|
|
152
|
+
into a DataViewer workspace, then opens it. You pick the target workspace from
|
|
153
|
+
an interactive list (or pass `--workspace <id>`; `--split` defaults to `train`).
|
|
154
|
+
Already-synced repos are reused — no duplicate, instant re-open.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
lql preview tatsu-lab/alpaca --hf
|
|
158
|
+
lql preview org/name --hf --split validation --workspace <id>
|
|
159
|
+
```
|
|
160
|
+
|
|
149
161
|
**Navigation** — two modes, toggle with `m` / `Tab`:
|
|
150
162
|
|
|
151
163
|
- **pager** (default): one sample at a time · `←/→` or `n`/`b` switch samples · `↑/↓`/`j`/`k`/PgUp-Dn scroll
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -83,11 +83,16 @@ Non-conversation fields render as labeled metadata.
|
|
|
83
83
|
|
|
84
84
|
lql preview <file.jsonl|file.json> # local file: each line/object is a row
|
|
85
85
|
lql preview <dataset-id> # platform dataset, fetched & paged lazily
|
|
86
|
+
lql preview <org/name> --hf # HuggingFace repo: sync to DataViewer, then view
|
|
86
87
|
lql preview data.jsonl -c messages # force field(s) as conversations
|
|
87
88
|
|
|
89
|
+
With --hf, SOURCE is a HuggingFace repo (org/name): it's synced into a
|
|
90
|
+
DataViewer workspace (you pick one interactively, or pass --workspace <id>;
|
|
91
|
+
--split defaults to train) and reused on later previews (dedup by repo+split).
|
|
92
|
+
|
|
88
93
|
Options: -c/--column (field(s) to treat as conversations; default auto-detect,
|
|
89
94
|
repeatable), -n/--limit (page size when paging a platform dataset), --offset
|
|
90
|
-
(start row index), --title, --profile, --api-url.
|
|
95
|
+
(start row index), --title, --hf, --split, --workspace, --profile, --api-url.
|
|
91
96
|
|
|
92
97
|
Navigation: two modes toggled with m/Tab — pager (one sample at a time; ←/→ or
|
|
93
98
|
n/b switch samples, ↑/↓/j/k scroll) and scroll (all samples; n/b jump between
|
|
@@ -12,6 +12,7 @@ these and renders to the terminal via Rich + Textual.
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
import re
|
|
15
|
+
import sys
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Annotated, List, Optional
|
|
17
18
|
|
|
@@ -585,13 +586,128 @@ def _normalize_loaded(data: object) -> List[object]:
|
|
|
585
586
|
return [data]
|
|
586
587
|
|
|
587
588
|
|
|
589
|
+
# --------------------------------------------------------------------------
|
|
590
|
+
# HuggingFace auto-sync (for `--hf`)
|
|
591
|
+
# --------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
# Default landing workspace for HF datasets synced on the fly (there is no
|
|
594
|
+
# "personal workspace" concept in the API, so we find-or-create this one).
|
|
595
|
+
_HF_WORKSPACE_NAME = "lql-preview"
|
|
596
|
+
# Terminal sync states. Accept several "done" spellings defensively in case the
|
|
597
|
+
# API's vocabulary varies (verified to return "ready" today).
|
|
598
|
+
_SYNC_READY = {"ready", "completed", "synced", "success", "done"}
|
|
599
|
+
_SYNC_FAILED = {"failed", "error", "cancelled", "canceled"}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _find_or_create_workspace(client: ApiClient, name: str) -> str:
|
|
603
|
+
"""Return the id of the workspace named `name`, creating it if absent."""
|
|
604
|
+
for w in client.get("/v1/workspaces").json():
|
|
605
|
+
if (w.get("display_name") or w.get("name") or "").strip().lower() == name.lower():
|
|
606
|
+
return w["id"]
|
|
607
|
+
created = client.post("/v1/workspaces", json={"name": name}).json()
|
|
608
|
+
return created["id"]
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _find_dataset_for_repo(client: ApiClient, workspace_id: str, repo: str, split: str) -> Optional[dict]:
|
|
612
|
+
"""Return an existing dataset in the workspace for this HF repo+split, if any."""
|
|
613
|
+
items = client.get("/v1/datasets", params={"workspace_id": workspace_id}).json()
|
|
614
|
+
for d in items or []:
|
|
615
|
+
if d.get("hf_repo_id") == repo and (d.get("hf_split") or "train") == split:
|
|
616
|
+
return d
|
|
617
|
+
return None
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _wait_until_ready(client: ApiClient, dataset_id: str, timeout: float = 180.0) -> None:
|
|
621
|
+
"""Poll the dataset's sync_status until it's ready (or fails / times out),
|
|
622
|
+
printing progress dots so a long sync doesn't look hung."""
|
|
623
|
+
import time
|
|
624
|
+
|
|
625
|
+
start = time.time()
|
|
626
|
+
dots = 0
|
|
627
|
+
while True:
|
|
628
|
+
d = client.get(f"/v1/datasets/{q(dataset_id)}").json()
|
|
629
|
+
status = (d.get("sync_status") or "").lower()
|
|
630
|
+
if status in _SYNC_READY:
|
|
631
|
+
sys.stdout.write("\n")
|
|
632
|
+
return
|
|
633
|
+
if status in _SYNC_FAILED:
|
|
634
|
+
print_error(f"Sync failed for {dataset_id} (status: {status or 'unknown'})", "sync_failed")
|
|
635
|
+
raise typer.Exit(5)
|
|
636
|
+
if time.time() - start > timeout:
|
|
637
|
+
sys.stdout.write("\n")
|
|
638
|
+
print_error(
|
|
639
|
+
f"Timed out waiting for sync (status: {status or 'unknown'}). "
|
|
640
|
+
f"It may still finish — re-run the same command to resume.",
|
|
641
|
+
"sync_timeout",
|
|
642
|
+
)
|
|
643
|
+
raise typer.Exit(1)
|
|
644
|
+
dots = (dots % 3) + 1
|
|
645
|
+
sys.stdout.write("\r syncing" + "." * dots + " ")
|
|
646
|
+
sys.stdout.flush()
|
|
647
|
+
time.sleep(2.0)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _resolve_hf_dataset(client: ApiClient, repo: str, split: str, ws_id: str) -> str:
|
|
651
|
+
"""Reuse an already-synced dataset for this HF repo in the given workspace if
|
|
652
|
+
present, otherwise create + sync one. Returns the dataset id."""
|
|
653
|
+
existing = _find_dataset_for_repo(client, ws_id, repo, split)
|
|
654
|
+
if existing is not None:
|
|
655
|
+
dataset_id = existing["id"]
|
|
656
|
+
if (existing.get("sync_status") or "").lower() not in _SYNC_READY:
|
|
657
|
+
sys.stdout.write(f"Resuming sync of {repo} …\n")
|
|
658
|
+
_wait_until_ready(client, dataset_id)
|
|
659
|
+
return dataset_id
|
|
660
|
+
sys.stdout.write(f"Syncing {repo} ({split}) into workspace '{_HF_WORKSPACE_NAME}' …\n")
|
|
661
|
+
created = client.post(
|
|
662
|
+
"/v1/datasets",
|
|
663
|
+
json={"workspace_id": ws_id, "hf_repo_id": repo, "hf_split": split, "display_name": repo},
|
|
664
|
+
).json()
|
|
665
|
+
dataset_id = created.get("id")
|
|
666
|
+
if not dataset_id:
|
|
667
|
+
print_error("Dataset creation did not return an id.", "create_failed")
|
|
668
|
+
raise typer.Exit(5)
|
|
669
|
+
if (created.get("sync_status") or "").lower() not in _SYNC_READY:
|
|
670
|
+
_wait_until_ready(client, dataset_id)
|
|
671
|
+
return dataset_id
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _resolve_workspace_arg(client: ApiClient, value: str) -> str:
|
|
675
|
+
"""Resolve a --workspace value that may be an id OR a name. Returns a
|
|
676
|
+
workspace id, creating the workspace if the value is an unknown name."""
|
|
677
|
+
workspaces = client.get("/v1/workspaces").json() or []
|
|
678
|
+
for w in workspaces:
|
|
679
|
+
if w.get("id") == value:
|
|
680
|
+
return value
|
|
681
|
+
for w in workspaces:
|
|
682
|
+
if (w.get("display_name") or w.get("name") or "").strip().lower() == value.strip().lower():
|
|
683
|
+
return w["id"]
|
|
684
|
+
return _find_or_create_workspace(client, value)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
_CREATE_SENTINEL = "__create_lql_preview__"
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _choose_workspace(client: ApiClient, tui_mod) -> Optional[str]:
|
|
691
|
+
"""Interactive workspace picker (arrow keys + Enter). Returns the chosen
|
|
692
|
+
workspace id, or None if the user cancels."""
|
|
693
|
+
workspaces = client.get("/v1/workspaces").json() or []
|
|
694
|
+
options = [((w.get("display_name") or w.get("name") or w["id"]), w["id"]) for w in workspaces]
|
|
695
|
+
options.append((f"➕ Create new workspace '{_HF_WORKSPACE_NAME}'", _CREATE_SENTINEL))
|
|
696
|
+
choice = tui_mod.select_workspace(options, title="Select a workspace for this HF dataset")
|
|
697
|
+
if choice is None:
|
|
698
|
+
return None
|
|
699
|
+
if choice == _CREATE_SENTINEL:
|
|
700
|
+
return _find_or_create_workspace(client, _HF_WORKSPACE_NAME)
|
|
701
|
+
return choice
|
|
702
|
+
|
|
703
|
+
|
|
588
704
|
# --------------------------------------------------------------------------
|
|
589
705
|
# Command
|
|
590
706
|
# --------------------------------------------------------------------------
|
|
591
707
|
|
|
592
708
|
|
|
593
709
|
def preview(
|
|
594
|
-
source: Annotated[str, typer.Argument(help="Local .jsonl/.json file,
|
|
710
|
+
source: Annotated[str, typer.Argument(help="Local .jsonl/.json file, a dataset ID, or an HF repo with --hf")],
|
|
595
711
|
column: Annotated[
|
|
596
712
|
Optional[List[str]],
|
|
597
713
|
typer.Option("--column", "-c", help="Field(s) to render as conversations (default: auto-detect)"),
|
|
@@ -599,15 +715,24 @@ def preview(
|
|
|
599
715
|
limit: Annotated[int, typer.Option("--limit", "-n", help="Page size when paging a platform dataset")] = 25,
|
|
600
716
|
offset: Annotated[int, typer.Option("--offset", help="Start at this row index")] = 0,
|
|
601
717
|
title: Annotated[Optional[str], typer.Option("--title", help="Title shown in the viewer header")] = None,
|
|
718
|
+
hf: Annotated[
|
|
719
|
+
bool, typer.Option("--hf", help="Treat SOURCE as a HuggingFace repo (org/name); sync it to DataViewer, then view")
|
|
720
|
+
] = False,
|
|
721
|
+
split: Annotated[str, typer.Option("--split", help="HF split (with --hf)")] = "train",
|
|
722
|
+
workspace: Annotated[
|
|
723
|
+
Optional[str],
|
|
724
|
+
typer.Option("--workspace", help="Workspace name or ID to sync into (with --hf); default: pick one interactively"),
|
|
725
|
+
] = None,
|
|
602
726
|
profile: ProfileOpt = None,
|
|
603
727
|
api_url: ApiUrlOpt = None,
|
|
604
728
|
) -> None:
|
|
605
729
|
"""View dataset samples in the terminal (a Textual TUI).
|
|
606
730
|
|
|
607
|
-
SOURCE may be a local .jsonl/.json file
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
731
|
+
SOURCE may be a local .jsonl/.json file, a platform dataset ID, or — with
|
|
732
|
+
--hf — a HuggingFace repo (org/name). Local files are loaded whole; platform
|
|
733
|
+
datasets are fetched and paged lazily (--limit is the page size). With --hf,
|
|
734
|
+
the repo is synced into a DataViewer workspace (reused if already synced),
|
|
735
|
+
then opened. Works over plain SSH — no browser or port-forward needed.
|
|
611
736
|
|
|
612
737
|
Navigation: ←/→ or n/b switch samples · ↑/↓/j/k scroll · m toggles
|
|
613
738
|
pager/scroll mode · q quits.
|
|
@@ -619,35 +744,69 @@ def preview(
|
|
|
619
744
|
raise typer.Exit(1)
|
|
620
745
|
|
|
621
746
|
local_path = Path(source)
|
|
622
|
-
is_local = local_path.exists() and local_path.is_file()
|
|
747
|
+
is_local = (not hf) and local_path.exists() and local_path.is_file()
|
|
623
748
|
|
|
749
|
+
# Local file → load whole, view immediately.
|
|
624
750
|
if is_local:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
751
|
+
tui_mod.run(
|
|
752
|
+
tui_mod.RowSource(initial=_load_local(local_path)),
|
|
753
|
+
title or local_path.name,
|
|
754
|
+
forced_cols=column or None,
|
|
755
|
+
start_idx=max(0, offset),
|
|
756
|
+
index_base=0, # local rows are absolute; the cursor just starts at offset
|
|
757
|
+
)
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
client = ApiClient(profile=profile, api_url=api_url)
|
|
761
|
+
|
|
762
|
+
# --hf: resolve (sync/reuse) the repo into a workspace and get its dataset id.
|
|
763
|
+
if hf:
|
|
764
|
+
if workspace:
|
|
765
|
+
ws_id = _resolve_workspace_arg(client, workspace)
|
|
766
|
+
elif sys.stdin.isatty() and sys.stdout.isatty():
|
|
767
|
+
ws_id = _choose_workspace(client, tui_mod)
|
|
768
|
+
if ws_id is None:
|
|
769
|
+
raise typer.Exit(0) # user cancelled the picker
|
|
770
|
+
else:
|
|
771
|
+
ws_id = _find_or_create_workspace(client, _HF_WORKSPACE_NAME)
|
|
772
|
+
dataset_id = _resolve_hf_dataset(client, source, split, ws_id)
|
|
773
|
+
view_title = title or f"{source} ({split})"
|
|
629
774
|
else:
|
|
630
|
-
|
|
631
|
-
page_size = limit if limit and limit > 0 else 25
|
|
632
|
-
|
|
633
|
-
def _fetch_page(off: int, lim: int) -> List[object]:
|
|
634
|
-
data = client.get(
|
|
635
|
-
f"/v1/datasets/{q(source)}/rows", params={"limit": str(lim), "offset": str(offset + off)}
|
|
636
|
-
).json()
|
|
637
|
-
return _normalize_loaded(data)
|
|
638
|
-
|
|
639
|
-
# Fetch the first page up front (with feedback) so the viewer opens
|
|
640
|
-
# already populated instead of launching into a blank alternate-screen
|
|
641
|
-
# while the network call blocks. API/auth errors surface here as normal
|
|
642
|
-
# CLI errors too, rather than vanishing behind the TUI.
|
|
643
|
-
print(f"Loading dataset {source} …", flush=True)
|
|
644
|
-
first = _fetch_page(0, page_size)
|
|
645
|
-
if not first:
|
|
646
|
-
print_error("Dataset returned no rows.", "empty_dataset")
|
|
647
|
-
raise typer.Exit(3)
|
|
648
|
-
row_source = tui_mod.RowSource(initial=first, fetch_page=_fetch_page, page_size=page_size)
|
|
649
|
-
start_idx = 0
|
|
650
|
-
index_base = offset # fetched window starts at --offset; show absolute indexes
|
|
775
|
+
dataset_id = source
|
|
651
776
|
view_title = title or f"dataset {source}"
|
|
652
777
|
|
|
653
|
-
|
|
778
|
+
page_size = limit if limit and limit > 0 else 25
|
|
779
|
+
|
|
780
|
+
def _fetch_page(off: int, lim: int) -> List[object]:
|
|
781
|
+
data = client.get(
|
|
782
|
+
f"/v1/datasets/{q(dataset_id)}/rows", params={"limit": str(lim), "offset": str(offset + off)}
|
|
783
|
+
).json()
|
|
784
|
+
return _normalize_loaded(data)
|
|
785
|
+
|
|
786
|
+
# Fetch the first page up front (with feedback) so the viewer opens already
|
|
787
|
+
# populated instead of launching into a blank alternate-screen while the
|
|
788
|
+
# network call blocks. API/auth errors surface here as normal CLI errors too.
|
|
789
|
+
print(f"Loading dataset {dataset_id} …", flush=True)
|
|
790
|
+
first = _fetch_page(0, page_size)
|
|
791
|
+
if not first and hf:
|
|
792
|
+
# Rows can lag a freshly-reported-ready sync (indexing/eventual
|
|
793
|
+
# consistency); give it a few short retries before giving up.
|
|
794
|
+
import time
|
|
795
|
+
|
|
796
|
+
for _ in range(3):
|
|
797
|
+
time.sleep(2.0)
|
|
798
|
+
first = _fetch_page(0, page_size)
|
|
799
|
+
if first:
|
|
800
|
+
break
|
|
801
|
+
if not first:
|
|
802
|
+
msg = f"Dataset returned no rows for split '{split}'." if hf else "Dataset returned no rows."
|
|
803
|
+
print_error(msg, "empty_dataset")
|
|
804
|
+
raise typer.Exit(3)
|
|
805
|
+
row_source = tui_mod.RowSource(initial=first, fetch_page=_fetch_page, page_size=page_size)
|
|
806
|
+
tui_mod.run(
|
|
807
|
+
row_source,
|
|
808
|
+
view_title,
|
|
809
|
+
forced_cols=column or None,
|
|
810
|
+
start_idx=0,
|
|
811
|
+
index_base=offset, # fetched window starts at --offset; show absolute indexes
|
|
812
|
+
)
|
|
@@ -338,6 +338,44 @@ class RowSource:
|
|
|
338
338
|
# --------------------------------------------------------------------------
|
|
339
339
|
|
|
340
340
|
|
|
341
|
+
def select_workspace(options: List[tuple], title: str = "Select a workspace") -> Optional[object]:
|
|
342
|
+
"""Interactive arrow-key picker. `options` is a list of (label, value);
|
|
343
|
+
returns the chosen value, or None if the user cancels (q / esc)."""
|
|
344
|
+
from textual.app import App, ComposeResult
|
|
345
|
+
from textual.binding import Binding
|
|
346
|
+
from textual.widgets import Footer, Header, OptionList
|
|
347
|
+
from textual.widgets.option_list import Option
|
|
348
|
+
|
|
349
|
+
class Picker(App):
|
|
350
|
+
CSS = "OptionList { height: 1fr; padding: 1 2; }"
|
|
351
|
+
BINDINGS = [Binding("escape,q", "cancel", "Cancel")]
|
|
352
|
+
|
|
353
|
+
def __init__(self) -> None:
|
|
354
|
+
super().__init__()
|
|
355
|
+
self.selected: Optional[object] = None
|
|
356
|
+
|
|
357
|
+
def compose(self) -> "ComposeResult":
|
|
358
|
+
yield Header()
|
|
359
|
+
yield OptionList(*[Option(label, id=str(i)) for i, (label, _v) in enumerate(options)])
|
|
360
|
+
yield Footer()
|
|
361
|
+
|
|
362
|
+
def on_mount(self) -> None:
|
|
363
|
+
self.title = title
|
|
364
|
+
self.sub_title = "↑/↓ move · Enter select · q cancel"
|
|
365
|
+
self.query_one(OptionList).focus()
|
|
366
|
+
|
|
367
|
+
def on_option_list_option_selected(self, event) -> None:
|
|
368
|
+
self.selected = options[int(event.option.id)][1]
|
|
369
|
+
self.exit()
|
|
370
|
+
|
|
371
|
+
def action_cancel(self) -> None:
|
|
372
|
+
self.exit()
|
|
373
|
+
|
|
374
|
+
app = Picker()
|
|
375
|
+
app.run()
|
|
376
|
+
return app.selected
|
|
377
|
+
|
|
378
|
+
|
|
341
379
|
def run(
|
|
342
380
|
source: "RowSource",
|
|
343
381
|
title: str,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.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
|
|
File without changes
|