riveter-sdk 0.1.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.
@@ -0,0 +1,180 @@
1
+ # Ruby/Rails
2
+ *.gem
3
+ *.rbc
4
+ /.config
5
+ /coverage/
6
+ /InstalledFiles
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /test/tmp/
11
+ /test/version_tmp/
12
+ /tmp/
13
+ /.bundle/
14
+ /vendor/bundle
15
+ /lib/bundler/man/
16
+ .ruby-version
17
+ .ruby-gemset
18
+ .rvmrc
19
+ .rbenv-version
20
+
21
+ # Rails specific
22
+ *.sqlite3
23
+ *.sqlite3-journal
24
+ *.sqlite3-shm
25
+ *.sqlite3-wal
26
+ /db/*.sqlite3
27
+ /db/*.sqlite3-journal
28
+ /db/*.sqlite3-shm
29
+ /db/*.sqlite3-wal
30
+ /log/*
31
+ !/log/.keep
32
+ /storage/*
33
+ !/storage/.keep
34
+ /tmp/*
35
+ !/tmp/.keep
36
+ /public/assets
37
+ /public/packs
38
+ /public/packs-test
39
+ /node_modules
40
+ /yarn-error.log
41
+ yarn-debug.log*
42
+ .yarn-integrity
43
+ .byebug_history
44
+ config/master.key
45
+ config/credentials/*.key
46
+
47
+ # Node.js/React
48
+ node_modules/
49
+ npm-debug.log*
50
+ yarn-debug.log*
51
+ yarn-error.log*
52
+ .pnpm-debug.log*
53
+ lerna-debug.log*
54
+ dist/
55
+ build/
56
+ # code-executor Lambda packaging artifacts (built in CI, see deploy-code-executor-aws.yml)
57
+ service-code-executor-js/lambda-pkg/
58
+ service-code-executor-js/function.zip
59
+ .env.*
60
+ .env.local
61
+ .env.development.local
62
+ .env.development
63
+ .env.test.local
64
+ .env.production.local
65
+ .cache/
66
+ .vite/
67
+ *.tsbuildinfo
68
+
69
+ # Vite
70
+ public/vite-dev/
71
+ public/vite-dev/*
72
+ public/vite-dev*
73
+ public/vite/
74
+ public/vite/*
75
+ public/vite*
76
+ public/vite-dev/assets/
77
+ vite.config.mts*
78
+
79
+ # Package managers
80
+ package-lock.json
81
+ yarn.lock
82
+ pnpm-lock.yaml
83
+
84
+ # IDE/Editor files
85
+ .vscode/
86
+ .idea/
87
+ *.swp
88
+ *.swo
89
+ *~
90
+ .DS_Store
91
+ .DS_Store?
92
+ ._*
93
+ .Spotlight-V100
94
+ .Trashes
95
+ ehthumbs.db
96
+ Thumbs.db
97
+
98
+ # Environment variables
99
+ .env
100
+ .env.example
101
+ .envrc
102
+ # MCP registry publish tokens (mcp-server)
103
+ .mcpregistry_*
104
+
105
+ # Logs
106
+ *.log
107
+ logs/
108
+
109
+ # Coverage
110
+ coverage/
111
+ .nyc_output
112
+ .coverage/
113
+
114
+ # Dependency directories
115
+ jspm_packages/
116
+
117
+ # Optional npm cache directory
118
+ .npm
119
+
120
+ # Optional eslint cache
121
+ .eslintcache
122
+
123
+ # Microbundle cache
124
+ .rpt2_cache/
125
+ .rts2_cache_cjs/
126
+ .rts2_cache_es/
127
+ .rts2_cache_umd/
128
+
129
+ # Optional REPL history
130
+ .node_repl_history
131
+
132
+ # Output of 'npm pack'
133
+ *.tgz
134
+
135
+ # Yarn Integrity file
136
+ .yarn-integrity
137
+
138
+ # dotenv environment variables file
139
+ .env.test
140
+
141
+ # parcel-bundler cache (https://parceljs.org/)
142
+ .parcel-cache
143
+
144
+ # Next.js build output
145
+ .next
146
+
147
+ # Nuxt.js build / generate output
148
+ .nuxt
149
+ dist
150
+
151
+ # Gatsby files
152
+ .cache/
153
+
154
+ # Storybook build outputs
155
+ .out
156
+ .storybook-out
157
+
158
+ # Temporary folders
159
+ tmp/
160
+ # Local agent/human scratch dumps (payload dumps, ad-hoc JSON, etc.)
161
+ tmp/scratch/
162
+ payload.json
163
+ temp/
164
+ script/dummy_data/
165
+
166
+ # SDKs (sdk/) — per-language build/test artifacts
167
+ sdk/python/.venv/
168
+ sdk/python/*.egg-info/
169
+ sdk/python/.pytest_cache/
170
+ __pycache__/
171
+ sdk/ruby/.bundle/
172
+ sdk/ruby/Gemfile.lock
173
+
174
+ # Vite Ruby
175
+ /public/vite*
176
+ node_modules
177
+ # Vite uses dotenv and suggests to ignore local-only env files. See
178
+ # https://vitejs.dev/guide/env-and-mode.html#env-files
179
+ *.local
180
+
@@ -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.
@@ -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,81 @@
1
+ # riveter-sdk
2
+
3
+ Official Python SDK for the [Riveter API](https://docs.riveterhq.com) — enrich data,
4
+ build datasets, scrape pages, and run web searches.
5
+
6
+ Requires Python 3.9+. The import name is `riveter`.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install riveter-sdk
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ from riveter import Riveter
18
+
19
+ # Reads RIVETER_API_KEY from the environment when api_key is not passed.
20
+ # Get a key at https://app.riveterhq.com/settings/api
21
+ riveter = Riveter(api_key="YOUR_API_KEY")
22
+
23
+ run = riveter.enrich(
24
+ prompt="Research each company",
25
+ attributes=["CEO", "Employee Count"],
26
+ input={"Company": ["Apple", "Google"]},
27
+ )
28
+
29
+ result = riveter.runs.wait_for_result(run.id)
30
+ print(result.output)
31
+ ```
32
+
33
+ ## The run lifecycle
34
+
35
+ Every async kickoff (`enrich`, `datasets.build`, `extractions.run`, ...) returns a run.
36
+
37
+ ```python
38
+ riveter.runs.get(run.id) # status + progress
39
+ riveter.runs.result(run.id, wait=50) # output (long-polls up to 50s)
40
+ riveter.runs.wait_for_result(run.id, timeout=600) # poll until finished
41
+ riveter.runs.stop(run.id) # stop early
42
+ ```
43
+
44
+ List runs with automatic pagination:
45
+
46
+ ```python
47
+ for run in riveter.runs.list(status="success").auto_paging_iter():
48
+ print(run.id)
49
+ ```
50
+
51
+ ## Surface
52
+
53
+ - `riveter.enrich(...)`, `riveter.quick_search(...)`, `riveter.scrape(...)`, `riveter.account()`
54
+ - `riveter.runs` — `get`, `result`, `stop`, `list`, `summary`, `wait_for_result`
55
+ - `riveter.enrichments` — `list`, `create`, `get`, `update`, `build_dataset`
56
+ - `riveter.datasets` — `build`, `extend`
57
+ - `riveter.configured_datasets` — `build`
58
+ - `riveter.extractions` — `create`, `get`, `run`
59
+ - `riveter.monitors` — `create`, `list`, `get`, `update`, `runs`
60
+
61
+ Responses are lightweight dataclasses; fields the SDK does not know yet stay reachable
62
+ via `.raw`.
63
+
64
+ ## Errors and retries
65
+
66
+ API failures raise `riveter.APIError` with `status`, `type` (`not_found`,
67
+ `insufficient_credits`, ...), `message`, and optional `details`. Network failures raise
68
+ `APIConnectionError` / `APITimeoutError`. 429s are retried automatically using the
69
+ `X-RateLimit-Reset` header; 5xx and network failures are retried for GETs. Configure with
70
+ `max_retries` and `timeout`.
71
+
72
+ ## Options
73
+
74
+ ```python
75
+ Riveter(
76
+ api_key="...", # default: env RIVETER_API_KEY
77
+ base_url="https://api.riveterhq.com/v1", # default: env RIVETER_BASE_URL, then this
78
+ timeout=60.0, # keep above 50 for `wait` long-polls
79
+ max_retries=2,
80
+ )
81
+ ```
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "riveter-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Riveter API — enrich data, build datasets, scrape pages, and run web searches"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Riveter", email = "support@riveterhq.com" }]
14
+ dependencies = ["httpx>=0.24,<1"]
15
+ keywords = ["riveter", "sdk", "api", "enrichment", "dataset", "scraping", "web-search"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://docs.riveterhq.com"
19
+ Documentation = "https://docs.riveterhq.com"
20
+
21
+ [project.optional-dependencies]
22
+ dev = ["pytest>=7"]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["riveter"]
26
+
27
+ [tool.hatch.build.targets.sdist]
28
+ include = ["riveter", "README.md", "LICENSE"]
@@ -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
+ ]
@@ -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