lql-cli 0.2.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
lql/_opts.py ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Annotated, Optional
2
+
3
+ import typer
4
+
5
+ ProfileOpt = Annotated[Optional[str], typer.Option("--profile", help="Profile to use")]
6
+ ApiUrlOpt = Annotated[Optional[str], typer.Option("--api-url", help="Override API base URL")]
7
+ JsonOpt = Annotated[bool, typer.Option("--json", help="Output raw JSON")]
lql/api.py ADDED
@@ -0,0 +1,69 @@
1
+ from typing import Optional
2
+
3
+ import httpx
4
+ import typer
5
+
6
+ from .config import get_api_url, get_token, validate_api_url
7
+ from .output import print_error
8
+
9
+ # Maps HTTP status -> process exit code (parity with the TS client).
10
+ _EXIT_MAP = {401: 2, 403: 2, 404: 3, 409: 4}
11
+
12
+
13
+ class ApiClient:
14
+ def __init__(self, profile: Optional[str] = None, api_url: Optional[str] = None):
15
+ # Validate an explicitly-passed --api-url too; get_api_url() validates
16
+ # the env/profile path.
17
+ base = validate_api_url(api_url) if api_url else get_api_url(profile)
18
+ token = get_token(profile)
19
+ if not token:
20
+ print_error(
21
+ "Not authenticated. Run `lql login` or set LQL_API_KEY environment variable.",
22
+ "unauthenticated",
23
+ )
24
+ raise typer.Exit(2)
25
+ self._client = httpx.Client(
26
+ base_url=base.rstrip("/"),
27
+ headers={"Authorization": f"Bearer {token}"},
28
+ timeout=60.0,
29
+ )
30
+
31
+ def _request(self, method: str, path: str, *, params=None, json=None) -> httpx.Response:
32
+ try:
33
+ resp = self._client.request(method, path, params=params, json=json)
34
+ except httpx.RequestError as e:
35
+ print_error(str(e) or "Network error", "network_error")
36
+ raise typer.Exit(1)
37
+
38
+ if resp.status_code >= 400:
39
+ body = None
40
+ try:
41
+ body = resp.json()
42
+ except Exception:
43
+ body = None
44
+ message = None
45
+ if isinstance(body, dict):
46
+ if isinstance(body.get("detail"), str):
47
+ message = body["detail"]
48
+ elif isinstance(body.get("message"), str):
49
+ message = body["message"]
50
+ message = message or f"Request failed (HTTP {resp.status_code})"
51
+ print_error(message, f"http_{resp.status_code}")
52
+ code = _EXIT_MAP.get(resp.status_code, 5 if resp.status_code >= 500 else 1)
53
+ raise typer.Exit(code)
54
+ return resp
55
+
56
+ def get(self, path, *, params=None):
57
+ return self._request("GET", path, params=params)
58
+
59
+ def post(self, path, *, params=None, json=None):
60
+ return self._request("POST", path, params=params, json=json)
61
+
62
+ def put(self, path, *, params=None, json=None):
63
+ return self._request("PUT", path, params=params, json=json)
64
+
65
+ def patch(self, path, *, params=None, json=None):
66
+ return self._request("PATCH", path, params=params, json=json)
67
+
68
+ def delete(self, path, *, params=None):
69
+ return self._request("DELETE", path, params=params)
lql/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ import sys
2
+ from typing import Annotated, Optional
3
+
4
+ import typer
5
+
6
+ from . import __version__
7
+ from .output import print_error
8
+ from .commands import (
9
+ annotations,
10
+ auth,
11
+ buckets,
12
+ datasets,
13
+ edits,
14
+ evals,
15
+ highlights,
16
+ instructions,
17
+ issues,
18
+ reports,
19
+ skills,
20
+ spec,
21
+ workspaces,
22
+ )
23
+
24
+ app = typer.Typer(
25
+ name="lql",
26
+ help=(
27
+ "CLI for the Liquid DataViewer platform — scriptable access for agents and humans.\n\n"
28
+ "If you are an AI agent: run `lql instructions` to get a full reference with all "
29
+ "commands, flags, examples, and common workflows in a single read."
30
+ ),
31
+ no_args_is_help=True,
32
+ add_completion=False,
33
+ )
34
+
35
+
36
+ def _version_callback(value: bool) -> None:
37
+ if value:
38
+ sys.stdout.write(f"{__version__}\n")
39
+ raise typer.Exit()
40
+
41
+
42
+ @app.callback()
43
+ def main_callback(
44
+ version: Annotated[
45
+ Optional[bool],
46
+ typer.Option("--version", callback=_version_callback, is_eager=True, help="Show version and exit"),
47
+ ] = None,
48
+ ) -> None:
49
+ pass
50
+
51
+
52
+ # Command groups
53
+ auth.register(app) # adds `auth` sub-app + top-level login/logout/whoami aliases
54
+ app.add_typer(workspaces.app, name="workspaces")
55
+ app.add_typer(datasets.app, name="datasets")
56
+ app.add_typer(evals.app, name="eval")
57
+ app.add_typer(edits.app, name="edits")
58
+ app.add_typer(spec.app, name="spec")
59
+ app.add_typer(annotations.app, name="annotations")
60
+ app.add_typer(highlights.app, name="highlights")
61
+ app.add_typer(issues.app, name="issues")
62
+ app.add_typer(reports.app, name="reports")
63
+ app.add_typer(buckets.app, name="buckets")
64
+
65
+ # Single commands
66
+ app.command("instructions", help="Print a full reference for agents and humans")(instructions.instructions)
67
+ app.add_typer(skills.app, name="skills")
68
+
69
+
70
+ @app.command("update")
71
+ def update() -> None:
72
+ """Show how to update lql to the latest version."""
73
+ sys.stdout.write(
74
+ "To update lql, use the tool you installed it with:\n"
75
+ " uv tool upgrade lql # if installed via `uv tool install`\n"
76
+ " pipx upgrade lql # if installed via pipx\n"
77
+ " pip install --upgrade lql # if installed via pip\n"
78
+ )
79
+
80
+
81
+ def main() -> None:
82
+ # Mirror the TS top-level catch: unexpected errors surface as the documented
83
+ # machine-readable contract, not a traceback. Typer/Click exits (SystemExit)
84
+ # and KeyboardInterrupt are BaseException, so they pass through untouched.
85
+ try:
86
+ app()
87
+ except Exception as e: # noqa: BLE001
88
+ print_error(str(e) or "Unexpected error", "unexpected_error")
89
+ raise SystemExit(1)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
File without changes
@@ -0,0 +1,69 @@
1
+ import sys
2
+ from typing import Annotated, Optional
3
+
4
+ import typer
5
+
6
+ from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
7
+ from ..api import ApiClient
8
+ from ..output import print_json, print_table
9
+ from ..sessions import resolve_session_id
10
+ from ..util import q
11
+
12
+ app = typer.Typer(help="Manage annotations")
13
+
14
+ SessionOpt = Annotated[Optional[str], typer.Option("--session", help="Target a specific review session (advanced)")]
15
+
16
+
17
+ @app.command("list")
18
+ def list_annotations(
19
+ dataset_id: Annotated[str, typer.Argument(help="Dataset ID")],
20
+ session: SessionOpt = None,
21
+ json_out: JsonOpt = False,
22
+ profile: ProfileOpt = None,
23
+ api_url: ApiUrlOpt = None,
24
+ ) -> None:
25
+ """List annotations for a dataset."""
26
+ client = ApiClient(profile=profile, api_url=api_url)
27
+ session_id = resolve_session_id(client, dataset_id, session)
28
+ items = client.get(f"/v1/sessions/{q(session_id)}/annotations").json()
29
+ print_table(
30
+ ["ID", "Row", "Rating", "Note"],
31
+ [
32
+ [
33
+ a.get("id") or "",
34
+ a.get("row_external_id") or "",
35
+ a.get("rating") if a.get("rating") is not None else "",
36
+ a.get("note") or "",
37
+ ]
38
+ for a in items
39
+ ],
40
+ json_out,
41
+ items,
42
+ )
43
+
44
+
45
+ @app.command("add")
46
+ def add(
47
+ dataset_id: Annotated[str, typer.Argument(help="Dataset ID")],
48
+ row: Annotated[str, typer.Option("--row", help="Row external ID")],
49
+ rating: Annotated[Optional[float], typer.Option("--rating", help="Rating value")] = None,
50
+ note: Annotated[Optional[str], typer.Option("--note", help="Note text")] = None,
51
+ session: SessionOpt = None,
52
+ json_out: JsonOpt = False,
53
+ profile: ProfileOpt = None,
54
+ api_url: ApiUrlOpt = None,
55
+ ) -> None:
56
+ """Add an annotation to a dataset row."""
57
+ client = ApiClient(profile=profile, api_url=api_url)
58
+ session_id = resolve_session_id(client, dataset_id, session)
59
+ body: dict = {"row_external_id": row}
60
+ if rating is not None:
61
+ # Match JS Number(): send an int when the value is integral (rating 1, not 1.0).
62
+ body["rating"] = int(rating) if float(rating).is_integer() else rating
63
+ if note is not None:
64
+ body["note"] = note
65
+ data = client.post(f"/v1/sessions/{q(session_id)}/annotations", json=body).json()
66
+ if json_out:
67
+ print_json(data)
68
+ else:
69
+ sys.stdout.write(f"Created annotation: {data.get('id', 'ok')}\n")
lql/commands/auth.py ADDED
@@ -0,0 +1,162 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import webbrowser
5
+ from typing import Annotated, Optional
6
+
7
+ import httpx
8
+ import typer
9
+
10
+ from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
11
+ from ..api import ApiClient
12
+ from ..config import get_api_url, read_config, validate_api_url, write_config
13
+ from ..output import print_error, print_json, print_table
14
+ from ..util import q
15
+
16
+ BROWSER_BASE_URL = "https://dataviewer.liquid.ai"
17
+
18
+
19
+ def _do_login(profile: Optional[str], api_url: Optional[str]) -> None:
20
+ api = validate_api_url(api_url) if api_url else get_api_url(profile)
21
+ profile_name = profile or "default"
22
+
23
+ env_token = os.environ.get("LQL_API_KEY")
24
+ if env_token:
25
+ config = read_config() or {"current_profile": "default", "profiles": {}}
26
+ existing = config["profiles"].get(profile_name, {})
27
+ config["profiles"][profile_name] = {**existing, "token": env_token, "api_url": api}
28
+ config["current_profile"] = profile_name
29
+ write_config(config)
30
+ sys.stdout.write(f'Stored LQL_API_KEY in profile "{profile_name}".\n')
31
+ return
32
+
33
+ # Create a CLI auth session
34
+ try:
35
+ res = httpx.post(f"{api}/v1/cli-auth/sessions", timeout=30.0)
36
+ res.raise_for_status()
37
+ session_data = res.json()
38
+ except Exception as err:
39
+ detail = None
40
+ if isinstance(err, httpx.HTTPStatusError):
41
+ try:
42
+ detail = err.response.json().get("detail")
43
+ except Exception:
44
+ detail = None
45
+ print_error(detail or str(err) or "Failed to start auth session", "auth_session_failed")
46
+ raise typer.Exit(1)
47
+
48
+ session_id = session_data["session_id"]
49
+ browser_code = session_data["browser_code"]
50
+ browser_url = f"{BROWSER_BASE_URL}/cli-auth?code={browser_code}&session={session_id}"
51
+
52
+ try:
53
+ webbrowser.open(browser_url)
54
+ except Exception:
55
+ pass
56
+ sys.stdout.write(f"Opening browser... if it didn't open, visit:\n{browser_url}\n")
57
+
58
+ deadline = time.monotonic() + 120
59
+ delay = 3.0
60
+ while time.monotonic() < deadline:
61
+ time.sleep(delay)
62
+ delay = min(delay * 1.5, 15.0)
63
+ try:
64
+ poll = httpx.get(f"{api}/v1/cli-auth/sessions/{session_id}", timeout=30.0).json()
65
+ except Exception:
66
+ continue
67
+
68
+ status = poll.get("status")
69
+ if status == "authorized":
70
+ config = read_config() or {"current_profile": "default", "profiles": {}}
71
+ config["profiles"][profile_name] = {
72
+ "token": poll.get("token"),
73
+ "key_id": poll.get("key_id"),
74
+ "api_url": api,
75
+ }
76
+ config["current_profile"] = profile_name
77
+ write_config(config)
78
+ sys.stdout.write(f'Authenticated! Profile "{profile_name}" saved.\n')
79
+ return
80
+ if status == "denied":
81
+ print_error("Authentication was denied.", "auth_denied")
82
+ raise typer.Exit(2)
83
+ if status == "expired":
84
+ print_error("Authentication session expired.", "auth_expired")
85
+ raise typer.Exit(2)
86
+
87
+ print_error("Authentication timed out after 120 seconds.", "auth_timeout")
88
+ raise typer.Exit(2)
89
+
90
+
91
+ def login(
92
+ profile: Annotated[str, typer.Option("--profile", help="Profile name to store credentials in")] = "default",
93
+ api_url: ApiUrlOpt = None,
94
+ ) -> None:
95
+ """Authenticate with the DataViewer platform."""
96
+ _do_login(profile, api_url)
97
+
98
+
99
+ def logout(
100
+ profile: ProfileOpt = None,
101
+ api_url: ApiUrlOpt = None,
102
+ ) -> None:
103
+ """Log out and revoke the current API key."""
104
+ config = read_config()
105
+ if not config:
106
+ print_error("No config found. Already logged out.", "no_config")
107
+ raise typer.Exit(2)
108
+ profile_name = profile or config.get("current_profile") or "default"
109
+ prof = config["profiles"].get(profile_name)
110
+
111
+ if prof and prof.get("key_id") and prof.get("token"):
112
+ try:
113
+ client = ApiClient(profile=profile_name, api_url=api_url)
114
+ client.delete(f"/v1/api-keys/{q(prof['key_id'])}")
115
+ except typer.Exit:
116
+ sys.stderr.write("Warning: could not revoke API key (already deleted or expired).\n")
117
+ else:
118
+ sys.stderr.write("Warning: no key_id in config — skipping remote key revocation.\n")
119
+
120
+ config["profiles"].pop(profile_name, None)
121
+ if config.get("current_profile") == profile_name:
122
+ remaining = list(config["profiles"].keys())
123
+ config["current_profile"] = remaining[0] if remaining else "default"
124
+ write_config(config)
125
+ sys.stdout.write(f'Logged out of profile "{profile_name}".\n')
126
+
127
+
128
+ def whoami(
129
+ json_out: JsonOpt = False,
130
+ profile: ProfileOpt = None,
131
+ api_url: ApiUrlOpt = None,
132
+ ) -> None:
133
+ """Show the currently authenticated user."""
134
+ client = ApiClient(profile=profile, api_url=api_url)
135
+ me = client.get("/v1/me").json()
136
+ if json_out:
137
+ print_json(me)
138
+ else:
139
+ print_table(
140
+ ["Field", "Value"],
141
+ [
142
+ ["ID", me.get("id") or ""],
143
+ ["Email", me.get("email") or ""],
144
+ ["Name", me.get("name") or me.get("display_name") or ""],
145
+ ],
146
+ False,
147
+ [me],
148
+ )
149
+
150
+
151
+ auth_app = typer.Typer(help="Authentication commands")
152
+ auth_app.command("login")(login)
153
+ auth_app.command("logout")(logout)
154
+ auth_app.command("whoami")(whoami)
155
+
156
+
157
+ def register(root: typer.Typer) -> None:
158
+ root.add_typer(auth_app, name="auth")
159
+ # Top-level aliases: `lql login` / `lql logout` / `lql whoami`
160
+ root.command("login", help="Authenticate with the DataViewer platform")(login)
161
+ root.command("logout", help="Log out (alias for `lql auth logout`)")(logout)
162
+ root.command("whoami", help="Show the currently authenticated user")(whoami)
@@ -0,0 +1,190 @@
1
+ import json
2
+ import sys
3
+ from typing import Annotated, Optional
4
+
5
+ import typer
6
+
7
+ from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
8
+ from ..api import ApiClient
9
+ from ..output import print_json, print_table
10
+ from ..util import q
11
+
12
+ app = typer.Typer(help="Manage S3 and Hugging Face buckets")
13
+
14
+
15
+ @app.command("list")
16
+ def list_buckets(json_out: JsonOpt = False, profile: ProfileOpt = None, api_url: ApiUrlOpt = None) -> None:
17
+ """List S3 buckets."""
18
+ client = ApiClient(profile=profile, api_url=api_url)
19
+ items = client.get("/v1/s3-buckets").json()
20
+ print_table(
21
+ ["ID", "Name", "Region"],
22
+ [[b.get("id") or "", b.get("name") or b.get("bucket_name") or "", b.get("region") or ""] for b in items],
23
+ json_out,
24
+ items,
25
+ )
26
+
27
+
28
+ @app.command("show")
29
+ def show(
30
+ id: Annotated[str, typer.Argument(help="Bucket ID")],
31
+ json_out: JsonOpt = False,
32
+ profile: ProfileOpt = None,
33
+ api_url: ApiUrlOpt = None,
34
+ ) -> None:
35
+ """Show S3 bucket details."""
36
+ client = ApiClient(profile=profile, api_url=api_url)
37
+ b = client.get(f"/v1/s3-buckets/{q(id)}").json()
38
+ if json_out:
39
+ print_json(b)
40
+ else:
41
+ print_table(
42
+ ["Field", "Value"],
43
+ [
44
+ ["ID", b.get("id") or ""],
45
+ ["Name", b.get("name") or b.get("bucket_name") or ""],
46
+ ["Region", b.get("region") or ""],
47
+ ["Endpoint", b.get("endpoint") or ""],
48
+ ],
49
+ False,
50
+ [b],
51
+ )
52
+
53
+
54
+ @app.command("probe")
55
+ def probe(
56
+ id: Annotated[str, typer.Argument(help="Bucket ID")],
57
+ json_out: JsonOpt = False,
58
+ profile: ProfileOpt = None,
59
+ api_url: ApiUrlOpt = None,
60
+ ) -> None:
61
+ """Probe an S3 bucket to test connectivity."""
62
+ client = ApiClient(profile=profile, api_url=api_url)
63
+ data = client.post(f"/v1/s3-buckets/{q(id)}/probe").json()
64
+ if json_out:
65
+ print_json(data)
66
+ else:
67
+ sys.stdout.write(f"Probe result: {data.get('status') or json.dumps(data)}\n")
68
+
69
+
70
+ @app.command("objects")
71
+ def objects(
72
+ id: Annotated[str, typer.Argument(help="Bucket ID")],
73
+ prefix: Annotated[Optional[str], typer.Option("--prefix", help="Key prefix filter")] = None,
74
+ json_out: JsonOpt = False,
75
+ profile: ProfileOpt = None,
76
+ api_url: ApiUrlOpt = None,
77
+ ) -> None:
78
+ """List objects in an S3 bucket."""
79
+ client = ApiClient(profile=profile, api_url=api_url)
80
+ params = {}
81
+ if prefix:
82
+ params["prefix"] = prefix
83
+ data = client.get(f"/v1/s3-buckets/{q(id)}/objects", params=params).json()
84
+ if json_out:
85
+ print_json(data)
86
+ else:
87
+ items = (data.get("objects") if isinstance(data, dict) else data) or []
88
+ print_table(
89
+ ["Key", "Size", "Last Modified"],
90
+ [
91
+ [
92
+ o.get("key") or o.get("Key") or "",
93
+ o.get("size") or o.get("Size") or "",
94
+ o.get("last_modified") or o.get("LastModified") or "",
95
+ ]
96
+ for o in items
97
+ ],
98
+ False,
99
+ items,
100
+ )
101
+
102
+
103
+ @app.command("attach")
104
+ def attach(
105
+ bucket_id: Annotated[str, typer.Argument(help="Bucket ID")],
106
+ workspace: Annotated[str, typer.Option("--workspace", help="Workspace ID")],
107
+ json_out: JsonOpt = False,
108
+ profile: ProfileOpt = None,
109
+ api_url: ApiUrlOpt = None,
110
+ ) -> None:
111
+ """Attach a bucket to a workspace."""
112
+ client = ApiClient(profile=profile, api_url=api_url)
113
+ data = client.post(f"/v1/s3-buckets/{q(bucket_id)}/attach", json={"workspace_id": workspace}).json()
114
+ if json_out:
115
+ print_json(data)
116
+ else:
117
+ sys.stdout.write(f"Attached bucket {bucket_id} to workspace {workspace}.\n")
118
+
119
+
120
+ @app.command("detach")
121
+ def detach(
122
+ bucket_id: Annotated[str, typer.Argument(help="Bucket ID")],
123
+ workspace: Annotated[str, typer.Option("--workspace", help="Workspace ID")],
124
+ profile: ProfileOpt = None,
125
+ api_url: ApiUrlOpt = None,
126
+ ) -> None:
127
+ """Detach a bucket from a workspace."""
128
+ client = ApiClient(profile=profile, api_url=api_url)
129
+ client.post(f"/v1/s3-buckets/{q(bucket_id)}/detach", json={"workspace_id": workspace})
130
+ sys.stdout.write(f"Detached bucket {bucket_id} from workspace {workspace}.\n")
131
+
132
+
133
+ @app.command("list-hf")
134
+ def list_hf(json_out: JsonOpt = False, profile: ProfileOpt = None, api_url: ApiUrlOpt = None) -> None:
135
+ """List Hugging Face bucket connections."""
136
+ client = ApiClient(profile=profile, api_url=api_url)
137
+ items = client.get("/v1/hf-buckets").json()
138
+ print_table(
139
+ ["ID", "Bucket", "Label"],
140
+ [[b.get("id") or "", b.get("owner_bucket") or "", b.get("label") or ""] for b in items],
141
+ json_out,
142
+ items,
143
+ )
144
+
145
+
146
+ @app.command("connect-hf")
147
+ def connect_hf(
148
+ bucket: Annotated[str, typer.Argument(metavar="owner/bucket", help="Hugging Face bucket")],
149
+ workspace: Annotated[str, typer.Option("--workspace", help="Workspace ID")],
150
+ label: Annotated[Optional[str], typer.Option("--label", help="Display label")] = None,
151
+ hf_key: Annotated[Optional[str], typer.Option("--hf-key", help="HF API key id (omit for public / server token)")] = None,
152
+ json_out: JsonOpt = False,
153
+ profile: ProfileOpt = None,
154
+ api_url: ApiUrlOpt = None,
155
+ ) -> None:
156
+ """Connect a Hugging Face bucket and attach it to a workspace."""
157
+ client = ApiClient(profile=profile, api_url=api_url)
158
+ body: dict = {"workspace_id": workspace, "bucket": bucket}
159
+ if label:
160
+ body["label"] = label
161
+ if hf_key:
162
+ body["hf_api_key_id"] = hf_key
163
+ data = client.post("/v1/hf-buckets", json=body).json()
164
+ if json_out:
165
+ print_json(data)
166
+ else:
167
+ sys.stdout.write(f"Connected HF bucket: {data.get('id')}\n")
168
+
169
+
170
+ @app.command("create-dataset")
171
+ def create_dataset(
172
+ bucket_id: Annotated[str, typer.Argument(help="HF bucket ID")],
173
+ workspace: Annotated[str, typer.Option("--workspace", help="Workspace ID")],
174
+ key: Annotated[str, typer.Option("--key", help="Path or glob within the bucket (e.g. data/*.parquet)")],
175
+ name: Annotated[Optional[str], typer.Option("--name", help="Display name")] = None,
176
+ json_out: JsonOpt = False,
177
+ profile: ProfileOpt = None,
178
+ api_url: ApiUrlOpt = None,
179
+ ) -> None:
180
+ """Create a dataset from a connected HF bucket."""
181
+ client = ApiClient(profile=profile, api_url=api_url)
182
+ body: dict = {"workspace_id": workspace, "hf_bucket_key": key}
183
+ if name:
184
+ body["display_name"] = name
185
+ data = client.post(f"/v1/hf-buckets/{q(bucket_id)}/datasets", json=body).json()
186
+ if json_out:
187
+ print_json(data)
188
+ else:
189
+ status = f" ({data.get('sync_status')})" if data.get("sync_status") else ""
190
+ sys.stdout.write(f"Created dataset: {data.get('id')}{status}\n")