specstream-client 1.2.0__tar.gz → 1.3.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.2.0 → specstream_client-1.3.0}/PKG-INFO +9 -6
- {specstream_client-1.2.0 → specstream_client-1.3.0}/README.md +8 -5
- {specstream_client-1.2.0 → specstream_client-1.3.0}/pyproject.toml +1 -1
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/__init__.py +1 -1
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/client.py +61 -1
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/lifecycle_commands.py +30 -1
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_client.py +20 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/.gitignore +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/LICENSE +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/__main__.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/authorization.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/cli.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/families.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/py.typed +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/__init__.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_cli.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_convergence.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_http_boundary.py +0 -0
- {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_m16_exports.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: specstream-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Python client for building lead lists and company lists from the live web. Every match comes with its source, priced before it runs.
|
|
5
5
|
Project-URL: Homepage, https://specstream.ai
|
|
6
6
|
Project-URL: Documentation, https://specstream.ai/docs/sdk
|
|
@@ -29,9 +29,12 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
`pip install specstream-client` (the `specstream` package on PyPI is a different project)
|
|
31
31
|
|
|
32
|
-
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine
|
|
32
|
+
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine. One platform to build, search, extract, enrich, map, and monitor data across the entire web.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Ask for any dataset in plain words, and SpecStream builds it from the entire live web: every match found, extracted and enriched into structured records, then monitored for change.
|
|
35
|
+
|
|
36
|
+
- Source URL and read time on every value.
|
|
37
|
+
- You pay for each row delivered.
|
|
35
38
|
|
|
36
39
|
The client has no dependencies and needs Python 3.12 or newer. It uses the same account, permissions, projects and balance as the workspace.
|
|
37
40
|
|
|
@@ -92,7 +95,7 @@ If the response is lost, `get_capacity_request(saved_request_key)` returns the s
|
|
|
92
95
|
|
|
93
96
|
- `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`.
|
|
94
97
|
- `spec_options(kind)` lists the facts you can ask for, per kind of thing. `edit_spec` changes a request without rereading it.
|
|
95
|
-
- `create_capacity_schedule` keeps a request
|
|
98
|
+
- `create_capacity_schedule` keeps a request up to date, hourly to yearly, inside per-run and per-period limits.
|
|
96
99
|
- `export` and `download_export` give CSV, XLSX, JSON or HTML. `create_customer_webhook` sends signed events.
|
|
97
100
|
- Projects, members, shares and starred items have their own methods.
|
|
98
101
|
|
|
@@ -101,7 +104,7 @@ Kinds of thing include local businesses, companies, people, creators, products,
|
|
|
101
104
|
## Command line
|
|
102
105
|
|
|
103
106
|
```sh
|
|
104
|
-
specstream compile "
|
|
107
|
+
specstream compile "Open-source vector databases on GitHub with more than 5,000 stars, with their license."
|
|
105
108
|
specstream get-capacity-balance
|
|
106
109
|
specstream quote-capacity-work --body quote-request.json
|
|
107
110
|
specstream authorize-capacity-work --body approved-execution.json
|
|
@@ -112,7 +115,7 @@ specstream rows --find-id FIND_ID
|
|
|
112
115
|
|
|
113
116
|
## Pricing
|
|
114
117
|
|
|
115
|
-
New accounts start with $10 of
|
|
118
|
+
New accounts start with $10 of credit for 30 days, no card. A delivered result costs 5 cents, 15 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).
|
|
116
119
|
|
|
117
120
|
## Links
|
|
118
121
|
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`pip install specstream-client` (the `specstream` package on PyPI is a different project)
|
|
4
4
|
|
|
5
|
-
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine
|
|
5
|
+
Python client and command line for [SpecStream](https://specstream.ai), the live web data engine. One platform to build, search, extract, enrich, map, and monitor data across the entire web.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Ask for any dataset in plain words, and SpecStream builds it from the entire live web: every match found, extracted and enriched into structured records, then monitored for change.
|
|
8
|
+
|
|
9
|
+
- Source URL and read time on every value.
|
|
10
|
+
- You pay for each row delivered.
|
|
8
11
|
|
|
9
12
|
The client has no dependencies and needs Python 3.12 or newer. It uses the same account, permissions, projects and balance as the workspace.
|
|
10
13
|
|
|
@@ -65,7 +68,7 @@ If the response is lost, `get_capacity_request(saved_request_key)` returns the s
|
|
|
65
68
|
|
|
66
69
|
- `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`.
|
|
67
70
|
- `spec_options(kind)` lists the facts you can ask for, per kind of thing. `edit_spec` changes a request without rereading it.
|
|
68
|
-
- `create_capacity_schedule` keeps a request
|
|
71
|
+
- `create_capacity_schedule` keeps a request up to date, hourly to yearly, inside per-run and per-period limits.
|
|
69
72
|
- `export` and `download_export` give CSV, XLSX, JSON or HTML. `create_customer_webhook` sends signed events.
|
|
70
73
|
- Projects, members, shares and starred items have their own methods.
|
|
71
74
|
|
|
@@ -74,7 +77,7 @@ Kinds of thing include local businesses, companies, people, creators, products,
|
|
|
74
77
|
## Command line
|
|
75
78
|
|
|
76
79
|
```sh
|
|
77
|
-
specstream compile "
|
|
80
|
+
specstream compile "Open-source vector databases on GitHub with more than 5,000 stars, with their license."
|
|
78
81
|
specstream get-capacity-balance
|
|
79
82
|
specstream quote-capacity-work --body quote-request.json
|
|
80
83
|
specstream authorize-capacity-work --body approved-execution.json
|
|
@@ -85,7 +88,7 @@ specstream rows --find-id FIND_ID
|
|
|
85
88
|
|
|
86
89
|
## Pricing
|
|
87
90
|
|
|
88
|
-
New accounts start with $10 of
|
|
91
|
+
New accounts start with $10 of credit for 30 days, no card. A delivered result costs 5 cents, 15 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).
|
|
89
92
|
|
|
90
93
|
## Links
|
|
91
94
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "specstream-client"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "Python client for building lead lists and company lists from the live web. Every match comes with its source, priced before it runs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -15,7 +15,7 @@ from specstream_client.authorization import require_work_authorization
|
|
|
15
15
|
from specstream_client.families import FAMILIES
|
|
16
16
|
|
|
17
17
|
DEFAULT_BASE_URL = "https://api.specstream.ai"
|
|
18
|
-
USER_AGENT = "specstream-python/1.
|
|
18
|
+
USER_AGENT = "specstream-python/1.3.0"
|
|
19
19
|
|
|
20
20
|
OpenFn = Callable[[urllib.request.Request, float], Any]
|
|
21
21
|
|
|
@@ -268,6 +268,64 @@ class Client:
|
|
|
268
268
|
path += "?" + encoded
|
|
269
269
|
return self._request("GET", path, None)
|
|
270
270
|
|
|
271
|
+
def web_search(self, *, q: str, passages: bool | None = None) -> dict[str, Any]:
|
|
272
|
+
"""
|
|
273
|
+
Fast search: answer a question from the live web in seconds, each value with its source.
|
|
274
|
+
"""
|
|
275
|
+
path = "/v1/web-search"
|
|
276
|
+
query: dict[str, Any] = {
|
|
277
|
+
"q": q,
|
|
278
|
+
"passages": passages,
|
|
279
|
+
}
|
|
280
|
+
encoded = urlencode(
|
|
281
|
+
{
|
|
282
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
283
|
+
for key, value in query.items()
|
|
284
|
+
if value is not None
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
if encoded:
|
|
288
|
+
path += "?" + encoded
|
|
289
|
+
return self._request("GET", path, None)
|
|
290
|
+
|
|
291
|
+
def extract_pages(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
292
|
+
"""
|
|
293
|
+
Read up to 20 pages live: each page's main text as Markdown or plain text, its title and
|
|
294
|
+
its published date. Only a page read is charged.
|
|
295
|
+
"""
|
|
296
|
+
path = "/v1/extract"
|
|
297
|
+
return self._request("POST", path, request)
|
|
298
|
+
|
|
299
|
+
def web_search_answers(
|
|
300
|
+
self,
|
|
301
|
+
*,
|
|
302
|
+
q: str | None = None,
|
|
303
|
+
limit: int | None = None,
|
|
304
|
+
cursor: str | None = None,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
"""List your web search history: each question, its answer and its sources, newest first."""
|
|
307
|
+
path = "/v1/web-search/answers"
|
|
308
|
+
query: dict[str, Any] = {
|
|
309
|
+
"q": q,
|
|
310
|
+
"limit": limit,
|
|
311
|
+
"cursor": cursor,
|
|
312
|
+
}
|
|
313
|
+
encoded = urlencode(
|
|
314
|
+
{
|
|
315
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
316
|
+
for key, value in query.items()
|
|
317
|
+
if value is not None
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
if encoded:
|
|
321
|
+
path += "?" + encoded
|
|
322
|
+
return self._request("GET", path, None)
|
|
323
|
+
|
|
324
|
+
def web_search_answer(self, answer_id: str) -> dict[str, Any]:
|
|
325
|
+
"""Open one kept web search answer as it was given."""
|
|
326
|
+
path = f"/v1/web-search/answers/{quote_path(answer_id, safe='')}"
|
|
327
|
+
return self._request("GET", path, None)
|
|
328
|
+
|
|
271
329
|
def workspace_list_notifications(
|
|
272
330
|
self,
|
|
273
331
|
*,
|
|
@@ -710,6 +768,7 @@ class Client:
|
|
|
710
768
|
project_id: str | None = None,
|
|
711
769
|
cursor: str | None = None,
|
|
712
770
|
limit: int | None = None,
|
|
771
|
+
format: str | None = None,
|
|
713
772
|
) -> dict[str, Any]:
|
|
714
773
|
"""Read where the request asked with this key stands and the rows it found so far."""
|
|
715
774
|
path = f"/v1/capacity/requests/{quote_path(request_key, safe='')}/rows"
|
|
@@ -717,6 +776,7 @@ class Client:
|
|
|
717
776
|
"project_id": project_id,
|
|
718
777
|
"cursor": cursor,
|
|
719
778
|
"limit": limit,
|
|
779
|
+
"format": format,
|
|
720
780
|
}
|
|
721
781
|
encoded = urlencode(
|
|
722
782
|
{
|
|
@@ -28,6 +28,34 @@ COMMANDS = {'get-vocabulary': {'method': 'get_vocabulary',
|
|
|
28
28
|
'body_required': False,
|
|
29
29
|
'query': {'q': 'string', 'limit': 'integer', 'cursor': 'string'},
|
|
30
30
|
'summary': 'Search the requests, lists and sources you can open'},
|
|
31
|
+
'web-search': {'method': 'web_search',
|
|
32
|
+
'arguments': [],
|
|
33
|
+
'body': False,
|
|
34
|
+
'body_required': False,
|
|
35
|
+
'query': {'q': 'string', 'passages': 'boolean'},
|
|
36
|
+
'summary': 'Fast search: answer a question from the live web in seconds, each '
|
|
37
|
+
'value with its source'},
|
|
38
|
+
'extract-pages': {'method': 'extract_pages',
|
|
39
|
+
'arguments': [],
|
|
40
|
+
'body': True,
|
|
41
|
+
'body_required': True,
|
|
42
|
+
'query': {},
|
|
43
|
+
'summary': "Read up to 20 pages live: each page's main text as Markdown or "
|
|
44
|
+
'plain text, its title and its published date. Only a page read is '
|
|
45
|
+
'charged'},
|
|
46
|
+
'web-search-answers': {'method': 'web_search_answers',
|
|
47
|
+
'arguments': [],
|
|
48
|
+
'body': False,
|
|
49
|
+
'body_required': False,
|
|
50
|
+
'query': {'q': 'string', 'limit': 'integer', 'cursor': 'string'},
|
|
51
|
+
'summary': 'List your web search history: each question, its answer and '
|
|
52
|
+
'its sources, newest first'},
|
|
53
|
+
'web-search-answer': {'method': 'web_search_answer',
|
|
54
|
+
'arguments': ['answer_id'],
|
|
55
|
+
'body': False,
|
|
56
|
+
'body_required': False,
|
|
57
|
+
'query': {},
|
|
58
|
+
'summary': 'Open one kept web search answer as it was given'},
|
|
31
59
|
'workspace-list-notifications': {'method': 'workspace_list_notifications',
|
|
32
60
|
'arguments': [],
|
|
33
61
|
'body': False,
|
|
@@ -342,7 +370,8 @@ COMMANDS = {'get-vocabulary': {'method': 'get_vocabulary',
|
|
|
342
370
|
'body_required': False,
|
|
343
371
|
'query': {'project_id': 'string',
|
|
344
372
|
'cursor': 'string',
|
|
345
|
-
'limit': 'integer'
|
|
373
|
+
'limit': 'integer',
|
|
374
|
+
'format': 'string'},
|
|
346
375
|
'summary': 'Read where the request asked with this key stands and '
|
|
347
376
|
'the rows it found so far'},
|
|
348
377
|
'get-capacity-execution': {'method': 'get_capacity_execution',
|
|
@@ -95,3 +95,23 @@ class ClientTests(unittest.TestCase):
|
|
|
95
95
|
self.assertEqual(client.authorize_capacity_work(approval)["find_id"], "fnd_fixture")
|
|
96
96
|
self.assertEqual(client.get_capacity_request("stable-review-001")["find_id"], "fnd_fixture")
|
|
97
97
|
self.assertFalse(any("/funding" in path or "/run" in path for _, path, _ in calls))
|
|
98
|
+
|
|
99
|
+
def test_web_search_returns_the_answers_kind_and_sources(self):
|
|
100
|
+
page = {
|
|
101
|
+
"request_id": "call-1",
|
|
102
|
+
"kind": "list",
|
|
103
|
+
"answer": None,
|
|
104
|
+
"rows": [{"name": "A", "page": "https://example.org/a", "sources": [0]}],
|
|
105
|
+
"sources": [{"url": "https://example.org/a", "cited": True}],
|
|
106
|
+
}
|
|
107
|
+
urls = []
|
|
108
|
+
|
|
109
|
+
def opener(request, timeout):
|
|
110
|
+
urls.append((request.get_method(), request.full_url))
|
|
111
|
+
return _FakeResponse(page)
|
|
112
|
+
|
|
113
|
+
found = Client("fixture-key", opener=opener).web_search(q="roasters in Lyon")
|
|
114
|
+
self.assertEqual(urls, [("GET", urls[0][1])])
|
|
115
|
+
self.assertTrue(urls[0][1].endswith("/v1/web-search?q=roasters+in+Lyon"))
|
|
116
|
+
self.assertEqual((found["kind"], found["sources"]), (page["kind"], page["sources"]))
|
|
117
|
+
self.assertEqual(found["rows"][0]["sources"], [0])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|