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.
Files changed (19) hide show
  1. {specstream_client-1.2.0 → specstream_client-1.3.0}/PKG-INFO +9 -6
  2. {specstream_client-1.2.0 → specstream_client-1.3.0}/README.md +8 -5
  3. {specstream_client-1.2.0 → specstream_client-1.3.0}/pyproject.toml +1 -1
  4. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/__init__.py +1 -1
  5. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/client.py +61 -1
  6. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/lifecycle_commands.py +30 -1
  7. {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_client.py +20 -0
  8. {specstream_client-1.2.0 → specstream_client-1.3.0}/.gitignore +0 -0
  9. {specstream_client-1.2.0 → specstream_client-1.3.0}/LICENSE +0 -0
  10. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/__main__.py +0 -0
  11. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/authorization.py +0 -0
  12. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/cli.py +0 -0
  13. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/families.py +0 -0
  14. {specstream_client-1.2.0 → specstream_client-1.3.0}/specstream_client/py.typed +0 -0
  15. {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/__init__.py +0 -0
  16. {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_cli.py +0 -0
  17. {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_convergence.py +0 -0
  18. {specstream_client-1.2.0 → specstream_client-1.3.0}/tests/test_http_boundary.py +0 -0
  19. {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.2.0
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 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, and you pay for each result delivered.
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
- Source URL and read time on every value.
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 current, hourly to yearly, inside per-run and per-period limits.
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 "Food banks in Texas, with their annual revenue."
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 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).
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 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, and you pay for each result delivered.
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
- Source URL and read time on every value.
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 current, hourly to yearly, inside per-run and per-period limits.
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 "Food banks in Texas, with their annual revenue."
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 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).
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.2.0"
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"
@@ -11,4 +11,4 @@ __all__ = [
11
11
  "SpecstreamError",
12
12
  "assert_family",
13
13
  ]
14
- __version__ = "1.2.0"
14
+ __version__ = "1.3.0"
@@ -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.2.0"
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])