nace-cli 0.1.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.
nace_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nace AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: nace-cli
3
+ Version: 0.1.0
4
+ Summary: Nace CLI: calibrated decisions and document jobs from the shell
5
+ Keywords: drex,nace,cli,document-intelligence
6
+ Author: Nace AI
7
+ Author-email: Nace AI <engineering@nace.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: nace-sdk>=0.1,<1
20
+ Requires-Dist: truststore>=0.10.4,<1
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://console.nace.ai
23
+ Project-URL: Documentation, https://console.nace.ai/docs/developer-tools/cli
24
+ Description-Content-Type: text/markdown
25
+
26
+ # nace-cli
27
+
28
+ `nace` — the [Drex API](https://console.nace.ai/docs) from the shell: calibrated decisions and document jobs.
29
+
30
+ ```bash
31
+ pipx install nace-cli # or: uv tool install nace-cli
32
+ nace login # paste a key from https://console.nace.ai/dashboard/api-keys
33
+ ```
34
+
35
+ `nace login` checks the key against Drex before saving it to `~/.nace/config.toml` (mode 0600). `$NACE_API_KEY` and `$NACE_BASE_URL` override the file; `--api-key -` reads the key from stdin for scripts. `nace --version` (or `nace version`) prints the CLI and SDK versions, and `NACE_LOG_LEVEL=debug` logs each request to stderr.
36
+
37
+ ## Decisions
38
+
39
+ ```bash
40
+ nace decide --state "I was charged twice for my March invoice." \
41
+ --noul "wants_refund=Is the customer asking for a refund?" \
42
+ --choice "topic=billing,shipping,other: Which topic is it?" \
43
+ --score "urgency=low,medium,high: How urgent is it?"
44
+ ```
45
+
46
+ ```
47
+ wants_refund noul 0.930
48
+ topic choice billing (confidence 0.880)
49
+ urgency score 1.400 on low < medium < high (confidence 0.500)
50
+ ```
51
+
52
+ `--state` takes text, `@file` or `-` (stdin); a JSON object or array is sent as JSON. Richer questions go in `--questions questions.json`, or a whole request in `--body`. `nace models` lists the models; `-m` picks one. `--json` prints the response body, `request_id` included.
53
+
54
+ ## Documents
55
+
56
+ ```bash
57
+ nace parse https://example.com/invoice.pdf > invoice.md
58
+ nace parse ./invoice.pdf --format markdown,text -p 1-3 --mode high
59
+ nace parse ./locked.pdf --password - # reads the PDF password from stdin, hidden at a terminal
60
+ nace extract ./invoice.pdf --schema invoice.schema.json
61
+ nace extract job:<parse job id> --schema-id sch_... --schema-version 2
62
+ nace classify ./scan.pdf --class invoice:Invoice --class receipt:Receipt --granularity page
63
+ nace split ./batch.pdf --classes classes.json
64
+ nace ground ./invoice.pdf --target "total=Total due" --target "Net 30"
65
+ ```
66
+
67
+ A `SOURCE` is one of:
68
+
69
+ - an `https://` URL. The document service picks the parser from the file name's extension, taken from the URL's last path segment; when the link doesn't end in one, name it with `--file-name invoice.pdf`;
70
+ - a local file, uploaded into your workspace first at its file name (`--upload-path` picks another path). The same bytes again reuse the file there; an edited file fails with `path_conflict`: pass another `--upload-path`, or `--on-conflict new_version` to add a version (jobs on that file not yet started then read the new version);
71
+ - `ws:<workspace_id>/<file_id>`, what `nace upload` prints;
72
+ - `job:<parse job id>` to reuse a finished parse (not for `classify`, `-p`, `--mode medium|high` or `--password`).
73
+
74
+ `-p/--pages` and `--mode` apply to parse, split, classify and extract. `--semantic` (deprecated) asks Ground to match each target by meaning; on PDF, Word and PowerPoint those targets fail with `semantic_mode_deprecated`. Any other request field goes in `--option key=value` (dotted keys nest, JSON values parse) or `--options request.json`; flags win over both, and the source always comes from `SOURCE`.
75
+
76
+ Each command waits for the job and prints Markdown for a parse, the result JSON otherwise. For a document too large to return inline, `nace parse` fetches the full Markdown instead of the preview; when it can't, it prints the preview, names the `nace file` command that fetches the rest, and exits `1`. `-o json` prints the whole job, `-o result` only the result, `-o id` the job id; `--save PATH` writes it to a file. `--async` prints the job id and returns at once; `--watch` streams progress to stderr; `--wait-seconds N` asks Drex to hold the create up to 60 s; `--wait-timeout` bounds the client-side wait (default 600 s).
77
+
78
+ ## Uploads
79
+
80
+ ```bash
81
+ nace upload ./invoice.pdf --path invoices/2026/invoice.pdf --on-conflict new_version
82
+ nace upload-grant --path inbox/scan.pdf --max-bytes 10000000 # a one-use upload token for another client
83
+ nace upload-session --path big.csv --total-size-bytes 734003200
84
+ ```
85
+
86
+ `upload-grant` and `upload-session` print JSON for another client to upload with: the CLI doesn't send parts or complete a session itself, and `nace upload` (or a local-file `SOURCE`) does every step for you.
87
+
88
+ ## Jobs and their files
89
+
90
+ ```bash
91
+ nace jobs --operation parse --status succeeded --limit 50 --cursor <next_cursor>
92
+ nace job <id> --wait # --watch implies --wait
93
+ nace events <id> # progress, one JSON object per line
94
+ nace job-request <id> # the request the job ran under
95
+ nace file <id> <document.content_url> # full Markdown, saved as <id>.md
96
+ nace file <id> parse-images/<ref> --save figure-1.png
97
+ nace rows <id> <rows_url> --all > sheet.tsv
98
+ nace cancel <id>
99
+ ```
100
+
101
+ `nace file` saves any file a job produced, waiting for Markdown still being prepared. Without `--save` it goes in the current directory as `<id>.md` for a document's Markdown, else `<id>-<kind>-<digest>` with the file's extension. `--link` prints a stored file's five-minute signed link instead.
102
+
103
+ ## Saved extraction schemas
104
+
105
+ ```bash
106
+ nace schema-create Invoice --schema invoice.schema.json
107
+ nace schema-version-add sch_... --schema invoice-v2.schema.json
108
+ nace schemas --all
109
+ nace schema-versions sch_... # oldest first
110
+ nace schema sch_... > invoice.schema.json # the JSON Schema itself; --json for the whole record
111
+ nace schema-version sch_... 1
112
+ ```
113
+
114
+ ## Output and exit codes
115
+
116
+ Stdout carries the payload and stderr the status, so `nace parse doc.pdf > doc.md` captures only the document. Exit `0` on success, `1` on an API or job failure (the message names the error type, code, request id and up to five fields that failed validation), `2` on a usage or configuration error.
117
+
118
+ Requests are retried up to twice on a `408`, `429`, `5xx`, dropped connection or timeout, but only while the retry can start within 30 s of the first attempt, so a request that fails after 30 s, such as one that hit `--timeout` (120 s) or a slow `529`, is not retried. `schema-create` and `schema-version-add` are never retried.
119
+
120
+ Full guide: https://console.nace.ai/docs/developer-tools/cli (install, sign-in, `decide`, documents, uploads, jobs, schemas, exit codes).
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,99 @@
1
+ # nace-cli
2
+
3
+ `nace` — the [Drex API](https://console.nace.ai/docs) from the shell: calibrated decisions and document jobs.
4
+
5
+ ```bash
6
+ pipx install nace-cli # or: uv tool install nace-cli
7
+ nace login # paste a key from https://console.nace.ai/dashboard/api-keys
8
+ ```
9
+
10
+ `nace login` checks the key against Drex before saving it to `~/.nace/config.toml` (mode 0600). `$NACE_API_KEY` and `$NACE_BASE_URL` override the file; `--api-key -` reads the key from stdin for scripts. `nace --version` (or `nace version`) prints the CLI and SDK versions, and `NACE_LOG_LEVEL=debug` logs each request to stderr.
11
+
12
+ ## Decisions
13
+
14
+ ```bash
15
+ nace decide --state "I was charged twice for my March invoice." \
16
+ --noul "wants_refund=Is the customer asking for a refund?" \
17
+ --choice "topic=billing,shipping,other: Which topic is it?" \
18
+ --score "urgency=low,medium,high: How urgent is it?"
19
+ ```
20
+
21
+ ```
22
+ wants_refund noul 0.930
23
+ topic choice billing (confidence 0.880)
24
+ urgency score 1.400 on low < medium < high (confidence 0.500)
25
+ ```
26
+
27
+ `--state` takes text, `@file` or `-` (stdin); a JSON object or array is sent as JSON. Richer questions go in `--questions questions.json`, or a whole request in `--body`. `nace models` lists the models; `-m` picks one. `--json` prints the response body, `request_id` included.
28
+
29
+ ## Documents
30
+
31
+ ```bash
32
+ nace parse https://example.com/invoice.pdf > invoice.md
33
+ nace parse ./invoice.pdf --format markdown,text -p 1-3 --mode high
34
+ nace parse ./locked.pdf --password - # reads the PDF password from stdin, hidden at a terminal
35
+ nace extract ./invoice.pdf --schema invoice.schema.json
36
+ nace extract job:<parse job id> --schema-id sch_... --schema-version 2
37
+ nace classify ./scan.pdf --class invoice:Invoice --class receipt:Receipt --granularity page
38
+ nace split ./batch.pdf --classes classes.json
39
+ nace ground ./invoice.pdf --target "total=Total due" --target "Net 30"
40
+ ```
41
+
42
+ A `SOURCE` is one of:
43
+
44
+ - an `https://` URL. The document service picks the parser from the file name's extension, taken from the URL's last path segment; when the link doesn't end in one, name it with `--file-name invoice.pdf`;
45
+ - a local file, uploaded into your workspace first at its file name (`--upload-path` picks another path). The same bytes again reuse the file there; an edited file fails with `path_conflict`: pass another `--upload-path`, or `--on-conflict new_version` to add a version (jobs on that file not yet started then read the new version);
46
+ - `ws:<workspace_id>/<file_id>`, what `nace upload` prints;
47
+ - `job:<parse job id>` to reuse a finished parse (not for `classify`, `-p`, `--mode medium|high` or `--password`).
48
+
49
+ `-p/--pages` and `--mode` apply to parse, split, classify and extract. `--semantic` (deprecated) asks Ground to match each target by meaning; on PDF, Word and PowerPoint those targets fail with `semantic_mode_deprecated`. Any other request field goes in `--option key=value` (dotted keys nest, JSON values parse) or `--options request.json`; flags win over both, and the source always comes from `SOURCE`.
50
+
51
+ Each command waits for the job and prints Markdown for a parse, the result JSON otherwise. For a document too large to return inline, `nace parse` fetches the full Markdown instead of the preview; when it can't, it prints the preview, names the `nace file` command that fetches the rest, and exits `1`. `-o json` prints the whole job, `-o result` only the result, `-o id` the job id; `--save PATH` writes it to a file. `--async` prints the job id and returns at once; `--watch` streams progress to stderr; `--wait-seconds N` asks Drex to hold the create up to 60 s; `--wait-timeout` bounds the client-side wait (default 600 s).
52
+
53
+ ## Uploads
54
+
55
+ ```bash
56
+ nace upload ./invoice.pdf --path invoices/2026/invoice.pdf --on-conflict new_version
57
+ nace upload-grant --path inbox/scan.pdf --max-bytes 10000000 # a one-use upload token for another client
58
+ nace upload-session --path big.csv --total-size-bytes 734003200
59
+ ```
60
+
61
+ `upload-grant` and `upload-session` print JSON for another client to upload with: the CLI doesn't send parts or complete a session itself, and `nace upload` (or a local-file `SOURCE`) does every step for you.
62
+
63
+ ## Jobs and their files
64
+
65
+ ```bash
66
+ nace jobs --operation parse --status succeeded --limit 50 --cursor <next_cursor>
67
+ nace job <id> --wait # --watch implies --wait
68
+ nace events <id> # progress, one JSON object per line
69
+ nace job-request <id> # the request the job ran under
70
+ nace file <id> <document.content_url> # full Markdown, saved as <id>.md
71
+ nace file <id> parse-images/<ref> --save figure-1.png
72
+ nace rows <id> <rows_url> --all > sheet.tsv
73
+ nace cancel <id>
74
+ ```
75
+
76
+ `nace file` saves any file a job produced, waiting for Markdown still being prepared. Without `--save` it goes in the current directory as `<id>.md` for a document's Markdown, else `<id>-<kind>-<digest>` with the file's extension. `--link` prints a stored file's five-minute signed link instead.
77
+
78
+ ## Saved extraction schemas
79
+
80
+ ```bash
81
+ nace schema-create Invoice --schema invoice.schema.json
82
+ nace schema-version-add sch_... --schema invoice-v2.schema.json
83
+ nace schemas --all
84
+ nace schema-versions sch_... # oldest first
85
+ nace schema sch_... > invoice.schema.json # the JSON Schema itself; --json for the whole record
86
+ nace schema-version sch_... 1
87
+ ```
88
+
89
+ ## Output and exit codes
90
+
91
+ Stdout carries the payload and stderr the status, so `nace parse doc.pdf > doc.md` captures only the document. Exit `0` on success, `1` on an API or job failure (the message names the error type, code, request id and up to five fields that failed validation), `2` on a usage or configuration error.
92
+
93
+ Requests are retried up to twice on a `408`, `429`, `5xx`, dropped connection or timeout, but only while the retry can start within 30 s of the first attempt, so a request that fails after 30 s, such as one that hit `--timeout` (120 s) or a slow `529`, is not retried. `schema-create` and `schema-version-add` are never retried.
94
+
95
+ Full guide: https://console.nace.ai/docs/developer-tools/cli (install, sign-in, `decide`, documents, uploads, jobs, schemas, exit codes).
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,85 @@
1
+ [project]
2
+ name = "nace-cli"
3
+ version = "0.1.0"
4
+ description = "Nace CLI: calibrated decisions and document jobs from the shell"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "drex",
11
+ "nace",
12
+ "cli",
13
+ "document-intelligence",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "nace-sdk>=0.1,<1",
28
+ "truststore>=0.10.4,<1",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Nace AI"
33
+ email = "engineering@nace.ai"
34
+
35
+ [project.urls]
36
+ Homepage = "https://console.nace.ai"
37
+ Documentation = "https://console.nace.ai/docs/developer-tools/cli"
38
+
39
+ [project.scripts]
40
+ nace = "nace_cli._entrypoint:main"
41
+
42
+ [tool.uv.sources.nace-sdk]
43
+ workspace = true
44
+
45
+ [tool.ruff]
46
+ target-version = "py311"
47
+ line-length = 150
48
+ fix = true
49
+
50
+ [tool.ruff.lint]
51
+ select = [
52
+ "E",
53
+ "F",
54
+ "W",
55
+ "I",
56
+ "B",
57
+ "UP",
58
+ "RUF",
59
+ "ASYNC",
60
+ "SIM",
61
+ "BLE",
62
+ "PLC0415",
63
+ ]
64
+ ignore = [
65
+ "D",
66
+ "ANN",
67
+ "PLR0913",
68
+ "ASYNC109",
69
+ ]
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ pythonpath = [
74
+ "src",
75
+ "tests",
76
+ ]
77
+ asyncio_mode = "auto"
78
+ addopts = [
79
+ "--strict-markers",
80
+ "--tb=short",
81
+ ]
82
+
83
+ [build-system]
84
+ requires = ["uv_build>=0.9.0,<0.12.0"]
85
+ build-backend = "uv_build"
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "nace-cli"
3
+ version = "0.1.0"
4
+ description = "Nace CLI: calibrated decisions and document jobs from the shell"
5
+ authors = [
6
+ { name = "Nace AI", email = "engineering@nace.ai" }
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.11"
12
+ keywords = [
13
+ "drex",
14
+ "nace",
15
+ "cli",
16
+ "document-intelligence",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "nace-sdk>=0.1,<1",
31
+ "truststore>=0.10.4,<1",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://console.nace.ai"
36
+ Documentation = "https://console.nace.ai/docs/developer-tools/cli"
37
+
38
+ [project.scripts]
39
+ nace = "nace_cli._entrypoint:main"
40
+
41
+ [tool.uv.sources]
42
+ nace-sdk = { workspace = true }
43
+
44
+ [build-system]
45
+ requires = ["uv_build>=0.9.0,<0.12.0"]
46
+ build-backend = "uv_build"
47
+
48
+ [tool.ruff]
49
+ target-version = "py311"
50
+ line-length = 150
51
+ fix = true
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "W", "I", "B", "UP", "RUF", "ASYNC", "SIM", "BLE", "PLC0415"]
55
+ ignore = [
56
+ "D",
57
+ "ANN",
58
+ "PLR0913",
59
+ "ASYNC109", # `timeout` on jobs.wait is a polling budget, not a cancel scope
60
+ ]
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ pythonpath = ["src", "tests"]
65
+ asyncio_mode = "auto"
66
+ addopts = ["--strict-markers", "--tb=short"]
@@ -0,0 +1,9 @@
1
+ """``nace`` — the Nace CLI.
2
+
3
+ Decisions (``POST /v1/systemone``), document jobs and uploads
4
+ (``/v1/documents/*``) and saved extraction schemas (``/v1/extraction-schemas``)
5
+ over ``nace-sdk``. Credentials come from
6
+ ``$NACE_API_KEY`` / ``$NACE_BASE_URL`` or ``~/.nace/config.toml``.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,151 @@
1
+ """``nace`` entry point: parser, client construction, and error reporting.
2
+
3
+ Exit codes: ``0`` success, ``1`` an API or job failure, ``2`` a usage or
4
+ configuration error. Stdout carries the payload; stderr carries status, so
5
+ ``nace parse doc.pdf > doc.md`` captures only the document.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import logging
12
+ import sys
13
+ from typing import Any
14
+
15
+ from nace_sdk import NaceAPIConnectionError, NaceAPIError, NaceClient, NaceError, NaceJobFailedError, NaceJobTimeoutError
16
+ from nace_sdk.constants import LOG_LEVEL_ENV, read_env
17
+ from nace_sdk.credentials import CredentialsError, resolve_credentials
18
+
19
+ from nace_cli import _decide, _docops, _jobs, _login, _schemas
20
+ from nace_cli._output import CommandFailed
21
+
22
+ #: Long enough for a decision (Drex holds one up to 60 s) and for a document
23
+ #: create that waits server-side; job waits have their own budget (--wait-timeout).
24
+ DEFAULT_TIMEOUT_SECONDS = 120.0
25
+ _ISSUE_LIMIT = 5
26
+
27
+
28
+ class _StderrHandler(logging.Handler):
29
+ """Writes to whatever ``sys.stderr`` is at the time, so redirection and capture see SDK logs."""
30
+
31
+ def emit(self, record: logging.LogRecord) -> None:
32
+ print(self.format(record), file=sys.stderr)
33
+
34
+
35
+ _SDK_LOG = _StderrHandler()
36
+ _SDK_LOG.setFormatter(logging.Formatter("nace_sdk %(levelname)s %(message)s"))
37
+
38
+
39
+ def main(argv: list[str] | None = None) -> int:
40
+ parser = build_parser()
41
+ args = parser.parse_args(argv)
42
+ if args.show_version:
43
+ return _login.run_version(args)
44
+ if args.command is None:
45
+ parser.print_help()
46
+ return 2
47
+ _log_to_stderr()
48
+ try:
49
+ if args.family == "standalone":
50
+ return args.run(args)
51
+ credentials = resolve_credentials()
52
+ with NaceClient(api_key=credentials.api_key, base_url=credentials.base_url, timeout=args.timeout) as client:
53
+ return args.run(client, args)
54
+ except CredentialsError as exc:
55
+ print(f"error: {exc}", file=sys.stderr)
56
+ return 2
57
+ except NaceAPIError as exc:
58
+ report_status_error(exc)
59
+ return 1
60
+ except (NaceAPIConnectionError, NaceJobFailedError, NaceJobTimeoutError, CommandFailed) as exc:
61
+ print(f"error: {exc}", file=sys.stderr)
62
+ return 1
63
+ except NaceError as exc:
64
+ # Raised before any request: a bad argument the SDK refused locally.
65
+ print(f"error: {exc}", file=sys.stderr)
66
+ return 2
67
+ except (ValueError, OSError) as exc:
68
+ print(f"error: {exc}", file=sys.stderr)
69
+ return 2
70
+
71
+
72
+ def _log_to_stderr() -> None:
73
+ """``$NACE_LOG_LEVEL`` sets the SDK logger's level when it loads; the CLI gives those records a place to go."""
74
+ logger = logging.getLogger("nace_sdk")
75
+ if read_env(LOG_LEVEL_ENV) and _SDK_LOG not in logger.handlers:
76
+ logger.addHandler(_SDK_LOG)
77
+
78
+
79
+ def _validation_lines(exc: NaceAPIError) -> list[dict[str, Any]]:
80
+ """Where a request failed validation: ``error.issues``, else the document routes' ``error.detail.errors``."""
81
+ if exc.issues:
82
+ return exc.issues
83
+ errors = exc.detail.get("errors") if isinstance(exc.detail, dict) else None
84
+ if not isinstance(errors, list):
85
+ return []
86
+ lines = []
87
+ for entry in errors:
88
+ if not isinstance(entry, dict):
89
+ continue
90
+ location = entry.get("loc")
91
+ path = entry.get("path") or (".".join(str(part) for part in location if part != "body") if isinstance(location, list) else "")
92
+ lines.append({"path": path, "message": entry.get("message") or entry.get("msg") or ""})
93
+ return lines
94
+
95
+
96
+ def report_status_error(exc: NaceAPIError) -> None:
97
+ """One line a person can act on, plus where a request failed validation."""
98
+ label = "/".join(str(part) for part in (exc.type, exc.code) if part)
99
+ tag = f" [{label}]" if label else ""
100
+ request = f" ({exc.request_id})" if exc.request_id else ""
101
+ print(f"error: {exc.status}{tag} {exc.message}{request}", file=sys.stderr)
102
+ issues = _validation_lines(exc)
103
+ for issue in issues[:_ISSUE_LIMIT]:
104
+ where = f"{issue.get('path')}: " if issue.get("path") else ""
105
+ print(f" - {where}{issue.get('message', '')}", file=sys.stderr)
106
+ if len(issues) > _ISSUE_LIMIT:
107
+ print(f" (+{len(issues) - _ISSUE_LIMIT} more)", file=sys.stderr)
108
+ if exc.status == 402:
109
+ billing = _login.dashboard_url("/dashboard/billing")
110
+ # payment_required is an unpaid invoice: a top-up doesn't lift it, paying the invoice does.
111
+ if exc.type == "payment_required":
112
+ print(f" Pay the unpaid invoice at {billing}", file=sys.stderr)
113
+ else:
114
+ print(f" Top up or add a card at {billing}", file=sys.stderr)
115
+ if exc.code == "path_conflict":
116
+ print(
117
+ " Upload to another path (--upload-path, or --path on `nace upload`), or pass --on-conflict new_version to add a version of that file.",
118
+ file=sys.stderr,
119
+ )
120
+
121
+
122
+ def build_parser() -> argparse.ArgumentParser:
123
+ parser = argparse.ArgumentParser(
124
+ prog="nace",
125
+ description=(
126
+ "Nace CLI: calibrated decisions and document jobs. Credentials come from "
127
+ "$NACE_API_KEY / $NACE_BASE_URL or ~/.nace/config.toml (`nace login`). "
128
+ "Set $NACE_LOG_LEVEL=debug to log each request to stderr."
129
+ ),
130
+ )
131
+ parser.add_argument("--version", dest="show_version", action="store_true", help="Print CLI and SDK versions (same as `nace version`).")
132
+ common = argparse.ArgumentParser(add_help=False)
133
+ common.add_argument("--json", action="store_true", help="Print the raw response JSON.")
134
+ common.add_argument(
135
+ "--timeout",
136
+ type=float,
137
+ default=DEFAULT_TIMEOUT_SECONDS,
138
+ metavar="SECONDS",
139
+ help=f"Per-request HTTP timeout (default {DEFAULT_TIMEOUT_SECONDS:.0f}s).",
140
+ )
141
+ sub = parser.add_subparsers(dest="command")
142
+ _login.add_parsers(sub)
143
+ _decide.add_parsers(sub, common)
144
+ _docops.add_parsers(sub, common)
145
+ _jobs.add_parsers(sub, common)
146
+ _schemas.add_parsers(sub, common)
147
+ return parser
148
+
149
+
150
+ if __name__ == "__main__":
151
+ raise SystemExit(main())