specstream-client 1.0.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.
- specstream_client-1.0.0/.gitignore +14 -0
- specstream_client-1.0.0/LICENSE +21 -0
- specstream_client-1.0.0/PKG-INFO +120 -0
- specstream_client-1.0.0/README.md +93 -0
- specstream_client-1.0.0/pyproject.toml +54 -0
- specstream_client-1.0.0/specstream_client/__init__.py +14 -0
- specstream_client-1.0.0/specstream_client/__main__.py +3 -0
- specstream_client-1.0.0/specstream_client/authorization.py +32 -0
- specstream_client-1.0.0/specstream_client/cli.py +139 -0
- specstream_client-1.0.0/specstream_client/client.py +1451 -0
- specstream_client-1.0.0/specstream_client/families.py +36 -0
- specstream_client-1.0.0/specstream_client/lifecycle_commands.py +969 -0
- specstream_client-1.0.0/specstream_client/py.typed +0 -0
- specstream_client-1.0.0/tests/__init__.py +0 -0
- specstream_client-1.0.0/tests/test_cli.py +38 -0
- specstream_client-1.0.0/tests/test_client.py +91 -0
- specstream_client-1.0.0/tests/test_convergence.py +45 -0
- specstream_client-1.0.0/tests/test_http_boundary.py +86 -0
- specstream_client-1.0.0/tests/test_m16_exports.py +175 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SpecStream SAS
|
|
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,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: specstream-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client and command line for SpecStream: ask for any list in plain words and get every match with its source, priced before anything runs.
|
|
5
|
+
Project-URL: Homepage, https://specstream.ai
|
|
6
|
+
Project-URL: Documentation, https://specstream.ai/docs/sdk
|
|
7
|
+
Project-URL: API, https://api.specstream.ai
|
|
8
|
+
Project-URL: Changelog, https://specstream.ai/changelog
|
|
9
|
+
Project-URL: Repository, https://github.com/Specstreamai/specstream-tools
|
|
10
|
+
Project-URL: Issues, https://github.com/Specstreamai/specstream-tools/issues
|
|
11
|
+
Author-email: SpecStream <support@specstream.ai>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: ai-agents,api-client,enrichment,lead-lists,live-web,mcp,monitoring,specstream,web-data
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# specstream-client
|
|
29
|
+
|
|
30
|
+
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine for teams and agents. Describe the list you need in plain words; SpecStream reads the web where those things live, hands back only the matches, each with the page it came from, and you pay only for the matches.
|
|
31
|
+
|
|
32
|
+
The client has no dependencies and needs Python 3.12 or newer. It uses the same account, permissions, projects and balance as the workspace.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pip install specstream-client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The package installs the import name `specstream` and the command `specstream`.
|
|
39
|
+
|
|
40
|
+
Create an API key under **Agents & API** in your SpecStream workspace and keep it in the environment:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
export SPECSTREAM_API_KEY="ss_live_..."
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Read a request, get the price
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import os
|
|
50
|
+
from specstream_client import Client
|
|
51
|
+
|
|
52
|
+
client = Client(os.environ["SPECSTREAM_API_KEY"])
|
|
53
|
+
|
|
54
|
+
read = client.compile("Open-source document-signing tools I can self-host. Include the license and latest release.")
|
|
55
|
+
offer = client.quote_capacity_work({
|
|
56
|
+
"ast_hash": read["ast_hash"],
|
|
57
|
+
"operation": "find",
|
|
58
|
+
"requested_units": read["ast"]["limit"]["max"],
|
|
59
|
+
})
|
|
60
|
+
print(offer["maximum_cents"], offer["expires_at"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Nothing has run yet. Review the quote: what will be found, the facts that come back, the most the work can cost, and when the price expires. A price lasts 30 minutes.
|
|
64
|
+
|
|
65
|
+
## Approve it, then read the results
|
|
66
|
+
|
|
67
|
+
After a person approves, send the approved maximum in cents and a request key you saved first:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
run = client.authorize_capacity_work({
|
|
71
|
+
"quote_id": offer["quote_id"],
|
|
72
|
+
"quote_hash": offer["quote_hash"],
|
|
73
|
+
"confirm_max_cents": approved_maximum_cents,
|
|
74
|
+
"idempotency_key": saved_request_key,
|
|
75
|
+
})
|
|
76
|
+
find_id = run["find_id"]
|
|
77
|
+
|
|
78
|
+
page = client.rows(find_id)
|
|
79
|
+
for row in page["rows"]:
|
|
80
|
+
print(row)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If the response is lost, `get_capacity_request(saved_request_key)` returns the same work. Keep the original key when you retry, and you will not pay twice. `get_find` reports progress, and `rows` pages through results with `next_cursor`. Reading, exporting and sharing results you own cost nothing.
|
|
84
|
+
|
|
85
|
+
`authorize_capacity_work` refuses to send anything unless the quote, its signature, the maximum and the key are all present.
|
|
86
|
+
|
|
87
|
+
## More than a first request
|
|
88
|
+
|
|
89
|
+
- `quote_capacity_work` also prices `extend` (more results), `add_facts` and `refresh` (a recheck, at half price) on a finished run. Pass the original `parent_find_id`.
|
|
90
|
+
- `spec_options(kind)` lists the facts you can ask for, per kind of thing. `edit_spec` changes a request without rereading it.
|
|
91
|
+
- `create_capacity_schedule` keeps a request current, hourly to yearly, inside per-run and per-period limits.
|
|
92
|
+
- `export` and `download_export` give CSV, XLSX, JSON or HTML. `create_customer_webhook` sends signed events.
|
|
93
|
+
- Projects, members, shares and starred items have their own methods.
|
|
94
|
+
|
|
95
|
+
Kinds of thing include local businesses, companies, people, creators, products, apps, job postings, papers, clinical trials, grants, tenders, regulations, vulnerabilities, software packages, repositories, datasets, books and podcasts.
|
|
96
|
+
|
|
97
|
+
## Command line
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
specstream compile "Food banks in Texas, with their annual revenue."
|
|
101
|
+
specstream get-capacity-balance
|
|
102
|
+
specstream quote-capacity-work --body quote-request.json
|
|
103
|
+
specstream authorize-capacity-work --body approved-execution.json
|
|
104
|
+
specstream rows --find-id FIND_ID
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`--body` takes a JSON file or `-` for stdin. Every command's `--help` describes its parameters. The command line exposes the same operations as the client.
|
|
108
|
+
|
|
109
|
+
## Pricing
|
|
110
|
+
|
|
111
|
+
New accounts start with $10 of work for 30 days, no card. A delivered result costs 10 cents, 30 cents or $1 depending on how many facts it carries. No result, no charge. Rechecks cost half. People and agents share one balance. See [pricing](https://specstream.ai/pricing).
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- [SDK docs](https://specstream.ai/docs/sdk) and [API docs](https://specstream.ai/docs/api)
|
|
116
|
+
- [Source and issues](https://github.com/Specstreamai/specstream-tools)
|
|
117
|
+
- [TypeScript client](https://www.npmjs.com/package/@specstream/sdk)
|
|
118
|
+
- Support: support@specstream.ai
|
|
119
|
+
|
|
120
|
+
MIT license.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# specstream-client
|
|
2
|
+
|
|
3
|
+
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine for teams and agents. Describe the list you need in plain words; SpecStream reads the web where those things live, hands back only the matches, each with the page it came from, and you pay only for the matches.
|
|
4
|
+
|
|
5
|
+
The client has no dependencies and needs Python 3.12 or newer. It uses the same account, permissions, projects and balance as the workspace.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install specstream-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package installs the import name `specstream` and the command `specstream`.
|
|
12
|
+
|
|
13
|
+
Create an API key under **Agents & API** in your SpecStream workspace and keep it in the environment:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
export SPECSTREAM_API_KEY="ss_live_..."
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Read a request, get the price
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import os
|
|
23
|
+
from specstream_client import Client
|
|
24
|
+
|
|
25
|
+
client = Client(os.environ["SPECSTREAM_API_KEY"])
|
|
26
|
+
|
|
27
|
+
read = client.compile("Open-source document-signing tools I can self-host. Include the license and latest release.")
|
|
28
|
+
offer = client.quote_capacity_work({
|
|
29
|
+
"ast_hash": read["ast_hash"],
|
|
30
|
+
"operation": "find",
|
|
31
|
+
"requested_units": read["ast"]["limit"]["max"],
|
|
32
|
+
})
|
|
33
|
+
print(offer["maximum_cents"], offer["expires_at"])
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Nothing has run yet. Review the quote: what will be found, the facts that come back, the most the work can cost, and when the price expires. A price lasts 30 minutes.
|
|
37
|
+
|
|
38
|
+
## Approve it, then read the results
|
|
39
|
+
|
|
40
|
+
After a person approves, send the approved maximum in cents and a request key you saved first:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
run = client.authorize_capacity_work({
|
|
44
|
+
"quote_id": offer["quote_id"],
|
|
45
|
+
"quote_hash": offer["quote_hash"],
|
|
46
|
+
"confirm_max_cents": approved_maximum_cents,
|
|
47
|
+
"idempotency_key": saved_request_key,
|
|
48
|
+
})
|
|
49
|
+
find_id = run["find_id"]
|
|
50
|
+
|
|
51
|
+
page = client.rows(find_id)
|
|
52
|
+
for row in page["rows"]:
|
|
53
|
+
print(row)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the response is lost, `get_capacity_request(saved_request_key)` returns the same work. Keep the original key when you retry, and you will not pay twice. `get_find` reports progress, and `rows` pages through results with `next_cursor`. Reading, exporting and sharing results you own cost nothing.
|
|
57
|
+
|
|
58
|
+
`authorize_capacity_work` refuses to send anything unless the quote, its signature, the maximum and the key are all present.
|
|
59
|
+
|
|
60
|
+
## More than a first request
|
|
61
|
+
|
|
62
|
+
- `quote_capacity_work` also prices `extend` (more results), `add_facts` and `refresh` (a recheck, at half price) on a finished run. Pass the original `parent_find_id`.
|
|
63
|
+
- `spec_options(kind)` lists the facts you can ask for, per kind of thing. `edit_spec` changes a request without rereading it.
|
|
64
|
+
- `create_capacity_schedule` keeps a request current, hourly to yearly, inside per-run and per-period limits.
|
|
65
|
+
- `export` and `download_export` give CSV, XLSX, JSON or HTML. `create_customer_webhook` sends signed events.
|
|
66
|
+
- Projects, members, shares and starred items have their own methods.
|
|
67
|
+
|
|
68
|
+
Kinds of thing include local businesses, companies, people, creators, products, apps, job postings, papers, clinical trials, grants, tenders, regulations, vulnerabilities, software packages, repositories, datasets, books and podcasts.
|
|
69
|
+
|
|
70
|
+
## Command line
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
specstream compile "Food banks in Texas, with their annual revenue."
|
|
74
|
+
specstream get-capacity-balance
|
|
75
|
+
specstream quote-capacity-work --body quote-request.json
|
|
76
|
+
specstream authorize-capacity-work --body approved-execution.json
|
|
77
|
+
specstream rows --find-id FIND_ID
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`--body` takes a JSON file or `-` for stdin. Every command's `--help` describes its parameters. The command line exposes the same operations as the client.
|
|
81
|
+
|
|
82
|
+
## Pricing
|
|
83
|
+
|
|
84
|
+
New accounts start with $10 of work for 30 days, no card. A delivered result costs 10 cents, 30 cents or $1 depending on how many facts it carries. No result, no charge. Rechecks cost half. People and agents share one balance. See [pricing](https://specstream.ai/pricing).
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- [SDK docs](https://specstream.ai/docs/sdk) and [API docs](https://specstream.ai/docs/api)
|
|
89
|
+
- [Source and issues](https://github.com/Specstreamai/specstream-tools)
|
|
90
|
+
- [TypeScript client](https://www.npmjs.com/package/@specstream/sdk)
|
|
91
|
+
- Support: support@specstream.ai
|
|
92
|
+
|
|
93
|
+
MIT license.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "specstream-client"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python client and command line for SpecStream: ask for any list in plain words and get every match with its source, priced before anything runs."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "SpecStream", email = "support@specstream.ai" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"specstream",
|
|
12
|
+
"web-data",
|
|
13
|
+
"live-web",
|
|
14
|
+
"lead-lists",
|
|
15
|
+
"enrichment",
|
|
16
|
+
"monitoring",
|
|
17
|
+
"mcp",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"api-client",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
|
+
"Environment :: Console",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://specstream.ai"
|
|
37
|
+
Documentation = "https://specstream.ai/docs/sdk"
|
|
38
|
+
API = "https://api.specstream.ai"
|
|
39
|
+
Changelog = "https://specstream.ai/changelog"
|
|
40
|
+
Repository = "https://github.com/Specstreamai/specstream-tools"
|
|
41
|
+
Issues = "https://github.com/Specstreamai/specstream-tools/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
specstream = "specstream_client.cli:main"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling>=1.27,<2"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["specstream_client"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = ["specstream_client", "tests", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SPECSTREAM Python SDK generated against finder.cross-source.v1 OpenAPI."""
|
|
2
|
+
|
|
3
|
+
from specstream_client.client import DEFAULT_BASE_URL, Client, SpecstreamError
|
|
4
|
+
from specstream_client.families import FAMILIES, FAMILY_SET, assert_family
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Client",
|
|
8
|
+
"DEFAULT_BASE_URL",
|
|
9
|
+
"FAMILIES",
|
|
10
|
+
"FAMILY_SET",
|
|
11
|
+
"SpecstreamError",
|
|
12
|
+
"assert_family",
|
|
13
|
+
]
|
|
14
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Require an explicit signed-work authorization before any client HTTP call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def require_work_authorization(value: Any) -> None:
|
|
10
|
+
required = {"quote_id", "quote_hash", "idempotency_key", "confirm_max_cents"}
|
|
11
|
+
if (
|
|
12
|
+
not isinstance(value, dict)
|
|
13
|
+
or not required <= value.keys()
|
|
14
|
+
or set(value) - required - {"authority_id"}
|
|
15
|
+
):
|
|
16
|
+
raise ValueError(
|
|
17
|
+
"Provide the exact quote identity, signature, maximum cents and request key"
|
|
18
|
+
)
|
|
19
|
+
if not isinstance(value["quote_id"], str) or not value["quote_id"]:
|
|
20
|
+
raise ValueError("An explicit quote identity is required")
|
|
21
|
+
if not isinstance(value["quote_hash"], str) or not re.fullmatch(
|
|
22
|
+
r"[0-9a-f]{64}", value["quote_hash"]
|
|
23
|
+
):
|
|
24
|
+
raise ValueError("The reviewed quote signature is required")
|
|
25
|
+
key = value["idempotency_key"]
|
|
26
|
+
if not isinstance(key, str) or not 8 <= len(key) <= 128:
|
|
27
|
+
raise ValueError("Use a stable request key containing 8 to 128 characters")
|
|
28
|
+
maximum = value["confirm_max_cents"]
|
|
29
|
+
if type(maximum) is not int or maximum < 0:
|
|
30
|
+
raise ValueError("The approved maximum must be an explicit nonnegative integer in cents")
|
|
31
|
+
if value.get("authority_id") is not None and not isinstance(value["authority_id"], str):
|
|
32
|
+
raise ValueError("Authority must be an explicit existing identifier")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""SPECSTREAM console: inspect a Spec, prepare a Find, and read its Delivery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from typing import Any, TextIO
|
|
11
|
+
|
|
12
|
+
from specstream_client.client import DEFAULT_BASE_URL, Client, SpecstreamError
|
|
13
|
+
from specstream_client.families import FAMILIES
|
|
14
|
+
from specstream_client.lifecycle_commands import COMMANDS
|
|
15
|
+
|
|
16
|
+
PROG = "specstream"
|
|
17
|
+
DESC = (
|
|
18
|
+
"The web. To your spec. Compile the request, quote-capacity-work, then "
|
|
19
|
+
"authorize-capacity-work with the exact approved maximum and a stable key. "
|
|
20
|
+
"Read the same work, evidence and balance from every interface. "
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parser() -> argparse.ArgumentParser:
|
|
25
|
+
parser = argparse.ArgumentParser(prog=PROG, description=DESC)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--base-url", default=os.environ.get("SPECSTREAM_BASE_URL", DEFAULT_BASE_URL)
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("--api-key", default=os.environ.get("SPECSTREAM_API_KEY", ""))
|
|
30
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
31
|
+
compile_cmd = sub.add_parser("compile", help="Interpret a brief as an editable Spec")
|
|
32
|
+
compile_cmd.add_argument("q")
|
|
33
|
+
compile_cmd.add_argument("--locale", default="en-US")
|
|
34
|
+
edit = sub.add_parser(
|
|
35
|
+
"edit-spec", help="Validate exact edited AST JSON, including required facts"
|
|
36
|
+
)
|
|
37
|
+
edit.add_argument(
|
|
38
|
+
"--ast", required=True, type=argparse.FileType("r"), help="AST JSON file or -"
|
|
39
|
+
)
|
|
40
|
+
options = sub.add_parser("spec-options", help="Read supported conditions and return facts")
|
|
41
|
+
options.add_argument("target", choices=FAMILIES)
|
|
42
|
+
for command, help_text in (
|
|
43
|
+
("get", "Inspect an owned Find"),
|
|
44
|
+
("rows", "Read Matches with evidence"),
|
|
45
|
+
("revisit", "Read one page of retained-target outcomes"),
|
|
46
|
+
("export", "Request a Delivery download link"),
|
|
47
|
+
):
|
|
48
|
+
item = sub.add_parser(command, help=help_text)
|
|
49
|
+
item.add_argument("--find-id", required=True)
|
|
50
|
+
if command == "rows":
|
|
51
|
+
item.add_argument("--cursor", default=None)
|
|
52
|
+
item.add_argument("--limit", type=int, default=100)
|
|
53
|
+
if command == "revisit":
|
|
54
|
+
item.add_argument("--after", default="")
|
|
55
|
+
item.add_argument("--limit", type=int, default=100)
|
|
56
|
+
if command == "export":
|
|
57
|
+
item.add_argument("--format", default="csv", choices=("csv", "xlsx", "json", "html"))
|
|
58
|
+
sub.add_parser("families", help="Print supported public targets")
|
|
59
|
+
for name, contract in COMMANDS.items():
|
|
60
|
+
command = sub.add_parser(name, help=contract["summary"])
|
|
61
|
+
for argument in contract["arguments"]:
|
|
62
|
+
command.add_argument("--" + argument.replace("_", "-"), required=True)
|
|
63
|
+
if contract["body"]:
|
|
64
|
+
command.add_argument(
|
|
65
|
+
"--request",
|
|
66
|
+
required=contract["body_required"],
|
|
67
|
+
type=argparse.FileType("r"),
|
|
68
|
+
help="Exact request JSON file or - for stdin",
|
|
69
|
+
)
|
|
70
|
+
for parameter, kind in contract["query"].items():
|
|
71
|
+
options: dict[str, Any] = {"default": None}
|
|
72
|
+
if kind == "boolean":
|
|
73
|
+
options["action"] = argparse.BooleanOptionalAction
|
|
74
|
+
else:
|
|
75
|
+
options["type"] = int if kind == "integer" else str
|
|
76
|
+
command.add_argument("--" + parameter.replace("_", "-"), **options)
|
|
77
|
+
return parser
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _dump(payload: Any, out: TextIO) -> None:
|
|
81
|
+
json.dump(payload, out, indent=2, sort_keys=True)
|
|
82
|
+
out.write("\n")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main(
|
|
86
|
+
argv: Sequence[str] | None = None,
|
|
87
|
+
out: TextIO | None = None,
|
|
88
|
+
err: TextIO | None = None,
|
|
89
|
+
) -> int:
|
|
90
|
+
stdout, stderr = out or sys.stdout, err or sys.stderr
|
|
91
|
+
parser = _parser()
|
|
92
|
+
ns = parser.parse_args(list(argv) if argv is not None else None)
|
|
93
|
+
if not ns.cmd:
|
|
94
|
+
parser.print_help(stdout)
|
|
95
|
+
return 0
|
|
96
|
+
try:
|
|
97
|
+
if ns.cmd == "families":
|
|
98
|
+
result: Any = list(FAMILIES)
|
|
99
|
+
else:
|
|
100
|
+
client = Client(api_key=ns.api_key, base_url=ns.base_url)
|
|
101
|
+
if ns.cmd in COMMANDS:
|
|
102
|
+
contract = COMMANDS[ns.cmd]
|
|
103
|
+
arguments = [getattr(ns, argument) for argument in contract["arguments"]]
|
|
104
|
+
if contract["body"]:
|
|
105
|
+
body = None
|
|
106
|
+
if ns.request is not None:
|
|
107
|
+
with ns.request as source:
|
|
108
|
+
body = json.load(source)
|
|
109
|
+
if not isinstance(body, dict):
|
|
110
|
+
raise ValueError("Request must be a JSON object")
|
|
111
|
+
arguments.append(body)
|
|
112
|
+
options = {name: getattr(ns, name) for name in contract["query"]}
|
|
113
|
+
result = getattr(client, contract["method"])(*arguments, **options)
|
|
114
|
+
elif ns.cmd == "compile":
|
|
115
|
+
result = client.compile(ns.q, locale=ns.locale)
|
|
116
|
+
elif ns.cmd == "edit-spec":
|
|
117
|
+
with ns.ast as source:
|
|
118
|
+
ast = json.load(source)
|
|
119
|
+
if not isinstance(ast, dict):
|
|
120
|
+
raise ValueError("AST must be a JSON object")
|
|
121
|
+
result = client.edit_spec(ast)
|
|
122
|
+
elif ns.cmd == "spec-options":
|
|
123
|
+
result = client.spec_options(ns.target)
|
|
124
|
+
elif ns.cmd == "get":
|
|
125
|
+
result = client.get_find(ns.find_id)
|
|
126
|
+
elif ns.cmd == "rows":
|
|
127
|
+
result = client.rows(ns.find_id, cursor=ns.cursor, limit=ns.limit)
|
|
128
|
+
elif ns.cmd == "revisit":
|
|
129
|
+
result = client.revisit(ns.find_id, after=ns.after, limit=ns.limit)
|
|
130
|
+
else:
|
|
131
|
+
result = client.export(ns.find_id, format=ns.format)
|
|
132
|
+
_dump(result, stdout)
|
|
133
|
+
return 0
|
|
134
|
+
except SpecstreamError as exc:
|
|
135
|
+
print(str(exc), file=stderr)
|
|
136
|
+
return 1
|
|
137
|
+
except (ValueError, OSError) as exc:
|
|
138
|
+
print(str(exc), file=stderr)
|
|
139
|
+
return 2
|