ndi-cli 0.4.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.
ndi_cli/_login.py ADDED
@@ -0,0 +1,47 @@
1
+ """``ndi login`` and ``ndi version``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from ndi_sdk.credentials import CredentialsError, login
9
+
10
+
11
+ def add_parsers(sub: argparse._SubParsersAction) -> None:
12
+ login_p = sub.add_parser("login", help="Authorize in the browser and save the API key.")
13
+ login_p.add_argument(
14
+ "--api-key",
15
+ nargs="?",
16
+ const="",
17
+ default=None,
18
+ metavar="KEY",
19
+ help="Paste a key instead of opening a browser. KEY=- reads stdin. Omit KEY to type it hidden.",
20
+ )
21
+ login_p.set_defaults(run=run_login, family="standalone", needs_workspace=False, needs_client=False)
22
+
23
+ version = sub.add_parser("version", help="Print CLI and SDK versions.")
24
+ version.set_defaults(run=run_version, family="standalone", needs_workspace=False, needs_client=False)
25
+
26
+
27
+ def run_login(args: argparse.Namespace) -> int:
28
+ prompt = args.api_key == ""
29
+ key = None if prompt else args.api_key
30
+ if key == "-":
31
+ key = sys.stdin.readline().strip()
32
+ try:
33
+ path = login(api_key=key, prompt=prompt, login_command="ndi login")
34
+ except CredentialsError as exc:
35
+ print(f"error: {exc}", file=sys.stderr)
36
+ return 2
37
+ print(f"saved API key to {path}", file=sys.stderr)
38
+ return 0
39
+
40
+
41
+ def run_version(_args: argparse.Namespace) -> int:
42
+ from ndi_cli import __version__
43
+ from ndi_sdk import __version__ as sdk_version
44
+
45
+ print(f"ndi {__version__}")
46
+ print(f"ndi-sdk {sdk_version}")
47
+ return 0
ndi_cli/_output.py ADDED
@@ -0,0 +1,128 @@
1
+ """Stdout is the payload; stderr is status. ``-o`` picks the shape."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ from ndi_sdk.models.jobs import IngestionResult, Job, ParseResult
10
+
11
+ OUTPUT_CHOICES = ("auto", "md", "json", "payload", "id")
12
+
13
+
14
+ def add_job_output_args(parser) -> None:
15
+ parser.add_argument(
16
+ "-o",
17
+ "--output",
18
+ choices=OUTPUT_CHOICES,
19
+ default="auto",
20
+ help="Result shape (default auto). --json is an alias for json.",
21
+ )
22
+ parser.add_argument("--save", default=None, metavar="PATH", help="Write one result to this file.")
23
+ parser.add_argument("--out-dir", default=None, metavar="DIR", help="Write one file per input.")
24
+ parser.add_argument("--async", dest="async_submit", action="store_true", help="Submit and print the job id.")
25
+
26
+
27
+ def output_mode(args) -> str:
28
+ if getattr(args, "json", False):
29
+ return "json"
30
+ return getattr(args, "output", None) or "auto"
31
+
32
+
33
+ def emit_text(text: str, args, *, default_stem: str, suffix: str) -> int:
34
+ save = getattr(args, "save", None)
35
+ out_dir = getattr(args, "out_dir", None)
36
+ if save and out_dir:
37
+ print("error: use --save or --out-dir, not both", file=sys.stderr)
38
+ return 2
39
+ if save:
40
+ return _write(Path(save), text)
41
+ if out_dir:
42
+ directory = Path(out_dir)
43
+ try:
44
+ directory.mkdir(parents=True, exist_ok=True)
45
+ except OSError as exc:
46
+ return _write_failed(directory, exc, text)
47
+ return _write(directory / f"{default_stem}{suffix}", text)
48
+ print(text)
49
+ return 0
50
+
51
+
52
+ def _write(target: Path, text: str) -> int:
53
+ # encoding is explicit: the default follows the locale, so a C/POSIX
54
+ # environment would fail on any non-ASCII document the job just produced.
55
+ try:
56
+ target.write_text(text, encoding="utf-8")
57
+ except OSError as exc:
58
+ return _write_failed(target, exc, text)
59
+ print(f"saved {target}", file=sys.stderr)
60
+ return 0
61
+
62
+
63
+ def _write_failed(target: Path, exc: OSError, text: str) -> int:
64
+ """Report the write failure without throwing away the result.
65
+
66
+ By the time a result is written the job has already run and been billed, so
67
+ the content goes to stdout rather than being lost with a traceback.
68
+ """
69
+ print(f"error: could not write {target}: {exc.strerror or exc}", file=sys.stderr)
70
+ print(text)
71
+ return 1
72
+
73
+
74
+ def render_job(job: Job, args, *, auto: Callable[[Job], str], stem: str) -> int:
75
+ mode = output_mode(args)
76
+ if mode == "id":
77
+ print(job.job_id)
78
+ return _content_failure(job)
79
+ if mode == "json":
80
+ code = emit_text(job.model_dump_json(indent=2, exclude_none=True), args, default_stem=stem, suffix=".json")
81
+ elif mode == "payload":
82
+ if job.result is None:
83
+ print("error: job has no result payload", file=sys.stderr)
84
+ return 1
85
+ code = emit_text(job.result.model_dump_json(indent=2, exclude_none=True), args, default_stem=stem, suffix=".json")
86
+ elif mode == "md":
87
+ markdown = _parse_markdown(job)
88
+ if markdown is None:
89
+ print("error: this job has no markdown; use -o auto|json|payload|id", file=sys.stderr)
90
+ return 1
91
+ code = emit_text(markdown, args, default_stem=stem, suffix=".md")
92
+ else:
93
+ code = emit_text(auto(job), args, default_stem=stem, suffix=_auto_suffix(job, auto))
94
+ return code or _content_failure(job)
95
+
96
+
97
+ def _content_failure(job: Job) -> int:
98
+ """Non-zero when the job succeeded but its payload reports failed work.
99
+
100
+ An ingestion job whose every file failed is still a ``succeeded`` job, so
101
+ without this ``ndi ingest … && next-step`` runs the next step on nothing.
102
+ """
103
+ result = job.result
104
+ if isinstance(result, IngestionResult) and result.files_failed:
105
+ print(
106
+ f"error: {result.files_failed} of {result.files_failed + result.files_ingested + result.files_skipped} "
107
+ "files failed to ingest",
108
+ file=sys.stderr,
109
+ )
110
+ return 1
111
+ return 0
112
+
113
+
114
+ def _parse_markdown(job: Job) -> str | None:
115
+ result = job.result
116
+ if isinstance(result, ParseResult):
117
+ return result.document.markdown
118
+ return None
119
+
120
+
121
+ def _auto_suffix(job: Job, auto: Callable[[Job], str]) -> str:
122
+ if isinstance(job.result, ParseResult):
123
+ document = job.result.document
124
+ if document.markdown is not None:
125
+ return ".md"
126
+ if document.text is None and document.blocks is not None:
127
+ return ".json"
128
+ return ".txt"