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 +21 -0
- nace_cli-0.1.0/PKG-INFO +124 -0
- nace_cli-0.1.0/README.md +99 -0
- nace_cli-0.1.0/pyproject.toml +85 -0
- nace_cli-0.1.0/pyproject.toml.orig +66 -0
- nace_cli-0.1.0/src/nace_cli/__init__.py +9 -0
- nace_cli-0.1.0/src/nace_cli/_cli.py +151 -0
- nace_cli-0.1.0/src/nace_cli/_decide.py +147 -0
- nace_cli-0.1.0/src/nace_cli/_docops.py +414 -0
- nace_cli-0.1.0/src/nace_cli/_entrypoint.py +24 -0
- nace_cli-0.1.0/src/nace_cli/_inputs.py +94 -0
- nace_cli-0.1.0/src/nace_cli/_jobs.py +292 -0
- nace_cli-0.1.0/src/nace_cli/_login.py +77 -0
- nace_cli-0.1.0/src/nace_cli/_output.py +169 -0
- nace_cli-0.1.0/src/nace_cli/_schemas.py +135 -0
- nace_cli-0.1.0/src/nace_cli/py.typed +0 -0
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.
|
nace_cli-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
nace_cli-0.1.0/README.md
ADDED
|
@@ -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())
|