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.
Files changed (34) hide show
  1. {lql_cli-0.7.0 → lql_cli-0.8.0}/PKG-INFO +1 -1
  2. {lql_cli-0.7.0 → lql_cli-0.8.0}/pyproject.toml +1 -1
  3. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/datasets.py +72 -3
  4. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/instructions.py +9 -2
  5. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/workspaces.py +64 -3
  6. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/output.py +21 -1
  7. {lql_cli-0.7.0 → lql_cli-0.8.0}/.claude/settings.local.json +0 -0
  8. {lql_cli-0.7.0 → lql_cli-0.8.0}/.gitignore +0 -0
  9. {lql_cli-0.7.0 → lql_cli-0.8.0}/README.md +0 -0
  10. {lql_cli-0.7.0 → lql_cli-0.8.0}/examples/agent-traces.jsonl +0 -0
  11. {lql_cli-0.7.0 → lql_cli-0.8.0}/package-lock.json +0 -0
  12. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/__init__.py +0 -0
  13. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/_group.py +0 -0
  14. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/_opts.py +0 -0
  15. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/api.py +0 -0
  16. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/cli.py +0 -0
  17. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/__init__.py +0 -0
  18. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/annotations.py +0 -0
  19. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/auth.py +0 -0
  20. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/buckets.py +0 -0
  21. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/edits.py +0 -0
  22. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/evals.py +0 -0
  23. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/highlights.py +0 -0
  24. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/issues.py +0 -0
  25. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/preview.py +0 -0
  26. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/reports.py +0 -0
  27. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/skills.py +0 -0
  28. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/spec.py +0 -0
  29. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/tui.py +0 -0
  30. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/commands/update.py +0 -0
  31. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/config.py +0 -0
  32. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/sessions.py +0 -0
  33. {lql_cli-0.7.0 → lql_cli-0.8.0}/src/lql/util.py +0 -0
  34. {lql_cli-0.7.0 → lql_cli-0.8.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lql-cli
3
- Version: 0.7.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lql-cli"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "lql — CLI for the Liquid DataViewer platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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>] # --search filters by name / slug substring
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
- [[w.get("id") or "", w.get("display_name") or w.get("name") or ""] for w in items],
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