sference-cli 0.0.2__tar.gz → 0.0.3__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.
@@ -8,3 +8,5 @@ build/
8
8
  .venv/
9
9
  *.egg
10
10
  .pytest_cache/
11
+ cli/.env
12
+
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: sference-cli
3
+ Version: 0.0.3
4
+ Summary: sference command-line interface
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: sference-sdk>=0.0.3
7
+ Requires-Dist: typer>=0.24.1
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # sference CLI
2
11
 
3
12
  Command-line interface for the sference batch API (`sference`). It uses the Python SDK (`sference-sdk`) and is published on PyPI as `sference-cli`.
@@ -56,7 +65,7 @@ sference stream create --name "my-stream" --window 24h
56
65
  sference stream list
57
66
  sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
58
67
  sference stream status --stream-id <stream_id>
59
- sference stream tail --stream-id <stream_id>
68
+ sference responses tail --stream-id <stream_id>
60
69
  ```
61
70
 
62
71
  ### cURL: OpenAI-compatible `/v1/responses`
@@ -87,7 +96,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
87
96
  | `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
88
97
  | `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
89
98
  | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
90
- | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **stream event** tail checkpoints (default `~/.sference/stream_checkpoints.json`) |
99
+ | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
91
100
 
92
101
  ## Commands
93
102
 
@@ -113,9 +122,17 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
113
122
 
114
123
  Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
115
124
 
125
+ ### Responses (`/v1/responses`)
126
+
127
+ | Command | Description |
128
+ |---------|-------------|
129
+ | `sference responses create` | Create one response (`--model`, `--content`, optional `--wait`, `--poll-ms`, `--timeout-s`) |
130
+ | `sference responses result` | Poll until terminal state (`--id`, `--poll-ms`) |
131
+ | `sference responses tail` | Print completion events as JSONL via `GET /v1/responses/events` (optional `--stream-id` to scope to a stream; omit for non-stream completions). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
132
+
116
133
  ### Stream (first-class streams API)
117
134
 
118
- Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination (`GET /v1/streams/{id}/events`). New API keys include `streams:read` and `streams:write`; older keys keep their existing scopes.
135
+ Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination on **`GET /v1/responses/events`** (pass **`stream_id`** when scoping to a stream). Authenticate with your **secret API key** like other `/v1` calls.
119
136
 
120
137
  | Command | Description |
121
138
  |---------|-------------|
@@ -125,7 +142,6 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
125
142
  | `sference stream submit` | Create responses from JSONL via `POST /v1/responses` per line (`metadata.stream_id` set automatically; `--stream-id`, `--input-file`, `--model` required for content-only lines) — per line: OpenAI batch-style `{custom_id?, method, url, body}` or content-only `{content}` |
126
143
  | `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
127
144
  | `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
128
- | `sference stream tail` | Print events as JSONL; checkpoints after each event (override with `SFERENCE_STREAM_CHECKPOINTS`). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
129
145
 
130
146
  Example JSONL lines for `stream submit` (both accepted):
131
147
 
@@ -211,6 +227,6 @@ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)**
211
227
 
212
228
  - **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
213
229
  - **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
214
- - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_stream_events` / `list_stream_events` for stream tailing
230
+ - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
215
231
 
216
232
  That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: sference-cli
3
- Version: 0.0.2
4
- Summary: sference command-line interface
5
- Requires-Python: >=3.12
6
- Requires-Dist: sference-sdk>=0.0.1
7
- Requires-Dist: typer>=0.24.1
8
- Description-Content-Type: text/markdown
9
-
10
1
  # sference CLI
11
2
 
12
3
  Command-line interface for the sference batch API (`sference`). It uses the Python SDK (`sference-sdk`) and is published on PyPI as `sference-cli`.
@@ -65,7 +56,7 @@ sference stream create --name "my-stream" --window 24h
65
56
  sference stream list
66
57
  sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
67
58
  sference stream status --stream-id <stream_id>
68
- sference stream tail --stream-id <stream_id>
59
+ sference responses tail --stream-id <stream_id>
69
60
  ```
70
61
 
71
62
  ### cURL: OpenAI-compatible `/v1/responses`
@@ -96,7 +87,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
96
87
  | `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
97
88
  | `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
98
89
  | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
99
- | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **stream event** tail checkpoints (default `~/.sference/stream_checkpoints.json`) |
90
+ | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
100
91
 
101
92
  ## Commands
102
93
 
@@ -122,9 +113,17 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
122
113
 
123
114
  Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
124
115
 
116
+ ### Responses (`/v1/responses`)
117
+
118
+ | Command | Description |
119
+ |---------|-------------|
120
+ | `sference responses create` | Create one response (`--model`, `--content`, optional `--wait`, `--poll-ms`, `--timeout-s`) |
121
+ | `sference responses result` | Poll until terminal state (`--id`, `--poll-ms`) |
122
+ | `sference responses tail` | Print completion events as JSONL via `GET /v1/responses/events` (optional `--stream-id` to scope to a stream; omit for non-stream completions). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
123
+
125
124
  ### Stream (first-class streams API)
126
125
 
127
- Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination (`GET /v1/streams/{id}/events`). New API keys include `streams:read` and `streams:write`; older keys keep their existing scopes.
126
+ Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination on **`GET /v1/responses/events`** (pass **`stream_id`** when scoping to a stream). Authenticate with your **secret API key** like other `/v1` calls.
128
127
 
129
128
  | Command | Description |
130
129
  |---------|-------------|
@@ -134,7 +133,6 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
134
133
  | `sference stream submit` | Create responses from JSONL via `POST /v1/responses` per line (`metadata.stream_id` set automatically; `--stream-id`, `--input-file`, `--model` required for content-only lines) — per line: OpenAI batch-style `{custom_id?, method, url, body}` or content-only `{content}` |
135
134
  | `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
136
135
  | `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
137
- | `sference stream tail` | Print events as JSONL; checkpoints after each event (override with `SFERENCE_STREAM_CHECKPOINTS`). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
138
136
 
139
137
  Example JSONL lines for `stream submit` (both accepted):
140
138
 
@@ -220,6 +218,6 @@ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)**
220
218
 
221
219
  - **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
222
220
  - **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
223
- - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_stream_events` / `list_stream_events` for stream tailing
221
+ - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
224
222
 
225
223
  That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "sference-cli"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "sference command-line interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
8
8
  "typer>=0.24.1",
9
- "sference-sdk>=0.0.1",
9
+ "sference-sdk>=0.0.3",
10
10
  ]
11
11
 
12
12
  [project.scripts]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import logging
4
5
  import os
5
6
  import sys
6
7
  import time
@@ -18,14 +19,18 @@ from . import stream_cache as stream_cache_mod
18
19
 
19
20
  _T = TypeVar("_T")
20
21
 
22
+ _logger = logging.getLogger(__name__)
23
+
21
24
 
22
25
  app = typer.Typer(help="sference CLI", invoke_without_command=True)
23
26
  auth_app = typer.Typer(help="Auth commands", invoke_without_command=True)
24
27
  batch_app = typer.Typer(help="Batch commands", invoke_without_command=True)
25
28
  stream_app = typer.Typer(help="Stream commands", invoke_without_command=True)
29
+ responses_app = typer.Typer(help="Responses commands", invoke_without_command=True)
26
30
  app.add_typer(auth_app, name="auth")
27
31
  app.add_typer(batch_app, name="batch")
28
32
  app.add_typer(stream_app, name="stream")
33
+ app.add_typer(responses_app, name="responses")
29
34
 
30
35
  CREDENTIALS_PATH = Path.home() / ".sference" / "credentials.json"
31
36
 
@@ -52,6 +57,12 @@ def _read_token() -> str | None:
52
57
 
53
58
 
54
59
  def _client(base_url: Optional[str] = None) -> SferenceClient:
60
+ # If the caller didn't explicitly pass --base-url, allow SFERENCE_BASE_URL
61
+ # to override the default.
62
+ env_base_url = os.environ.get("SFERENCE_BASE_URL")
63
+ if env_base_url and (base_url is None or base_url == "https://api.sference.com"):
64
+ _logger.info(f"Using SFERENCE_BASE_URL: {env_base_url}")
65
+ base_url = env_base_url
55
66
  return SferenceClient(base_url=base_url, api_key=_read_token())
56
67
 
57
68
 
@@ -121,6 +132,26 @@ def _print(value: object, as_json: bool) -> None:
121
132
  typer.echo(value)
122
133
 
123
134
 
135
+ def _wait_for_response(
136
+ client: SferenceClient, response_id: str, *, poll_ms: int, timeout_s: int
137
+ ) -> Any:
138
+ """Poll GET /v1/responses/{id} until terminal status or timeout."""
139
+ deadline = time.time() + max(1, timeout_s)
140
+ last: Any = None
141
+ while True:
142
+ last = _call_api(lambda: client.get_response(response_id))
143
+ if hasattr(last, "model_dump"):
144
+ d = last.model_dump()
145
+ status = d.get("status") if isinstance(d, dict) else None
146
+ else:
147
+ status = getattr(last, "status", None)
148
+ if status in ("completed", "failed", "cancelled"):
149
+ return last
150
+ if time.time() >= deadline:
151
+ return last
152
+ time.sleep(max(10, poll_ms) / 1000.0)
153
+
154
+
124
155
  def _mvp_batch_window_only(value: str) -> str:
125
156
  if value != "24h":
126
157
  raise typer.BadParameter('Only "24h" is supported in MVP.', param_hint="--window")
@@ -165,6 +196,14 @@ def _stream_root(ctx: typer.Context) -> None:
165
196
  raise typer.Exit(code=0)
166
197
 
167
198
 
199
+ @responses_app.callback()
200
+ def _responses_root(ctx: typer.Context) -> None:
201
+ """Print help when invoked without a command."""
202
+ if ctx.invoked_subcommand is None:
203
+ typer.echo(ctx.get_help())
204
+ raise typer.Exit(code=0)
205
+
206
+
168
207
  @auth_app.command("login")
169
208
  def auth_login(
170
209
  api_key: Optional[str] = typer.Option(
@@ -172,6 +211,12 @@ def auth_login(
172
211
  "--api-key",
173
212
  help="API key (sk_...) or JWT. Non-interactive: saves immediately (e.g. CI).",
174
213
  ),
214
+ as_json: bool = typer.Option(False, "--json", help="Print authenticated user as JSON after validation."),
215
+ validate: bool = typer.Option(
216
+ True,
217
+ "--validate/--no-validate",
218
+ help="Validate the credential by calling GET /v1/auth/me and print the authenticated user.",
219
+ ),
175
220
  console_url: Optional[str] = typer.Option(
176
221
  None,
177
222
  "--console-url",
@@ -188,6 +233,16 @@ def auth_login(
188
233
  raise typer.Exit(code=1)
189
234
  _write_token(key)
190
235
  typer.echo(f"Credentials saved to {CREDENTIALS_PATH}")
236
+ if validate:
237
+ client = _client(None)
238
+ me = _call_api(client.get_me)
239
+ if as_json:
240
+ _print(me, True)
241
+ else:
242
+ if isinstance(me, dict) and "username" in me and "role" in me:
243
+ typer.echo(f"Authenticated as {me['username']} ({me['role']}).")
244
+ else:
245
+ typer.echo(f"Authenticated. {me}")
191
246
  return
192
247
 
193
248
  base = (console_url or DEFAULT_CONSOLE_URL).rstrip("/")
@@ -213,6 +268,16 @@ def auth_login(
213
268
  raise typer.Exit(code=1)
214
269
  _write_token(token.strip())
215
270
  typer.echo(f"Credentials saved to {CREDENTIALS_PATH}")
271
+ if validate:
272
+ client = _client(None)
273
+ me = _call_api(client.get_me)
274
+ if as_json:
275
+ _print(me, True)
276
+ else:
277
+ if isinstance(me, dict) and "username" in me and "role" in me:
278
+ typer.echo(f"Authenticated as {me['username']} ({me['role']}).")
279
+ else:
280
+ typer.echo(f"Authenticated. {me}")
216
281
 
217
282
 
218
283
  @auth_app.command("me")
@@ -226,6 +291,113 @@ def auth_me(
226
291
  _print(me, as_json)
227
292
 
228
293
 
294
+ @responses_app.command("create")
295
+ def responses_create(
296
+ model: str = typer.Option(..., "--model"),
297
+ content: str = typer.Option(..., "--content"),
298
+ wait: bool = typer.Option(False, "--wait/--no-wait"),
299
+ poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval when --wait is enabled."),
300
+ timeout_s: int = typer.Option(60, "--timeout-s", help="Timeout in seconds when --wait is enabled."),
301
+ base_url: str = typer.Option("https://api.sference.com"),
302
+ ) -> None:
303
+ _ensure_api_credential()
304
+ client = _client(base_url)
305
+ resp = _call_api(
306
+ lambda: client.create_response(model=model, input=[{"role": "user", "content": content}])
307
+ )
308
+ if wait:
309
+ final = _wait_for_response(client, resp.id, poll_ms=poll_ms, timeout_s=timeout_s)
310
+ if getattr(final, "status", None) not in ("completed", "failed", "cancelled"):
311
+ _print(final.model_dump(), True)
312
+ raise typer.Exit(code=1)
313
+ resp = final
314
+ _print(resp.model_dump(), True)
315
+
316
+
317
+ @responses_app.command("result")
318
+ def responses_result(
319
+ id: str = typer.Option(..., "--id"),
320
+ poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval while waiting for completion."),
321
+ base_url: str = typer.Option("https://api.sference.com"),
322
+ ) -> None:
323
+ """Wait for a response to reach a terminal status and print the final JSON.
324
+
325
+ Use Ctrl-C to stop waiting.
326
+ """
327
+ _ensure_api_credential()
328
+ client = _client(base_url)
329
+ try:
330
+ while True:
331
+ resp = _call_api(lambda: client.get_response(id))
332
+ d = resp.model_dump()
333
+ status = d.get("status") if isinstance(d, dict) else None
334
+ if status in ("completed", "failed", "cancelled"):
335
+ _print(d, True)
336
+ return
337
+ time.sleep(max(10, poll_ms) / 1000.0)
338
+ except KeyboardInterrupt:
339
+ raise typer.Exit(code=130) from None
340
+
341
+
342
+ @responses_app.command("tail")
343
+ def responses_tail(
344
+ stream_id: Optional[str] = typer.Option(
345
+ None,
346
+ "--stream-id",
347
+ help="Optional stream UUID; omit to tail non-stream completion events only.",
348
+ ),
349
+ consumer: str = typer.Option("default", "--consumer", help="Checkpoint namespace for resume."),
350
+ from_latest: bool = typer.Option(
351
+ False,
352
+ "--from-latest",
353
+ help="Ignore saved checkpoint and start from the latest events page.",
354
+ ),
355
+ no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
356
+ poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
357
+ base_url: str = typer.Option("https://api.sference.com"),
358
+ as_json: bool = typer.Option(False, "--json"),
359
+ ) -> None:
360
+ """Print completion events as JSONL (GET /v1/responses/events). Ctrl-C to stop.
361
+
362
+ Omit --stream-id for standalone/batch completions; pass --stream-id to scope to a stream.
363
+ """
364
+ _ = as_json
365
+ _ensure_api_credential()
366
+ client = _client(base_url)
367
+ bu = base_url.rstrip("/")
368
+ ck = stream_id if stream_id else "__responses_events__"
369
+
370
+ if from_latest and not no_checkpoint:
371
+ clear_checkpoint(bu, ck, consumer)
372
+
373
+ last: str | None = None
374
+ if not no_checkpoint and not from_latest:
375
+ last = load_checkpoint(bu, ck, consumer)
376
+
377
+ try:
378
+ while True:
379
+
380
+ def fetch() -> Any:
381
+ return client.list_responses_events(
382
+ stream_id=stream_id,
383
+ limit=50,
384
+ starting_after=last,
385
+ wait_ms=min(poll_ms, 30000),
386
+ )
387
+
388
+ page = _call_api(fetch)
389
+ if not page.data:
390
+ time.sleep(max(1, poll_ms) / 1000.0)
391
+ continue
392
+ for ev in page.data:
393
+ typer.echo(json.dumps(ev.model_dump(), default=str))
394
+ last = ev.completion_id
395
+ if not no_checkpoint:
396
+ save_checkpoint(bu, ck, consumer, ev.completion_id)
397
+ except KeyboardInterrupt:
398
+ typer.echo("Interrupted.", err=True)
399
+ raise typer.Exit(code=130) from None
400
+
229
401
  @batch_app.command("list")
230
402
  def batch_list(
231
403
  base_url: str = typer.Option("https://api.sference.com"),
@@ -519,7 +691,8 @@ def stream_submit(
519
691
  typer.echo(f"Unrecognized line format: {line[:100]}", err=True)
520
692
  raise typer.Exit(code=1)
521
693
 
522
- resp = _call_api(lambda: client.create_response(body))
694
+ # SDK signature is keyword-only; pass the payload as kwargs.
695
+ resp = _call_api(lambda: client.create_response(**body))
523
696
  created_responses.append(resp)
524
697
 
525
698
  # Return stream detail-like response for compatibility
@@ -556,58 +729,6 @@ def stream_cancel(
556
729
  _print(st.model_dump(), as_json)
557
730
 
558
731
 
559
- @stream_app.command("tail")
560
- def stream_tail(
561
- stream_id: str = typer.Option(..., "--stream-id"),
562
- consumer: str = typer.Option("default", "--consumer", help="Checkpoint namespace for resume."),
563
- from_latest: bool = typer.Option(
564
- False,
565
- "--from-latest",
566
- help="Ignore saved checkpoint and start from the latest events page.",
567
- ),
568
- no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
569
- poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
570
- base_url: str = typer.Option("https://api.sference.com"),
571
- as_json: bool = typer.Option(False, "--json"),
572
- ) -> None:
573
- """Print stream result events as JSONL (stdout). Checkpoints after each event unless --no-checkpoint."""
574
- _ = as_json
575
- _ensure_api_credential()
576
- client = _client(base_url)
577
- bu = base_url.rstrip("/")
578
-
579
- if from_latest and not no_checkpoint:
580
- clear_checkpoint(bu, stream_id, consumer)
581
-
582
- last: str | None = None
583
- if not no_checkpoint and not from_latest:
584
- last = load_checkpoint(bu, stream_id, consumer)
585
-
586
- try:
587
- while True:
588
-
589
- def fetch() -> Any:
590
- return client.list_stream_events(
591
- stream_id,
592
- limit=50,
593
- starting_after=last,
594
- wait_ms=min(poll_ms, 30000),
595
- )
596
-
597
- page = _call_api(fetch)
598
- if not page.data:
599
- time.sleep(max(1, poll_ms) / 1000.0)
600
- continue
601
- for ev in page.data:
602
- typer.echo(json.dumps(ev.model_dump(), default=str))
603
- last = ev.completion_id
604
- if not no_checkpoint:
605
- save_checkpoint(bu, stream_id, consumer, ev.completion_id)
606
- except KeyboardInterrupt:
607
- typer.echo("Interrupted.", err=True)
608
- raise typer.Exit(code=130) from None
609
-
610
-
611
732
  @batch_app.command("download-results")
612
733
  def batch_download_results(
613
734
  batch_id: str = typer.Option(..., "--batch-id"),