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.
Files changed (34) hide show
  1. {lql_cli-0.3.0 → lql_cli-0.4.0}/PKG-INFO +15 -3
  2. {lql_cli-0.3.0 → lql_cli-0.4.0}/README.md +14 -2
  3. {lql_cli-0.3.0 → lql_cli-0.4.0}/pyproject.toml +1 -1
  4. lql_cli-0.4.0/src/lql/__init__.py +1 -0
  5. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/instructions.py +6 -1
  6. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/preview.py +191 -32
  7. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/tui.py +38 -0
  8. {lql_cli-0.3.0 → lql_cli-0.4.0}/uv.lock +1 -1
  9. lql_cli-0.3.0/src/lql/__init__.py +0 -1
  10. {lql_cli-0.3.0 → lql_cli-0.4.0}/.claude/settings.local.json +0 -0
  11. {lql_cli-0.3.0 → lql_cli-0.4.0}/.gitignore +0 -0
  12. {lql_cli-0.3.0 → lql_cli-0.4.0}/examples/agent-traces.jsonl +0 -0
  13. {lql_cli-0.3.0 → lql_cli-0.4.0}/package-lock.json +0 -0
  14. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/_opts.py +0 -0
  15. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/api.py +0 -0
  16. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/cli.py +0 -0
  17. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/__init__.py +0 -0
  18. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/annotations.py +0 -0
  19. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/auth.py +0 -0
  20. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/buckets.py +0 -0
  21. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/datasets.py +0 -0
  22. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/edits.py +0 -0
  23. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/evals.py +0 -0
  24. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/highlights.py +0 -0
  25. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/issues.py +0 -0
  26. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/reports.py +0 -0
  27. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/skills.py +0 -0
  28. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/spec.py +0 -0
  29. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/update.py +0 -0
  30. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/commands/workspaces.py +0 -0
  31. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/config.py +0 -0
  32. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/output.py +0 -0
  33. {lql_cli-0.3.0 → lql_cli-0.4.0}/src/lql/sessions.py +0 -0
  34. {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.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 or a platform dataset ID. No browser, and
153
- nothing to forward over SSH — it's just the terminal.
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 or a platform dataset ID. No browser, and
138
- nothing to forward over SSH — it's just the terminal.
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lql-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "lql — CLI for the Liquid DataViewer platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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, or a dataset ID")],
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 or a platform dataset ID. Local
608
- files are loaded whole; platform datasets are fetched and paged lazily as
609
- you navigate (--limit is the page size). Works over plain SSH — no browser
610
- or port-forward needed.
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
- row_source = tui_mod.RowSource(initial=_load_local(local_path))
626
- start_idx = max(0, offset)
627
- index_base = 0 # local rows are absolute; the cursor just starts at offset
628
- view_title = title or local_path.name
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
- client = ApiClient(profile=profile, api_url=api_url)
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
- tui_mod.run(row_source, view_title, forced_cols=column or None, start_idx=start_idx, index_base=index_base)
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,
@@ -315,7 +315,7 @@ wheels = [
315
315
 
316
316
  [[package]]
317
317
  name = "lql-cli"
318
- version = "0.2.1"
318
+ version = "0.4.0"
319
319
  source = { editable = "." }
320
320
  dependencies = [
321
321
  { name = "httpx" },
@@ -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