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 +48 -0
- riveter/_client.py +205 -0
- riveter/_errors.py +63 -0
- riveter/_models.py +285 -0
- riveter/_pagination.py +39 -0
- riveter/_resources.py +349 -0
- riveter/_version.py +1 -0
- riveter_sdk-0.1.0.dist-info/METADATA +97 -0
- riveter_sdk-0.1.0.dist-info/RECORD +11 -0
- riveter_sdk-0.1.0.dist-info/WHEEL +4 -0
- riveter_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
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,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.
|