leadsets 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.
- leadsets/__init__.py +58 -0
- leadsets/_base.py +171 -0
- leadsets/_client.py +239 -0
- leadsets/_exceptions.py +264 -0
- leadsets/_models.py +104 -0
- leadsets/_pagination.py +86 -0
- leadsets/_version.py +1 -0
- leadsets/py.typed +0 -0
- leadsets/resources/__init__.py +0 -0
- leadsets/resources/_compat.py +8 -0
- leadsets/resources/companies.py +219 -0
- leadsets/resources/emails.py +83 -0
- leadsets/resources/jobs.py +54 -0
- leadsets/resources/people.py +219 -0
- leadsets/resources/scrape.py +111 -0
- leadsets/resources/usage.py +45 -0
- leadsets/types.py +457 -0
- leadsets-0.1.0.dist-info/METADATA +419 -0
- leadsets-0.1.0.dist-info/RECORD +21 -0
- leadsets-0.1.0.dist-info/WHEEL +4 -0
- leadsets-0.1.0.dist-info/licenses/LICENSE +21 -0
leadsets/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Official Python SDK for the Leadsets B2B data API.
|
|
2
|
+
|
|
3
|
+
>>> import os
|
|
4
|
+
>>> from leadsets import Leadsets
|
|
5
|
+
>>> ls = Leadsets(os.environ["LEADSETS_KEY"])
|
|
6
|
+
>>> res = ls.people.search(function=["revenue"], seniority=["vp", "head"], employees="20-200",
|
|
7
|
+
... country="US", industry="saas", limit=10)
|
|
8
|
+
>>> res.items[0].match_reasons, res.usage
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import types
|
|
12
|
+
from ._base import DEFAULT_BASE_URL
|
|
13
|
+
from ._client import AsyncLeadsets, Leadsets
|
|
14
|
+
from ._exceptions import (
|
|
15
|
+
APIConnectionError,
|
|
16
|
+
APIError,
|
|
17
|
+
APITimeoutError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
FreeRecordsExhaustedError,
|
|
20
|
+
IdempotencyConflictError,
|
|
21
|
+
InternalServerError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
LeadsetsError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
PaymentRequiredError,
|
|
26
|
+
PermissionDeniedError,
|
|
27
|
+
RateLimitError,
|
|
28
|
+
ScrapeUnitsExhaustedError,
|
|
29
|
+
ServiceUnavailableError,
|
|
30
|
+
)
|
|
31
|
+
from ._exceptions import APIConnectionError as ConnectionError
|
|
32
|
+
from ._exceptions import APITimeoutError as TimeoutError
|
|
33
|
+
from ._version import __version__
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"DEFAULT_BASE_URL",
|
|
37
|
+
"APIConnectionError",
|
|
38
|
+
"APIError",
|
|
39
|
+
"APITimeoutError",
|
|
40
|
+
"AsyncLeadsets",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"ConnectionError",
|
|
43
|
+
"FreeRecordsExhaustedError",
|
|
44
|
+
"IdempotencyConflictError",
|
|
45
|
+
"InternalServerError",
|
|
46
|
+
"InvalidRequestError",
|
|
47
|
+
"Leadsets",
|
|
48
|
+
"LeadsetsError",
|
|
49
|
+
"NotFoundError",
|
|
50
|
+
"PaymentRequiredError",
|
|
51
|
+
"PermissionDeniedError",
|
|
52
|
+
"RateLimitError",
|
|
53
|
+
"ScrapeUnitsExhaustedError",
|
|
54
|
+
"ServiceUnavailableError",
|
|
55
|
+
"TimeoutError",
|
|
56
|
+
"__version__",
|
|
57
|
+
"types",
|
|
58
|
+
]
|
leadsets/_base.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Shared request logic for the sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import random
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any, TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ._exceptions import APIError, LeadsetsError, make_api_error, parse_retry_after
|
|
15
|
+
from ._models import Model
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .types import RateLimitInfo, ResponseMeta
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.leadsets.ai"
|
|
20
|
+
DEFAULT_TIMEOUT = 120.0
|
|
21
|
+
DEFAULT_MAX_RETRIES = 2
|
|
22
|
+
DEFAULT_MAX_RETRY_DELAY = 60.0
|
|
23
|
+
|
|
24
|
+
USER_AGENT = f"leadsets-sdk-python/{__version__} (Python {platform.python_version()}; httpx {httpx.__version__})"
|
|
25
|
+
|
|
26
|
+
M = TypeVar("M", bound=Model)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _env(name: str) -> str | None:
|
|
30
|
+
v = os.environ.get(name)
|
|
31
|
+
return v.strip() if v and v.strip() else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_api_key(api_key: str | None) -> str:
|
|
35
|
+
key = api_key or _env("LEADSETS_API_KEY") or _env("LEADSETS_KEY")
|
|
36
|
+
if not key:
|
|
37
|
+
raise LeadsetsError(
|
|
38
|
+
"No API key. Pass one to Leadsets(api_key) or set the LEADSETS_API_KEY environment variable. "
|
|
39
|
+
"Get a free key at https://www.leadsets.ai/signup."
|
|
40
|
+
)
|
|
41
|
+
return key
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_base_url(base_url: str | None) -> str:
|
|
45
|
+
return (base_url or _env("LEADSETS_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def new_idempotency_key() -> str:
|
|
49
|
+
return str(uuid.uuid4())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def backoff(attempt: int) -> float:
|
|
53
|
+
"""Exponential backoff with jitter: ~0.5 s, 1 s, 2 s … capped at 8 s."""
|
|
54
|
+
base: float = min(8.0, 0.5 * 2**attempt)
|
|
55
|
+
return base * (0.75 + random.random() * 0.25)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clean(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
59
|
+
"""Drop unset (None) parameters; turn tuples and other sequences into lists."""
|
|
60
|
+
out: dict[str, Any] = {}
|
|
61
|
+
for k, v in params.items():
|
|
62
|
+
if v is None:
|
|
63
|
+
continue
|
|
64
|
+
if isinstance(v, (tuple, set, frozenset)):
|
|
65
|
+
v = list(v)
|
|
66
|
+
out[k] = v
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BaseClient:
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
api_key: str | None,
|
|
74
|
+
*,
|
|
75
|
+
base_url: str | None,
|
|
76
|
+
max_retries: int,
|
|
77
|
+
timeout: float | httpx.Timeout | None,
|
|
78
|
+
max_retry_delay: float,
|
|
79
|
+
default_headers: Mapping[str, str] | None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self.api_key = resolve_api_key(api_key)
|
|
82
|
+
self.base_url = resolve_base_url(base_url)
|
|
83
|
+
self.max_retries = max(0, int(max_retries))
|
|
84
|
+
self.timeout = timeout
|
|
85
|
+
self.max_retry_delay = max(0.0, float(max_retry_delay))
|
|
86
|
+
self._default_headers = dict(default_headers or {})
|
|
87
|
+
|
|
88
|
+
def _headers(
|
|
89
|
+
self, *, has_body: bool, idempotency_key: str | None, extra: Mapping[str, str] | None
|
|
90
|
+
) -> dict[str, str]:
|
|
91
|
+
h = {
|
|
92
|
+
"Accept": "application/json",
|
|
93
|
+
"User-Agent": USER_AGENT,
|
|
94
|
+
**self._default_headers,
|
|
95
|
+
**(extra or {}),
|
|
96
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
97
|
+
}
|
|
98
|
+
if has_body:
|
|
99
|
+
h["Content-Type"] = "application/json"
|
|
100
|
+
if idempotency_key:
|
|
101
|
+
h["Idempotency-Key"] = idempotency_key
|
|
102
|
+
return h
|
|
103
|
+
|
|
104
|
+
def _url(self, path: str) -> str:
|
|
105
|
+
return f"{self.base_url}/v1{path}"
|
|
106
|
+
|
|
107
|
+
def _retry_delay(self, err: APIError | None, attempt: int) -> float | None:
|
|
108
|
+
"""Seconds to wait before retrying, or None to not retry. ``err`` is None for network errors."""
|
|
109
|
+
if err is None:
|
|
110
|
+
return backoff(attempt)
|
|
111
|
+
if not (err.status_code in (408, 429) or err.status_code >= 500):
|
|
112
|
+
return None
|
|
113
|
+
if err.retry_after is not None:
|
|
114
|
+
if err.retry_after > self.max_retry_delay:
|
|
115
|
+
return None
|
|
116
|
+
return err.retry_after + random.random() * 0.25
|
|
117
|
+
return backoff(attempt)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _parse(cls: type[M], response: httpx.Response, idempotency_key: str | None, attempts: int) -> M:
|
|
121
|
+
try:
|
|
122
|
+
data = response.json() if response.content else {}
|
|
123
|
+
except ValueError:
|
|
124
|
+
data = {}
|
|
125
|
+
if not isinstance(data, dict):
|
|
126
|
+
data = {}
|
|
127
|
+
obj = cls.from_dict(data)
|
|
128
|
+
if hasattr(obj, "last_response"):
|
|
129
|
+
object.__setattr__(obj, "last_response", response_meta(response, data, idempotency_key, attempts))
|
|
130
|
+
return obj
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _error(response: httpx.Response) -> APIError:
|
|
134
|
+
return make_api_error(response)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _int(v: str | None) -> int | None:
|
|
138
|
+
if v is None or v == "":
|
|
139
|
+
return None
|
|
140
|
+
try:
|
|
141
|
+
return int(v)
|
|
142
|
+
except ValueError:
|
|
143
|
+
try:
|
|
144
|
+
return int(float(v))
|
|
145
|
+
except ValueError:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def rate_limit_info(h: httpx.Headers) -> RateLimitInfo:
|
|
150
|
+
return RateLimitInfo(
|
|
151
|
+
records_remaining=_int(h.get("x-leadsets-records-remaining")),
|
|
152
|
+
requests_remaining=_int(h.get("x-leadsets-requests-remaining")),
|
|
153
|
+
weekly_remaining=_int(h.get("x-leadsets-weekly-remaining")),
|
|
154
|
+
free_remaining=_int(h.get("x-leadsets-free-remaining")),
|
|
155
|
+
retry_after=parse_retry_after(h.get("retry-after")),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def response_meta(
|
|
160
|
+
response: httpx.Response, data: dict[str, Any], idempotency_key: str | None, attempts: int
|
|
161
|
+
) -> ResponseMeta:
|
|
162
|
+
rid = data.get("request_id")
|
|
163
|
+
return ResponseMeta(
|
|
164
|
+
status_code=response.status_code,
|
|
165
|
+
headers=response.headers,
|
|
166
|
+
request_id=rid if isinstance(rid, str) else None,
|
|
167
|
+
idempotency_key=idempotency_key,
|
|
168
|
+
idempotent_replayed=response.headers.get("idempotent-replayed") == "true",
|
|
169
|
+
rate_limit=rate_limit_info(response.headers),
|
|
170
|
+
attempts=attempts,
|
|
171
|
+
)
|
leadsets/_client.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._base import (
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_MAX_RETRY_DELAY,
|
|
14
|
+
DEFAULT_TIMEOUT,
|
|
15
|
+
BaseClient,
|
|
16
|
+
M,
|
|
17
|
+
new_idempotency_key,
|
|
18
|
+
)
|
|
19
|
+
from ._exceptions import APIConnectionError, APIError, APITimeoutError
|
|
20
|
+
from .resources.companies import AsyncCompanies, Companies
|
|
21
|
+
from .resources.emails import AsyncEmails, Emails
|
|
22
|
+
from .resources.jobs import AsyncJobs, Jobs
|
|
23
|
+
from .resources.people import AsyncPeople, People
|
|
24
|
+
from .resources.scrape import AsyncScrape, Scrape
|
|
25
|
+
from .resources.usage import AsyncUsageResource, UsageResource
|
|
26
|
+
|
|
27
|
+
Method = Literal["GET", "POST"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sleep(seconds: float) -> None:
|
|
31
|
+
time.sleep(seconds)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _async_sleep(seconds: float) -> None:
|
|
35
|
+
await asyncio.sleep(seconds)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Leadsets(BaseClient):
|
|
39
|
+
"""The Leadsets API client.
|
|
40
|
+
|
|
41
|
+
>>> ls = Leadsets(os.environ["LEADSETS_KEY"])
|
|
42
|
+
>>> res = ls.people.search(function=["revenue"], country="US", limit=10)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
people: People
|
|
46
|
+
companies: Companies
|
|
47
|
+
emails: Emails
|
|
48
|
+
scrape: Scrape
|
|
49
|
+
jobs: Jobs
|
|
50
|
+
usage: UsageResource
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
*,
|
|
56
|
+
base_url: str | None = None,
|
|
57
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
58
|
+
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
|
|
59
|
+
max_retry_delay: float = DEFAULT_MAX_RETRY_DELAY,
|
|
60
|
+
default_headers: Mapping[str, str] | None = None,
|
|
61
|
+
http_client: httpx.Client | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Args:
|
|
65
|
+
api_key: Your API key (``ls_live_…``). Defaults to ``LEADSETS_API_KEY`` or ``LEADSETS_KEY``.
|
|
66
|
+
base_url: Defaults to https://api.leadsets.ai (or ``LEADSETS_BASE_URL``).
|
|
67
|
+
max_retries: Retries on 429, 5xx, timeouts and network errors. Default 2.
|
|
68
|
+
timeout: Per-attempt timeout in seconds. Default 120.
|
|
69
|
+
max_retry_delay: The longest ``Retry-After`` (seconds) to wait for before retrying.
|
|
70
|
+
A 429 asking for longer (a weekly limit, say) is raised at once. Default 60.
|
|
71
|
+
default_headers: Headers sent with every request.
|
|
72
|
+
http_client: Your own ``httpx.Client`` (proxies, transports…).
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(
|
|
75
|
+
api_key,
|
|
76
|
+
base_url=base_url,
|
|
77
|
+
max_retries=max_retries,
|
|
78
|
+
timeout=timeout,
|
|
79
|
+
max_retry_delay=max_retry_delay,
|
|
80
|
+
default_headers=default_headers,
|
|
81
|
+
)
|
|
82
|
+
self._owns_client = http_client is None
|
|
83
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
84
|
+
self.people = People(self)
|
|
85
|
+
self.companies = Companies(self)
|
|
86
|
+
self.emails = Emails(self)
|
|
87
|
+
self.scrape = Scrape(self)
|
|
88
|
+
self.jobs = Jobs(self)
|
|
89
|
+
self.usage = UsageResource(self)
|
|
90
|
+
|
|
91
|
+
def request(
|
|
92
|
+
self,
|
|
93
|
+
method: Method,
|
|
94
|
+
path: str,
|
|
95
|
+
cls: type[M],
|
|
96
|
+
*,
|
|
97
|
+
body: Mapping[str, Any] | None = None,
|
|
98
|
+
idempotent: bool = False,
|
|
99
|
+
idempotency_key: str | None = None,
|
|
100
|
+
timeout: float | httpx.Timeout | None = None,
|
|
101
|
+
max_retries: int | None = None,
|
|
102
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
103
|
+
) -> M:
|
|
104
|
+
key = idempotency_key or (new_idempotency_key() if idempotent else None)
|
|
105
|
+
headers = self._headers(has_body=body is not None, idempotency_key=key, extra=extra_headers)
|
|
106
|
+
retries = self.max_retries if max_retries is None else max(0, max_retries)
|
|
107
|
+
t = self.timeout if timeout is None else timeout
|
|
108
|
+
for attempt in range(retries + 1):
|
|
109
|
+
try:
|
|
110
|
+
res = self._client.request(method, self._url(path), json=body, headers=headers, timeout=t)
|
|
111
|
+
except httpx.TimeoutException as e:
|
|
112
|
+
if attempt < retries:
|
|
113
|
+
_sleep(self._retry_delay(None, attempt) or 0)
|
|
114
|
+
continue
|
|
115
|
+
raise APITimeoutError() from e
|
|
116
|
+
except httpx.TransportError as e:
|
|
117
|
+
if attempt < retries:
|
|
118
|
+
_sleep(self._retry_delay(None, attempt) or 0)
|
|
119
|
+
continue
|
|
120
|
+
raise APIConnectionError() from e
|
|
121
|
+
if res.is_success:
|
|
122
|
+
return self._parse(cls, res, key, attempt + 1)
|
|
123
|
+
err: APIError = self._error(res)
|
|
124
|
+
delay = self._retry_delay(err, attempt)
|
|
125
|
+
if attempt < retries and delay is not None:
|
|
126
|
+
_sleep(delay)
|
|
127
|
+
continue
|
|
128
|
+
raise err
|
|
129
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
"""Close the underlying HTTP client (if the SDK created it)."""
|
|
133
|
+
if self._owns_client:
|
|
134
|
+
self._client.close()
|
|
135
|
+
|
|
136
|
+
def __enter__(self) -> Leadsets:
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(
|
|
140
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
|
|
141
|
+
) -> None:
|
|
142
|
+
self.close()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AsyncLeadsets(BaseClient):
|
|
146
|
+
"""The async Leadsets API client.
|
|
147
|
+
|
|
148
|
+
>>> async with AsyncLeadsets() as ls:
|
|
149
|
+
... res = await ls.people.search(function=["revenue"], country="US", limit=10)
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
people: AsyncPeople
|
|
153
|
+
companies: AsyncCompanies
|
|
154
|
+
emails: AsyncEmails
|
|
155
|
+
scrape: AsyncScrape
|
|
156
|
+
jobs: AsyncJobs
|
|
157
|
+
usage: AsyncUsageResource
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
api_key: str | None = None,
|
|
162
|
+
*,
|
|
163
|
+
base_url: str | None = None,
|
|
164
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
165
|
+
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
|
|
166
|
+
max_retry_delay: float = DEFAULT_MAX_RETRY_DELAY,
|
|
167
|
+
default_headers: Mapping[str, str] | None = None,
|
|
168
|
+
http_client: httpx.AsyncClient | None = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Same arguments as :class:`Leadsets`; ``http_client`` is an ``httpx.AsyncClient``."""
|
|
171
|
+
super().__init__(
|
|
172
|
+
api_key,
|
|
173
|
+
base_url=base_url,
|
|
174
|
+
max_retries=max_retries,
|
|
175
|
+
timeout=timeout,
|
|
176
|
+
max_retry_delay=max_retry_delay,
|
|
177
|
+
default_headers=default_headers,
|
|
178
|
+
)
|
|
179
|
+
self._owns_client = http_client is None
|
|
180
|
+
self._client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
181
|
+
self.people = AsyncPeople(self)
|
|
182
|
+
self.companies = AsyncCompanies(self)
|
|
183
|
+
self.emails = AsyncEmails(self)
|
|
184
|
+
self.scrape = AsyncScrape(self)
|
|
185
|
+
self.jobs = AsyncJobs(self)
|
|
186
|
+
self.usage = AsyncUsageResource(self)
|
|
187
|
+
|
|
188
|
+
async def request(
|
|
189
|
+
self,
|
|
190
|
+
method: Method,
|
|
191
|
+
path: str,
|
|
192
|
+
cls: type[M],
|
|
193
|
+
*,
|
|
194
|
+
body: Mapping[str, Any] | None = None,
|
|
195
|
+
idempotent: bool = False,
|
|
196
|
+
idempotency_key: str | None = None,
|
|
197
|
+
timeout: float | httpx.Timeout | None = None,
|
|
198
|
+
max_retries: int | None = None,
|
|
199
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
200
|
+
) -> M:
|
|
201
|
+
key = idempotency_key or (new_idempotency_key() if idempotent else None)
|
|
202
|
+
headers = self._headers(has_body=body is not None, idempotency_key=key, extra=extra_headers)
|
|
203
|
+
retries = self.max_retries if max_retries is None else max(0, max_retries)
|
|
204
|
+
t = self.timeout if timeout is None else timeout
|
|
205
|
+
for attempt in range(retries + 1):
|
|
206
|
+
try:
|
|
207
|
+
res = await self._client.request(method, self._url(path), json=body, headers=headers, timeout=t)
|
|
208
|
+
except httpx.TimeoutException as e:
|
|
209
|
+
if attempt < retries:
|
|
210
|
+
await _async_sleep(self._retry_delay(None, attempt) or 0)
|
|
211
|
+
continue
|
|
212
|
+
raise APITimeoutError() from e
|
|
213
|
+
except httpx.TransportError as e:
|
|
214
|
+
if attempt < retries:
|
|
215
|
+
await _async_sleep(self._retry_delay(None, attempt) or 0)
|
|
216
|
+
continue
|
|
217
|
+
raise APIConnectionError() from e
|
|
218
|
+
if res.is_success:
|
|
219
|
+
return self._parse(cls, res, key, attempt + 1)
|
|
220
|
+
err: APIError = self._error(res)
|
|
221
|
+
delay = self._retry_delay(err, attempt)
|
|
222
|
+
if attempt < retries and delay is not None:
|
|
223
|
+
await _async_sleep(delay)
|
|
224
|
+
continue
|
|
225
|
+
raise err
|
|
226
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
227
|
+
|
|
228
|
+
async def aclose(self) -> None:
|
|
229
|
+
"""Close the underlying HTTP client (if the SDK created it)."""
|
|
230
|
+
if self._owns_client:
|
|
231
|
+
await self._client.aclose()
|
|
232
|
+
|
|
233
|
+
async def __aenter__(self) -> AsyncLeadsets:
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
async def __aexit__(
|
|
237
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
|
|
238
|
+
) -> None:
|
|
239
|
+
await self.aclose()
|