content-cli 0.4.0__tar.gz → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: content-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Command-line client for the Content engine (talks to /api/v1).
5
5
  Project-URL: Homepage, https://github.com/LatentNoise/content
6
6
  Project-URL: Source, https://github.com/LatentNoise/content
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Multimedia :: Video
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.11
24
- Requires-Dist: content-sdk==0.4.0
24
+ Requires-Dist: content-sdk==0.6.0
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # `content` — Content CLI
@@ -95,3 +95,19 @@ content cancel <job_id> ; content retry <job_id>
95
95
 
96
96
  Global flags: `--api-url URL` and `--json` (raw JSON output) go before the
97
97
  subcommand. Exit code is non-zero on API errors or a failed watched job.
98
+
99
+ ## Exit codes when watching a job
100
+
101
+ `content ... --watch` and `content watch <job>` return the outcome, so a script
102
+ can chain on it (ADR 0021):
103
+
104
+ | Code | Meaning |
105
+ | --- | --- |
106
+ | `0` | `succeeded` — everything asked for was produced |
107
+ | `2` | `partially_succeeded` — some of it was, and at least one step failed |
108
+ | `1` | `failed` or `cancelled` |
109
+
110
+ `2` is deliberately distinct from `1`. A playlist that yielded five videos of
111
+ six, or a download whose delivery into your library was refused, is not a
112
+ failure — but it is not a success either, and a script that treats it as one
113
+ will quietly move on with missing files.
@@ -69,3 +69,19 @@ content cancel <job_id> ; content retry <job_id>
69
69
 
70
70
  Global flags: `--api-url URL` and `--json` (raw JSON output) go before the
71
71
  subcommand. Exit code is non-zero on API errors or a failed watched job.
72
+
73
+ ## Exit codes when watching a job
74
+
75
+ `content ... --watch` and `content watch <job>` return the outcome, so a script
76
+ can chain on it (ADR 0021):
77
+
78
+ | Code | Meaning |
79
+ | --- | --- |
80
+ | `0` | `succeeded` — everything asked for was produced |
81
+ | `2` | `partially_succeeded` — some of it was, and at least one step failed |
82
+ | `1` | `failed` or `cancelled` |
83
+
84
+ `2` is deliberately distinct from `1`. A playlist that yielded five videos of
85
+ six, or a download whose delivery into your library was refused, is not a
86
+ failure — but it is not a success either, and a script that treats it as one
87
+ will quietly move on with missing files.
@@ -16,6 +16,21 @@ from content_sdk.resources import TERMINAL_STATUSES
16
16
  from content_cli import __version__
17
17
  from content_cli.builders import audio_request, video_request
18
18
 
19
+ # Exit codes for a watched job (ADR 0021). Three outcomes, three codes: a
20
+ # script chaining on success must not proceed as if everything arrived, and it
21
+ # should still be able to tell "some of it worked" from "none of it did".
22
+ EXIT_OK = 0
23
+ EXIT_FAILED = 1
24
+ EXIT_PARTIAL = 2
25
+
26
+
27
+ def _exit_code_for(status: str) -> int:
28
+ if status == "succeeded":
29
+ return EXIT_OK
30
+ if status == "partially_succeeded":
31
+ return EXIT_PARTIAL
32
+ return EXIT_FAILED
33
+
19
34
 
20
35
  def _out(obj, as_json: bool) -> None:
21
36
  print(json.dumps(obj, indent=2, ensure_ascii=False) if as_json else obj)
@@ -29,8 +44,32 @@ def _print_job(job: dict) -> None:
29
44
 
30
45
 
31
46
  def _watch(client: ContentClient, job_id: str) -> str:
32
- """Stream events until the job reaches a terminal state."""
47
+ """Follow a job's events until it reaches a terminal state.
48
+
49
+ Uses the engine's event stream rather than asking again on a timer: a
50
+ forty-minute download deserves a progress line that moves when something
51
+ moves. The stream ends by itself when the job does.
52
+
53
+ Falls back to polling if the stream cannot be established — an old engine,
54
+ or a proxy that buffers SSE — because watching a job must not depend on it.
55
+ """
33
56
  seen = 0
57
+ try:
58
+ for event in client.stream_events(job_id):
59
+ seen = event["sequence"]
60
+ print(f" {seen:>3} {event['type']} {event['data'] or ''}")
61
+ except Exception as exc: # noqa: BLE001 — any stream failure falls back below
62
+ # Say so rather than degrade silently: a user who sees updates arrive
63
+ # every two seconds instead of instantly deserves to know the stream
64
+ # was refused, and by what.
65
+ print(
66
+ f"note: event stream unavailable ({exc}); polling instead", file=sys.stderr
67
+ )
68
+ return _watch_by_polling(client, job_id, seen)
69
+
70
+
71
+ def _watch_by_polling(client: ContentClient, job_id: str, seen: int = 0) -> str:
72
+ """The original loop, kept as the fallback and as the terminal read."""
34
73
  while True:
35
74
  for event in client.events(job_id, after_sequence=seen):
36
75
  seen = event.sequence
@@ -38,6 +77,11 @@ def _watch(client: ContentClient, job_id: str) -> str:
38
77
  status = client.get_job(job_id).status
39
78
  if status in TERMINAL_STATUSES:
40
79
  print(f"→ {status}")
80
+ if status == "partially_succeeded":
81
+ print(
82
+ f" some steps failed — inspect them with: content job {job_id}",
83
+ file=sys.stderr,
84
+ )
41
85
  return status
42
86
  time.sleep(2.0)
43
87
 
@@ -49,7 +93,7 @@ def _submit_and_maybe_watch(client: ContentClient, request: dict, args) -> int:
49
93
  print(job.id)
50
94
  if getattr(args, "watch", False):
51
95
  status = _watch(client, job.id)
52
- return 0 if status in ("succeeded", "partially_succeeded") else 1
96
+ return _exit_code_for(status)
53
97
  return 0
54
98
 
55
99
 
@@ -239,7 +283,7 @@ def run(argv: list[str], client: ContentClient) -> int:
239
283
  _out(job, True) if args.json else _print_job(job)
240
284
  elif cmd == "watch":
241
285
  status = _watch(client, args.job_id)
242
- return 0 if status in ("succeeded", "partially_succeeded") else 1
286
+ return _exit_code_for(status)
243
287
  elif cmd == "artifacts":
244
288
  arts = client.artifacts(args.job_id)
245
289
  if args.json:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "content-cli"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "Command-line client for the Content engine (talks to /api/v1)."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -24,7 +24,7 @@ classifiers = [
24
24
  # Pinned to the exact SDK release, not a floor: the monorepo ships one version
25
25
  # for everything (`make version`), and a CLI resolving to a different SDK than
26
26
  # the one it was tested against is precisely the drift that guarantees buys.
27
- dependencies = ["content-sdk==0.4.0"]
27
+ dependencies = ["content-sdk==0.6.0"]
28
28
 
29
29
  [project.scripts]
30
30
  content = "content_cli.cli:main"
File without changes
File without changes
File without changes