lql-cli 0.7.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.7.0 → lql_cli-0.8.0}/PKG-INFO +1 -1
- {lql_cli-0.7.0 → lql_cli-0.8.0}/pyproject.toml +1 -1
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/datasets.py +72 -3
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/instructions.py +9 -2
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/workspaces.py +64 -3
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/output.py +21 -1
- {lql_cli-0.7.0 → lql_cli-0.8.0}/.claude/settings.local.json +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/.gitignore +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/README.md +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/examples/agent-traces.jsonl +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/package-lock.json +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/__init__.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/_group.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/_opts.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/api.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/cli.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/__init__.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/annotations.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/auth.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/buckets.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/edits.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/evals.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/highlights.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/issues.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/preview.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/reports.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/skills.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/spec.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/tui.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/update.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/config.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/sessions.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/util.py +0 -0
- {lql_cli-0.7.0 → lql_cli-0.8.0}/uv.lock +0 -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>]
|
|
@@ -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")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|