riveter-sdk 0.1.0__py3-none-any.whl

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.
riveter/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Official Python SDK for the Riveter API. https://docs.riveterhq.com"""
2
+
3
+ from ._client import Riveter
4
+ from ._errors import APIConnectionError, APIError, APITimeoutError, RiveterError
5
+ from ._models import (
6
+ Account,
7
+ AccountInfo,
8
+ AccountUser,
9
+ ApiKeyInfo,
10
+ Credit,
11
+ Enrichment,
12
+ EnrichmentSummary,
13
+ Extraction,
14
+ Monitor,
15
+ Pagination,
16
+ Run,
17
+ RunListItem,
18
+ RunProgress,
19
+ RunsSummary,
20
+ ScrapeResult,
21
+ )
22
+ from ._pagination import RunsPage
23
+ from ._version import __version__
24
+
25
+ __all__ = [
26
+ "Riveter",
27
+ "RiveterError",
28
+ "APIError",
29
+ "APIConnectionError",
30
+ "APITimeoutError",
31
+ "Run",
32
+ "RunProgress",
33
+ "RunListItem",
34
+ "RunsPage",
35
+ "RunsSummary",
36
+ "Pagination",
37
+ "Enrichment",
38
+ "EnrichmentSummary",
39
+ "Extraction",
40
+ "Monitor",
41
+ "ScrapeResult",
42
+ "Account",
43
+ "AccountInfo",
44
+ "AccountUser",
45
+ "ApiKeyInfo",
46
+ "Credit",
47
+ "__version__",
48
+ ]
riveter/_client.py ADDED
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import httpx
8
+
9
+ from ._errors import APIConnectionError, APIError, APITimeoutError, RiveterError
10
+ from ._models import AccountInfo, Run, ScrapeResult
11
+ from ._resources import ConfiguredDatasets, Datasets, Enrichments, Extractions, Monitors, Runs
12
+ from ._version import __version__
13
+
14
+ DEFAULT_BASE_URL = "https://api.riveterhq.com/v1"
15
+
16
+
17
+ def _drop_none(mapping: Dict[str, Any]) -> Dict[str, Any]:
18
+ return {key: value for key, value in mapping.items() if value is not None}
19
+
20
+
21
+ class Riveter:
22
+ """Client for the Riveter API. https://docs.riveterhq.com
23
+
24
+ Args:
25
+ api_key: Falls back to the ``RIVETER_API_KEY`` environment variable.
26
+ base_url: Falls back to ``RIVETER_BASE_URL``, then the production URL.
27
+ timeout: Per-request timeout in seconds; must exceed the 50s ``wait``
28
+ long-poll. Default 60.
29
+ max_retries: Extra attempts after a 429 (any method) or a 5xx/network
30
+ failure (GETs only). Default 2.
31
+ http_client: Custom ``httpx.Client`` (proxies, mock transports, ...).
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ api_key: Optional[str] = None,
37
+ base_url: Optional[str] = None,
38
+ timeout: float = 60.0,
39
+ max_retries: int = 2,
40
+ http_client: Optional[httpx.Client] = None,
41
+ ):
42
+ self.api_key = api_key or os.environ.get("RIVETER_API_KEY")
43
+ if not self.api_key:
44
+ raise RiveterError(
45
+ "Missing API key: pass api_key or set RIVETER_API_KEY "
46
+ "(get one at https://app.riveterhq.com/settings/api)"
47
+ )
48
+ self.base_url = (base_url or os.environ.get("RIVETER_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
49
+ self.timeout = timeout
50
+ self.max_retries = max_retries
51
+ self._http = http_client or httpx.Client(timeout=timeout)
52
+ self.runs = Runs(self)
53
+ self.enrichments = Enrichments(self)
54
+ self.datasets = Datasets(self)
55
+ self.configured_datasets = ConfiguredDatasets(self)
56
+ self.extractions = Extractions(self)
57
+ self.monitors = Monitors(self)
58
+
59
+ def close(self) -> None:
60
+ self._http.close()
61
+
62
+ def __enter__(self) -> "Riveter":
63
+ return self
64
+
65
+ def __exit__(self, *exc_info: Any) -> None:
66
+ self.close()
67
+
68
+ # ----- top-level operations -----
69
+
70
+ def enrich(
71
+ self,
72
+ *,
73
+ enrichment_id: Optional[str] = None,
74
+ output: Optional[Dict[str, Any]] = None,
75
+ prompt: Optional[str] = None,
76
+ attributes: Optional[List[str]] = None,
77
+ input: Optional[Dict[str, List[str]]] = None,
78
+ dataset_id: Optional[str] = None,
79
+ run_key: Optional[str] = None,
80
+ webhook_url: Optional[str] = None,
81
+ allow_duplicate_input: Optional[bool] = None,
82
+ ) -> Run:
83
+ """Start an enrichment run on rows of input data. (operationId: enrich)"""
84
+ body = _drop_none(
85
+ {
86
+ "enrichment_id": enrichment_id,
87
+ "output": output,
88
+ "prompt": prompt,
89
+ "attributes": attributes,
90
+ "input": input,
91
+ "dataset_id": dataset_id,
92
+ "run_key": run_key,
93
+ "webhook_url": webhook_url,
94
+ "allow_duplicate_input": allow_duplicate_input,
95
+ }
96
+ )
97
+ return Run.from_dict(self.request("POST", "/enrich", json=body))
98
+
99
+ def quick_search(
100
+ self,
101
+ query: str,
102
+ *,
103
+ date_start: Optional[str] = None,
104
+ date_end: Optional[str] = None,
105
+ run_key: Optional[str] = None,
106
+ ) -> Run:
107
+ """Run one web search synchronously; results are already in ``output``.
108
+ (operationId: quickSearch)"""
109
+ body = _drop_none(
110
+ {"query": query, "date_start": date_start, "date_end": date_end, "run_key": run_key}
111
+ )
112
+ return Run.from_dict(self.request("POST", "/quick_search", json=body))
113
+
114
+ def scrape(
115
+ self,
116
+ url: str,
117
+ *,
118
+ proxy_country_code: Optional[str] = None,
119
+ skip_cache: Optional[bool] = None,
120
+ ) -> ScrapeResult:
121
+ """Scrape a webpage and return its text synchronously. (operationId: scrape)"""
122
+ body = _drop_none(
123
+ {"url": url, "proxy_country_code": proxy_country_code, "skip_cache": skip_cache}
124
+ )
125
+ return ScrapeResult.from_dict(self.request("POST", "/scrape", json=body))
126
+
127
+ def account(self) -> AccountInfo:
128
+ """Account, plan, and credit info for the API key. (operationId: getAccount)"""
129
+ return AccountInfo.from_dict(self.request("GET", "/account"))
130
+
131
+ # ----- transport -----
132
+
133
+ def request(
134
+ self,
135
+ method: str,
136
+ path: str,
137
+ *,
138
+ params: Optional[Dict[str, Any]] = None,
139
+ json: Optional[Dict[str, Any]] = None,
140
+ timeout: Optional[float] = None,
141
+ ) -> Any:
142
+ url = self.base_url + path
143
+ headers = {
144
+ "Authorization": f"Bearer {self.api_key}",
145
+ "User-Agent": f"riveter-sdk-python/{__version__}",
146
+ }
147
+ retry_on_failure = method == "GET"
148
+ attempt = 0
149
+ while True:
150
+ try:
151
+ response = self._http.request(
152
+ method,
153
+ url,
154
+ params=_drop_none(params or {}) or None,
155
+ json=json,
156
+ headers=headers,
157
+ timeout=timeout if timeout is not None else self.timeout,
158
+ )
159
+ except httpx.TimeoutException as exc:
160
+ if retry_on_failure and attempt < self.max_retries:
161
+ time.sleep(_backoff_seconds(attempt))
162
+ attempt += 1
163
+ continue
164
+ raise APITimeoutError(f"Request timed out: {method} {path}") from exc
165
+ except httpx.HTTPError as exc:
166
+ if retry_on_failure and attempt < self.max_retries:
167
+ time.sleep(_backoff_seconds(attempt))
168
+ attempt += 1
169
+ continue
170
+ raise APIConnectionError(f"Request failed: {method} {path}: {exc}") from exc
171
+
172
+ if response.is_success:
173
+ return response.json() if response.content else None
174
+
175
+ if response.status_code == 429 and attempt < self.max_retries:
176
+ time.sleep(_rate_limit_delay_seconds(response, attempt))
177
+ attempt += 1
178
+ continue
179
+ if response.status_code >= 500 and retry_on_failure and attempt < self.max_retries:
180
+ time.sleep(_backoff_seconds(attempt))
181
+ attempt += 1
182
+ continue
183
+
184
+ raise APIError.from_response(response.status_code, _parse_body(response))
185
+
186
+
187
+ def _backoff_seconds(attempt: int) -> float:
188
+ return min(0.5 * (2**attempt), 8.0)
189
+
190
+
191
+ def _rate_limit_delay_seconds(response: httpx.Response, attempt: int) -> float:
192
+ try:
193
+ reset = float(response.headers.get("X-RateLimit-Reset", ""))
194
+ except ValueError:
195
+ return _backoff_seconds(attempt)
196
+ if reset <= 0:
197
+ return _backoff_seconds(attempt)
198
+ return min(max(reset - time.time(), 0.0), 30.0)
199
+
200
+
201
+ def _parse_body(response: httpx.Response) -> Any:
202
+ try:
203
+ return response.json()
204
+ except ValueError:
205
+ return response.text
riveter/_errors.py ADDED
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ _FALLBACK_TYPES = {
6
+ 400: "bad_request",
7
+ 401: "unauthorized",
8
+ 403: "forbidden",
9
+ 404: "not_found",
10
+ 409: "conflict",
11
+ 422: "validation",
12
+ 429: "rate_limited",
13
+ }
14
+
15
+
16
+ class RiveterError(Exception):
17
+ """Base class for every error raised by this SDK."""
18
+
19
+
20
+ class APIConnectionError(RiveterError):
21
+ """Network-level failure: the request never produced an HTTP response."""
22
+
23
+
24
+ class APITimeoutError(APIConnectionError):
25
+ """A request or wait helper exceeded its time budget."""
26
+
27
+
28
+ class APIError(RiveterError):
29
+ """A non-2xx HTTP response, normalized across both error body dialects."""
30
+
31
+ def __init__(self, status: int, type: str, message: str, details: Any = None):
32
+ super().__init__(message)
33
+ self.status = status
34
+ self.type = type
35
+ self.message = message
36
+ self.details = details
37
+
38
+ @classmethod
39
+ def from_response(cls, status: int, body: Any) -> "APIError":
40
+ """Builds an APIError from either the clean ``{"error": {...}}`` envelope
41
+ or the legacy ``{"request_status": "error"}`` shape (401s)."""
42
+ if isinstance(body, dict):
43
+ error = body.get("error")
44
+ if isinstance(error, dict) and isinstance(error.get("type"), str):
45
+ return cls(
46
+ status=status,
47
+ type=error["type"],
48
+ message=error.get("message") or f"HTTP {status}",
49
+ details=error.get("details"),
50
+ )
51
+ if body.get("request_status") == "error":
52
+ return cls(
53
+ status=status,
54
+ type=body.get("error_type") or "error",
55
+ message=body.get("message") or f"HTTP {status}",
56
+ details=body.get("errors"),
57
+ )
58
+ message: Optional[str] = body[:200] if isinstance(body, str) and body else None
59
+ return cls(
60
+ status=status,
61
+ type=_FALLBACK_TYPES.get(status, f"http_{status}"),
62
+ message=message or f"HTTP {status}",
63
+ )
riveter/_models.py ADDED
@@ -0,0 +1,285 @@
1
+ """Typed views over API response dicts.
2
+
3
+ Models keep the full response in ``.raw``, so fields added to the API later
4
+ stay reachable without an SDK update.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field, fields
10
+ from typing import Any, Callable, Dict, List, Optional
11
+
12
+
13
+ def _build(cls, data: Optional[Dict[str, Any]], **nested: Callable[[Any], Any]):
14
+ data = dict(data or {})
15
+ known = {f.name: data.get(f.name) for f in fields(cls) if f.name != "raw"}
16
+ for name, builder in nested.items():
17
+ value = data.get(name)
18
+ known[name] = builder(value) if value is not None else None
19
+ return cls(**known, raw=data)
20
+
21
+
22
+ @dataclass
23
+ class RunProgress:
24
+ percent_complete: Optional[float] = None
25
+ estimated_seconds_remaining: Optional[float] = None
26
+ elapsed_seconds: Optional[float] = None
27
+ completed_cells: Optional[int] = None
28
+ total_cells_expected: Optional[int] = None
29
+ not_found_cells: Optional[int] = None
30
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
31
+
32
+ @classmethod
33
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RunProgress":
34
+ return _build(cls, data)
35
+
36
+
37
+ @dataclass
38
+ class Run:
39
+ """The uniform run envelope returned by kickoffs and every /runs endpoint.
40
+
41
+ Endpoint-specific kickoff extras (``dataset_id``, ``credits_charged``, ...)
42
+ are optional fields; ``output`` is ``None`` until the run finishes.
43
+ """
44
+
45
+ id: Optional[str] = None
46
+ type: Optional[str] = None
47
+ status: Optional[str] = None
48
+ progress: Optional[RunProgress] = None
49
+ credits_used: Optional[float] = None
50
+ app_url: Optional[str] = None
51
+ result_url: Optional[str] = None
52
+ started_at: Optional[str] = None
53
+ finished_at: Optional[str] = None
54
+ error: Optional[Dict[str, Any]] = None
55
+ enrichment_id: Optional[str] = None
56
+ enrichment_name: Optional[str] = None
57
+ dataset_id: Optional[str] = None
58
+ extraction_id: Optional[str] = None
59
+ monitor_id: Optional[str] = None
60
+ webhook_url: Optional[str] = None
61
+ output: Any = None
62
+ max_items: Optional[int] = None
63
+ enrichment_run_id: Optional[str] = None
64
+ source_dataset_id: Optional[str] = None
65
+ configured_dataset_id: Optional[str] = None
66
+ credits_charged: Optional[float] = None
67
+ variables: Optional[Dict[str, Any]] = None
68
+ validation_warning: Optional[str] = None
69
+ tier: Optional[str] = None
70
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Run":
74
+ return _build(cls, data, progress=RunProgress.from_dict)
75
+
76
+ @property
77
+ def is_finished(self) -> bool:
78
+ return self.status in ("success", "stopped")
79
+
80
+
81
+ @dataclass
82
+ class RunListItem:
83
+ id: Optional[str] = None
84
+ type: Optional[str] = None
85
+ status: Optional[str] = None
86
+ enrichment_id: Optional[str] = None
87
+ enrichment_name: Optional[str] = None
88
+ row_count: Optional[int] = None
89
+ credits_used: Optional[float] = None
90
+ error: Optional[Dict[str, Any]] = None
91
+ created_at: Optional[str] = None
92
+ started_at: Optional[str] = None
93
+ finished_at: Optional[str] = None
94
+ app_url: Optional[str] = None
95
+ result_url: Optional[str] = None
96
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
97
+
98
+ @classmethod
99
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RunListItem":
100
+ return _build(cls, data)
101
+
102
+
103
+ @dataclass
104
+ class Pagination:
105
+ page: Optional[int] = None
106
+ per_page: Optional[int] = None
107
+ total_count: Optional[int] = None
108
+ total_pages: Optional[int] = None
109
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
110
+
111
+ @classmethod
112
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Pagination":
113
+ return _build(cls, data)
114
+
115
+
116
+ @dataclass
117
+ class RunsSummary:
118
+ counts: Optional[Dict[str, int]] = None
119
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
120
+
121
+ @classmethod
122
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RunsSummary":
123
+ return _build(cls, data)
124
+
125
+
126
+ @dataclass
127
+ class EnrichmentSummary:
128
+ id: Optional[str] = None
129
+ name: Optional[str] = None
130
+ status: Optional[str] = None
131
+ app_url: Optional[str] = None
132
+ columns: Optional[Dict[str, Any]] = None
133
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
134
+
135
+ @classmethod
136
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "EnrichmentSummary":
137
+ return _build(cls, data)
138
+
139
+
140
+ @dataclass
141
+ class Enrichment:
142
+ id: Optional[str] = None
143
+ name: Optional[str] = None
144
+ status: Optional[str] = None
145
+ app_url: Optional[str] = None
146
+ input: Optional[Dict[str, Any]] = None
147
+ output: Optional[Dict[str, Any]] = None
148
+ dataset_id: Optional[str] = None
149
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
150
+
151
+ @classmethod
152
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Enrichment":
153
+ return _build(cls, data)
154
+
155
+
156
+ @dataclass
157
+ class Extraction:
158
+ id: Optional[str] = None
159
+ name: Optional[str] = None
160
+ status: Optional[str] = None
161
+ app_url: Optional[str] = None
162
+ starting_url: Optional[str] = None
163
+ goal_description: Optional[str] = None
164
+ output_record_json_schema: Any = None
165
+ required_keys: Optional[List[str]] = None
166
+ locked: Optional[bool] = None
167
+ validation_passing: Optional[bool] = None
168
+ discovered_at: Optional[str] = None
169
+ run_credits_required: Optional[float] = None
170
+ credits_charged: Optional[float] = None
171
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
172
+
173
+ @classmethod
174
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Extraction":
175
+ return _build(cls, data)
176
+
177
+
178
+ @dataclass
179
+ class Monitor:
180
+ id: Optional[str] = None
181
+ name: Optional[str] = None
182
+ enabled: Optional[bool] = None
183
+ cadence: Optional[str] = None
184
+ minute: Optional[int] = None
185
+ hour: Optional[int] = None
186
+ day_of_week: Optional[int] = None
187
+ day_of_month: Optional[int] = None
188
+ timezone: Optional[str] = None
189
+ webhook_url: Optional[str] = None
190
+ alert_rule: Optional[str] = None
191
+ output_format: Optional[str] = None
192
+ next_run_at: Optional[str] = None
193
+ schedule_summary: Optional[str] = None
194
+ enrichment_id: Optional[str] = None
195
+ enrichment_name: Optional[str] = None
196
+ created_at: Optional[str] = None
197
+ has_input: Optional[bool] = None
198
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
199
+
200
+ @classmethod
201
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Monitor":
202
+ return _build(cls, data)
203
+
204
+
205
+ @dataclass
206
+ class ScrapeResult:
207
+ """Synchronous scrape result (legacy request_status format, per the spec)."""
208
+
209
+ request_status: Optional[str] = None
210
+ text: Optional[str] = None
211
+ url: Optional[str] = None
212
+ base_url_for_links: Optional[str] = None
213
+ status_code: Optional[int] = None
214
+ possibly_blocked: Optional[bool] = None
215
+ credit_used: Optional[float] = None
216
+ riveter_app_link: Optional[str] = None
217
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
218
+
219
+ @classmethod
220
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "ScrapeResult":
221
+ return _build(cls, data)
222
+
223
+
224
+ @dataclass
225
+ class Credit:
226
+ count: Optional[int] = None
227
+ max: Optional[int] = None
228
+ balance: Optional[int] = None
229
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
230
+
231
+ @classmethod
232
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Credit":
233
+ return _build(cls, data)
234
+
235
+
236
+ @dataclass
237
+ class Account:
238
+ uuid: Optional[str] = None
239
+ name: Optional[str] = None
240
+ plan: Optional[str] = None
241
+ credit: Optional[Credit] = None
242
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
243
+
244
+ @classmethod
245
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Account":
246
+ return _build(cls, data, credit=Credit.from_dict)
247
+
248
+
249
+ @dataclass
250
+ class AccountUser:
251
+ uuid: Optional[str] = None
252
+ name: Optional[str] = None
253
+ email: Optional[str] = None
254
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
255
+
256
+ @classmethod
257
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "AccountUser":
258
+ return _build(cls, data)
259
+
260
+
261
+ @dataclass
262
+ class ApiKeyInfo:
263
+ name: Optional[str] = None
264
+ last_used_at: Optional[str] = None
265
+ created_by: Optional[AccountUser] = None
266
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
267
+
268
+ @classmethod
269
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "ApiKeyInfo":
270
+ return _build(cls, data, created_by=AccountUser.from_dict)
271
+
272
+
273
+ @dataclass
274
+ class AccountInfo:
275
+ """Account info (legacy request_status format, per the spec)."""
276
+
277
+ request_status: Optional[str] = None
278
+ message: Optional[str] = None
279
+ account: Optional[Account] = None
280
+ api_key_info: Optional[ApiKeyInfo] = None
281
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
282
+
283
+ @classmethod
284
+ def from_dict(cls, data: Optional[Dict[str, Any]]) -> "AccountInfo":
285
+ return _build(cls, data, account=Account.from_dict, api_key_info=ApiKeyInfo.from_dict)
riveter/_pagination.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Iterator, Optional
4
+
5
+ from ._models import Pagination, RunListItem
6
+
7
+
8
+ class RunsPage:
9
+ """One page of runs. Iterate the page directly, or use
10
+ :meth:`auto_paging_iter` to walk every following page."""
11
+
12
+ def __init__(self, data: Optional[Dict[str, Any]], fetch_page: Callable[[int], Dict[str, Any]]):
13
+ data = data or {}
14
+ self.runs = [RunListItem.from_dict(item) for item in data.get("runs") or []]
15
+ self.pagination = Pagination.from_dict(data.get("pagination"))
16
+ self.monitor_id = data.get("monitor_id")
17
+ self._fetch_page = fetch_page
18
+
19
+ def __iter__(self) -> Iterator[RunListItem]:
20
+ return iter(self.runs)
21
+
22
+ def __len__(self) -> int:
23
+ return len(self.runs)
24
+
25
+ @property
26
+ def has_next_page(self) -> bool:
27
+ return (self.pagination.page or 1) < (self.pagination.total_pages or 1)
28
+
29
+ def next_page(self) -> "RunsPage":
30
+ return RunsPage(self._fetch_page((self.pagination.page or 1) + 1), self._fetch_page)
31
+
32
+ def auto_paging_iter(self) -> Iterator[RunListItem]:
33
+ page = self
34
+ while True:
35
+ for run in page.runs:
36
+ yield run
37
+ if not page.has_next_page:
38
+ return
39
+ page = page.next_page()
riveter/_resources.py ADDED
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
5
+
6
+ from ._errors import APITimeoutError
7
+ from ._models import Enrichment, EnrichmentSummary, Extraction, Monitor, Run, RunsSummary
8
+ from ._pagination import RunsPage
9
+
10
+ if TYPE_CHECKING:
11
+ from ._client import Riveter
12
+
13
+
14
+ def _drop_none(mapping: Dict[str, Any]) -> Dict[str, Any]:
15
+ return {key: value for key, value in mapping.items() if value is not None}
16
+
17
+
18
+ class Runs:
19
+ """The uniform lifecycle for every async operation — status, results, stop."""
20
+
21
+ def __init__(self, client: "Riveter"):
22
+ self._client = client
23
+
24
+ def get(self, run_id: str) -> Run:
25
+ """Status and progress of a run. (operationId: getRun)"""
26
+ return Run.from_dict(self._client.request("GET", f"/runs/{run_id}"))
27
+
28
+ def result(self, run_id: str, *, wait: Optional[int] = None) -> Run:
29
+ """The run plus its ``output`` (None until finished). ``wait``
30
+ long-polls up to 50 seconds server-side. (operationId: getRunResult)"""
31
+ timeout = max(self._client.timeout, wait + 10) if wait else None
32
+ data = self._client.request(
33
+ "GET", f"/runs/{run_id}/result", params={"wait": wait}, timeout=timeout
34
+ )
35
+ return Run.from_dict(data)
36
+
37
+ def stop(self, run_id: str) -> Run:
38
+ """Stop a run early. (operationId: stopRun)"""
39
+ return Run.from_dict(self._client.request("POST", f"/runs/{run_id}/stop"))
40
+
41
+ def list(
42
+ self,
43
+ *,
44
+ type: Optional[str] = None,
45
+ status: Optional[str] = None,
46
+ enrichment_id: Optional[str] = None,
47
+ monitor_id: Optional[str] = None,
48
+ created_after: Optional[str] = None,
49
+ created_before: Optional[str] = None,
50
+ page: Optional[int] = None,
51
+ per_page: Optional[int] = None,
52
+ ) -> RunsPage:
53
+ """List the account's runs, newest first. (operationId: listRuns)"""
54
+ params = _drop_none(
55
+ {
56
+ "type": type,
57
+ "status": status,
58
+ "enrichment_id": enrichment_id,
59
+ "monitor_id": monitor_id,
60
+ "created_after": created_after,
61
+ "created_before": created_before,
62
+ "per_page": per_page,
63
+ }
64
+ )
65
+
66
+ def fetch_page(page_number: int) -> Dict[str, Any]:
67
+ return self._client.request("GET", "/runs", params={**params, "page": page_number})
68
+
69
+ return RunsPage(fetch_page(page or 1), fetch_page)
70
+
71
+ def summary(self) -> RunsSummary:
72
+ """All-time run counts by status. (operationId: runsSummary)"""
73
+ return RunsSummary.from_dict(self._client.request("GET", "/runs/summary"))
74
+
75
+ def wait_for_result(self, run_id: str, *, timeout: float = 600.0, poll_wait: int = 50) -> Run:
76
+ """Polls :meth:`result` with 50s long-polls until the run reaches
77
+ ``success`` or ``stopped``; raises APITimeoutError past ``timeout``."""
78
+ deadline = time.monotonic() + timeout
79
+ while True:
80
+ run = self.result(run_id, wait=poll_wait)
81
+ if run.is_finished:
82
+ return run
83
+ if time.monotonic() >= deadline:
84
+ raise APITimeoutError(f"Run {run_id} did not finish within {timeout}s")
85
+
86
+
87
+ class Enrichments:
88
+ """Saved enrichment configurations."""
89
+
90
+ def __init__(self, client: "Riveter"):
91
+ self._client = client
92
+
93
+ def list(self) -> List[EnrichmentSummary]:
94
+ """List enrichments with their output column configuration. (operationId: listEnrichments)"""
95
+ response = self._client.request("GET", "/enrichments")
96
+ return [EnrichmentSummary.from_dict(item) for item in response.get("enrichments") or []]
97
+
98
+ def create(self, *, dataset_id: str) -> Enrichment:
99
+ """Create an enrichment (no run) from a completed dataset build. (operationId: createEnrichment)"""
100
+ return Enrichment.from_dict(
101
+ self._client.request("POST", "/enrichments", json={"dataset_id": dataset_id})
102
+ )
103
+
104
+ def get(self, enrichment_id: str) -> Enrichment:
105
+ """The enrichment's input columns and output column configuration. (operationId: getEnrichment)"""
106
+ return Enrichment.from_dict(self._client.request("GET", f"/enrichments/{enrichment_id}"))
107
+
108
+ def update(
109
+ self,
110
+ enrichment_id: str,
111
+ *,
112
+ output: Optional[Dict[str, Any]] = None,
113
+ column_order: Optional[List[str]] = None,
114
+ ) -> Dict[str, Any]:
115
+ """Add, update, rename, delete, or reorder output columns. (operationId: updateEnrichment)"""
116
+ body = _drop_none({"output": output, "column_order": column_order})
117
+ return self._client.request("PATCH", f"/enrichments/{enrichment_id}", json=body)
118
+
119
+ def build_dataset(
120
+ self,
121
+ enrichment_id: str,
122
+ *,
123
+ prompt: str,
124
+ qualifiers: Optional[List[str]] = None,
125
+ max_items: Optional[int] = None,
126
+ dataset_webhook_url: Optional[str] = None,
127
+ auto_run_enrichment: Optional[bool] = None,
128
+ auto_run_enrichment_webhook_url: Optional[str] = None,
129
+ ) -> Run:
130
+ """Build a dataset shaped for this enrichment. (operationId: buildDatasetForEnrichment)"""
131
+ body = _drop_none(
132
+ {
133
+ "prompt": prompt,
134
+ "qualifiers": qualifiers,
135
+ "max_items": max_items,
136
+ "dataset_webhook_url": dataset_webhook_url,
137
+ "auto_run_enrichment": auto_run_enrichment,
138
+ "auto_run_enrichment_webhook_url": auto_run_enrichment_webhook_url,
139
+ }
140
+ )
141
+ return Run.from_dict(
142
+ self._client.request("POST", f"/enrichments/{enrichment_id}/datasets", json=body)
143
+ )
144
+
145
+
146
+ class Datasets:
147
+ """Dataset builds — generate rows from prompts or structured specs."""
148
+
149
+ def __init__(self, client: "Riveter"):
150
+ self._client = client
151
+
152
+ def build(
153
+ self,
154
+ *,
155
+ prompt: Optional[str] = None,
156
+ identifiers: Optional[List[str]] = None,
157
+ qualifiers: Optional[List[str]] = None,
158
+ attributes: Optional[List[str]] = None,
159
+ max_items: Optional[int] = None,
160
+ dataset_webhook_url: Optional[str] = None,
161
+ auto_run_enrichment: Optional[bool] = None,
162
+ auto_run_enrichment_webhook_url: Optional[str] = None,
163
+ ) -> Run:
164
+ """Build a dataset from a prompt, a structured spec, or both. (operationId: buildDataset)"""
165
+ body = _drop_none(
166
+ {
167
+ "prompt": prompt,
168
+ "identifiers": identifiers,
169
+ "qualifiers": qualifiers,
170
+ "attributes": attributes,
171
+ "max_items": max_items,
172
+ "dataset_webhook_url": dataset_webhook_url,
173
+ "auto_run_enrichment": auto_run_enrichment,
174
+ "auto_run_enrichment_webhook_url": auto_run_enrichment_webhook_url,
175
+ }
176
+ )
177
+ return Run.from_dict(self._client.request("POST", "/datasets", json=body))
178
+
179
+ def extend(
180
+ self,
181
+ dataset_id: str,
182
+ *,
183
+ prompt: Optional[str] = None,
184
+ qualifiers: Optional[List[str]] = None,
185
+ max_items: Optional[int] = None,
186
+ dataset_webhook_url: Optional[str] = None,
187
+ auto_run_enrichment: Optional[bool] = None,
188
+ auto_run_enrichment_webhook_url: Optional[str] = None,
189
+ ) -> Run:
190
+ """Generate new deduplicated rows for a completed dataset build. (operationId: extendDataset)"""
191
+ body = _drop_none(
192
+ {
193
+ "prompt": prompt,
194
+ "qualifiers": qualifiers,
195
+ "max_items": max_items,
196
+ "dataset_webhook_url": dataset_webhook_url,
197
+ "auto_run_enrichment": auto_run_enrichment,
198
+ "auto_run_enrichment_webhook_url": auto_run_enrichment_webhook_url,
199
+ }
200
+ )
201
+ return Run.from_dict(self._client.request("POST", f"/datasets/{dataset_id}/extend", json=body))
202
+
203
+
204
+ class ConfiguredDatasets:
205
+ """Reusable, pre-configured dataset templates (cds_...)."""
206
+
207
+ def __init__(self, client: "Riveter"):
208
+ self._client = client
209
+
210
+ def build(
211
+ self,
212
+ configured_dataset_id: str,
213
+ *,
214
+ parameters: Optional[Dict[str, str]] = None,
215
+ tier: Optional[str] = None,
216
+ dataset_webhook_url: Optional[str] = None,
217
+ ) -> Run:
218
+ """Run a configured dataset template. (operationId: buildConfiguredDataset)"""
219
+ body = _drop_none(
220
+ {"parameters": parameters, "tier": tier, "dataset_webhook_url": dataset_webhook_url}
221
+ )
222
+ return Run.from_dict(
223
+ self._client.request("POST", f"/configured_datasets/{configured_dataset_id}/build", json=body)
224
+ )
225
+
226
+
227
+ class Extractions:
228
+ """Reusable site scrape/extract recipes and their runs."""
229
+
230
+ def __init__(self, client: "Riveter"):
231
+ self._client = client
232
+
233
+ def create(
234
+ self,
235
+ *,
236
+ starting_url: str,
237
+ goal_description: str,
238
+ output_record_json_schema: Dict[str, Any],
239
+ name: Optional[str] = None,
240
+ required_keys: Optional[List[str]] = None,
241
+ ) -> Extraction:
242
+ """Create an extraction and start its agent discovery. (operationId: createExtraction)"""
243
+ body = _drop_none(
244
+ {
245
+ "starting_url": starting_url,
246
+ "goal_description": goal_description,
247
+ "output_record_json_schema": output_record_json_schema,
248
+ "name": name,
249
+ "required_keys": required_keys,
250
+ }
251
+ )
252
+ return Extraction.from_dict(self._client.request("POST", "/extractions", json=body))
253
+
254
+ def get(self, extraction_id: str) -> Extraction:
255
+ """The extraction's status and definition. (operationId: getExtraction)"""
256
+ return Extraction.from_dict(self._client.request("GET", f"/extractions/{extraction_id}"))
257
+
258
+ def run(
259
+ self,
260
+ extraction_id: str,
261
+ *,
262
+ variables: Optional[Dict[str, str]] = None,
263
+ webhook_url: Optional[str] = None,
264
+ ) -> Run:
265
+ """Execute a ready extraction. (operationId: runExtraction)"""
266
+ body = _drop_none({"variables": variables, "webhook_url": webhook_url})
267
+ return Run.from_dict(
268
+ self._client.request("POST", f"/extractions/{extraction_id}/runs", json=body)
269
+ )
270
+
271
+
272
+ class Monitors:
273
+ """Scheduled enrichment runs with webhooks."""
274
+
275
+ def __init__(self, client: "Riveter"):
276
+ self._client = client
277
+
278
+ def create(
279
+ self,
280
+ *,
281
+ enrichment_id: str,
282
+ cadence: str,
283
+ minute: int,
284
+ hour: int,
285
+ timezone: str,
286
+ day_of_week: Optional[int] = None,
287
+ day_of_month: Optional[int] = None,
288
+ webhook_url: Optional[str] = None,
289
+ alert_rule: Optional[str] = None,
290
+ output_format: Optional[str] = None,
291
+ run_immediately: Optional[bool] = None,
292
+ input: Optional[Dict[str, List[str]]] = None,
293
+ ) -> Monitor:
294
+ """Create a monitor that re-runs an enrichment on a schedule. (operationId: createMonitor)"""
295
+ body = _drop_none(
296
+ {
297
+ "enrichment_id": enrichment_id,
298
+ "cadence": cadence,
299
+ "minute": minute,
300
+ "hour": hour,
301
+ "timezone": timezone,
302
+ "day_of_week": day_of_week,
303
+ "day_of_month": day_of_month,
304
+ "webhook_url": webhook_url,
305
+ "alert_rule": alert_rule,
306
+ "output_format": output_format,
307
+ "run_immediately": run_immediately,
308
+ "input": input,
309
+ }
310
+ )
311
+ return Monitor.from_dict(self._client.request("POST", "/monitors", json=body))
312
+
313
+ def list(self) -> List[Monitor]:
314
+ """List the account's monitors, newest first. (operationId: listMonitors)"""
315
+ response = self._client.request("GET", "/monitors")
316
+ return [Monitor.from_dict(item) for item in response.get("monitors") or []]
317
+
318
+ def get(self, monitor_id: str) -> Monitor:
319
+ """The monitor's schedule, webhook, and next run time. (operationId: getMonitor)"""
320
+ return Monitor.from_dict(self._client.request("GET", f"/monitors/{monitor_id}"))
321
+
322
+ def update(
323
+ self,
324
+ monitor_id: str,
325
+ *,
326
+ enabled: Optional[bool] = None,
327
+ webhook_url: Optional[str] = None,
328
+ ) -> Monitor:
329
+ """Pause, resume, or repoint a monitor. (operationId: updateMonitor)"""
330
+ body = _drop_none({"enabled": enabled, "webhook_url": webhook_url})
331
+ return Monitor.from_dict(self._client.request("PATCH", f"/monitors/{monitor_id}", json=body))
332
+
333
+ def runs(
334
+ self,
335
+ monitor_id: str,
336
+ *,
337
+ status: Optional[str] = None,
338
+ page: Optional[int] = None,
339
+ per_page: Optional[int] = None,
340
+ ) -> RunsPage:
341
+ """The monitor's run history, newest first. (operationId: listMonitorRuns)"""
342
+ params = _drop_none({"status": status, "per_page": per_page})
343
+
344
+ def fetch_page(page_number: int) -> Dict[str, Any]:
345
+ return self._client.request(
346
+ "GET", f"/monitors/{monitor_id}/runs", params={**params, "page": page_number}
347
+ )
348
+
349
+ return RunsPage(fetch_page(page or 1), fetch_page)
riveter/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.5
2
+ Name: riveter-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Riveter API — enrich data, build datasets, scrape pages, and run web searches
5
+ Project-URL: Homepage, https://docs.riveterhq.com
6
+ Project-URL: Documentation, https://docs.riveterhq.com
7
+ Author-email: Riveter <support@riveterhq.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,dataset,enrichment,riveter,scraping,sdk,web-search
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: httpx<1,>=0.24
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # riveter-sdk
18
+
19
+ Official Python SDK for the [Riveter API](https://docs.riveterhq.com) — enrich data,
20
+ build datasets, scrape pages, and run web searches.
21
+
22
+ Requires Python 3.9+. The import name is `riveter`.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install riveter-sdk
28
+ ```
29
+
30
+ ## Quickstart
31
+
32
+ ```python
33
+ from riveter import Riveter
34
+
35
+ # Reads RIVETER_API_KEY from the environment when api_key is not passed.
36
+ # Get a key at https://app.riveterhq.com/settings/api
37
+ riveter = Riveter(api_key="YOUR_API_KEY")
38
+
39
+ run = riveter.enrich(
40
+ prompt="Research each company",
41
+ attributes=["CEO", "Employee Count"],
42
+ input={"Company": ["Apple", "Google"]},
43
+ )
44
+
45
+ result = riveter.runs.wait_for_result(run.id)
46
+ print(result.output)
47
+ ```
48
+
49
+ ## The run lifecycle
50
+
51
+ Every async kickoff (`enrich`, `datasets.build`, `extractions.run`, ...) returns a run.
52
+
53
+ ```python
54
+ riveter.runs.get(run.id) # status + progress
55
+ riveter.runs.result(run.id, wait=50) # output (long-polls up to 50s)
56
+ riveter.runs.wait_for_result(run.id, timeout=600) # poll until finished
57
+ riveter.runs.stop(run.id) # stop early
58
+ ```
59
+
60
+ List runs with automatic pagination:
61
+
62
+ ```python
63
+ for run in riveter.runs.list(status="success").auto_paging_iter():
64
+ print(run.id)
65
+ ```
66
+
67
+ ## Surface
68
+
69
+ - `riveter.enrich(...)`, `riveter.quick_search(...)`, `riveter.scrape(...)`, `riveter.account()`
70
+ - `riveter.runs` — `get`, `result`, `stop`, `list`, `summary`, `wait_for_result`
71
+ - `riveter.enrichments` — `list`, `create`, `get`, `update`, `build_dataset`
72
+ - `riveter.datasets` — `build`, `extend`
73
+ - `riveter.configured_datasets` — `build`
74
+ - `riveter.extractions` — `create`, `get`, `run`
75
+ - `riveter.monitors` — `create`, `list`, `get`, `update`, `runs`
76
+
77
+ Responses are lightweight dataclasses; fields the SDK does not know yet stay reachable
78
+ via `.raw`.
79
+
80
+ ## Errors and retries
81
+
82
+ API failures raise `riveter.APIError` with `status`, `type` (`not_found`,
83
+ `insufficient_credits`, ...), `message`, and optional `details`. Network failures raise
84
+ `APIConnectionError` / `APITimeoutError`. 429s are retried automatically using the
85
+ `X-RateLimit-Reset` header; 5xx and network failures are retried for GETs. Configure with
86
+ `max_retries` and `timeout`.
87
+
88
+ ## Options
89
+
90
+ ```python
91
+ Riveter(
92
+ api_key="...", # default: env RIVETER_API_KEY
93
+ base_url="https://api.riveterhq.com/v1", # default: env RIVETER_BASE_URL, then this
94
+ timeout=60.0, # keep above 50 for `wait` long-polls
95
+ max_retries=2,
96
+ )
97
+ ```
@@ -0,0 +1,11 @@
1
+ riveter/__init__.py,sha256=f-5XD35dUyigGg_j3Et-s5chUg2fkdgxGB18D2AxRVM,932
2
+ riveter/_client.py,sha256=IiLkvikos9fRoKB3AMfQk9RVlivhOs12kW0N2CLkxcg,7339
3
+ riveter/_errors.py,sha256=70LmavK_KQPWNN676GoMwmWMSGZF9TFFRDj31omWIXM,2115
4
+ riveter/_models.py,sha256=pcgKh3bNCifdVSFUJeW1g59vpLSaoz795JDtscPSs6M,9005
5
+ riveter/_pagination.py,sha256=nLpIparrYeIHpUZHS5i0S3lfrBTQ8BvqotENsixItEg,1314
6
+ riveter/_resources.py,sha256=Xe0h9VxZDxGfME7bonVw5z-P7iX0S7RXCV-Yi44HlZs,13260
7
+ riveter/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
8
+ riveter_sdk-0.1.0.dist-info/METADATA,sha256=7_TFxJmWEeA7LzQlkAV1ur77vpQhEXUOPT0ppVDC2AM,3111
9
+ riveter_sdk-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
10
+ riveter_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=l5XLfEkWWQz-81JmUa3lW5rsVOqT8RD1Rc36yCC9oQU,1061
11
+ riveter_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cody
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.