blitz-api-py 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.
Files changed (40) hide show
  1. blitz_api/__init__.py +65 -0
  2. blitz_api/_base_client.py +191 -0
  3. blitz_api/_client.py +13 -0
  4. blitz_api/_client_async.py +143 -0
  5. blitz_api/_client_sync.py +145 -0
  6. blitz_api/_compat.py +26 -0
  7. blitz_api/_constants.py +36 -0
  8. blitz_api/_exceptions.py +113 -0
  9. blitz_api/_pagination_async.py +128 -0
  10. blitz_api/_pagination_base.py +52 -0
  11. blitz_api/_pagination_sync.py +130 -0
  12. blitz_api/_rate_limit.py +14 -0
  13. blitz_api/_rate_limit_async.py +67 -0
  14. blitz_api/_rate_limit_sync.py +69 -0
  15. blitz_api/_version.py +1 -0
  16. blitz_api/py.typed +0 -0
  17. blitz_api/resources/__init__.py +27 -0
  18. blitz_api/resources/_async/__init__.py +1 -0
  19. blitz_api/resources/_async/account.py +27 -0
  20. blitz_api/resources/_async/enrichment.py +116 -0
  21. blitz_api/resources/_async/search.py +153 -0
  22. blitz_api/resources/_async/utils.py +43 -0
  23. blitz_api/resources/_sync/__init__.py +3 -0
  24. blitz_api/resources/_sync/account.py +29 -0
  25. blitz_api/resources/_sync/enrichment.py +118 -0
  26. blitz_api/resources/_sync/search.py +155 -0
  27. blitz_api/resources/_sync/utils.py +45 -0
  28. blitz_api/types/__init__.py +108 -0
  29. blitz_api/types/_models.py +23 -0
  30. blitz_api/types/account.py +27 -0
  31. blitz_api/types/enrichment.py +76 -0
  32. blitz_api/types/enums.py +633 -0
  33. blitz_api/types/filters.py +130 -0
  34. blitz_api/types/search.py +36 -0
  35. blitz_api/types/shared.py +119 -0
  36. blitz_api/types/utils.py +35 -0
  37. blitz_api_py-0.1.0.dist-info/METADATA +220 -0
  38. blitz_api_py-0.1.0.dist-info/RECORD +40 -0
  39. blitz_api_py-0.1.0.dist-info/WHEEL +4 -0
  40. blitz_api_py-0.1.0.dist-info/licenses/LICENSE +21 -0
blitz_api/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """Typed Python SDK for the Blitz API.
2
+
3
+ Quickstart::
4
+
5
+ from blitz_api import BlitzAPI
6
+
7
+ client = BlitzAPI() # reads the BLITZ_API_KEY environment variable
8
+ result = client.enrichment.email(
9
+ person_linkedin_url="https://www.linkedin.com/in/example",
10
+ )
11
+ print(result.found, result.email)
12
+
13
+ See https://docs.blitz-api.ai for the full API reference.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from ._client import AsyncBlitzAPI, BlitzAPI
19
+ from ._exceptions import (
20
+ APIConnectionError,
21
+ APIResponseValidationError,
22
+ APIStatusError,
23
+ APITimeoutError,
24
+ AuthenticationError,
25
+ BlitzError,
26
+ InsufficientCreditsError,
27
+ NotFoundError,
28
+ RateLimitError,
29
+ ServerError,
30
+ )
31
+ from ._pagination_async import AsyncCursorPage, AsyncPageNumberPage
32
+ from ._pagination_sync import CursorPage, PageNumberPage
33
+ from ._version import __version__
34
+ from .types import (
35
+ CompanyFilter,
36
+ Industry,
37
+ PeopleFilter,
38
+ )
39
+
40
+ __all__ = [
41
+ "__version__",
42
+ # clients
43
+ "BlitzAPI",
44
+ "AsyncBlitzAPI",
45
+ # pagination (returned by the search.* methods)
46
+ "CursorPage",
47
+ "AsyncCursorPage",
48
+ "PageNumberPage",
49
+ "AsyncPageNumberPage",
50
+ # exceptions
51
+ "BlitzError",
52
+ "APIConnectionError",
53
+ "APITimeoutError",
54
+ "APIResponseValidationError",
55
+ "APIStatusError",
56
+ "AuthenticationError",
57
+ "InsufficientCreditsError",
58
+ "NotFoundError",
59
+ "RateLimitError",
60
+ "ServerError",
61
+ # commonly-used types (full set under blitz_api.types)
62
+ "Industry",
63
+ "CompanyFilter",
64
+ "PeopleFilter",
65
+ ]
@@ -0,0 +1,191 @@
1
+ """Shared, IO-free request-pipeline logic for the sync and async clients.
2
+
3
+ The actual network calls live in :mod:`blitz_api._client`; everything here is
4
+ pure so both clients share identical header building, retry decisions, backoff
5
+ computation, error mapping, and response parsing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import random
12
+ from datetime import datetime, timezone
13
+ from email.utils import parsedate_to_datetime
14
+ from enum import Enum
15
+ from typing import Any, TypeVar, cast
16
+ from urllib.parse import urljoin
17
+
18
+ import httpx
19
+ from pydantic import ValidationError
20
+
21
+ from . import _constants as C
22
+ from ._exceptions import (
23
+ APIResponseValidationError,
24
+ APIStatusError,
25
+ AuthenticationError,
26
+ BlitzError,
27
+ InsufficientCreditsError,
28
+ NotFoundError,
29
+ RateLimitError,
30
+ ServerError,
31
+ )
32
+ from .types._models import BlitzModel
33
+
34
+ ResponseT = TypeVar("ResponseT", bound=BlitzModel)
35
+
36
+ # Status code -> exception class. Anything not listed that is still non-2xx
37
+ # falls back to a generic APIStatusError (or ServerError for any 5xx).
38
+ _STATUS_EXCEPTIONS: dict[int, type[APIStatusError]] = {
39
+ 401: AuthenticationError,
40
+ 402: InsufficientCreditsError,
41
+ 404: NotFoundError,
42
+ 429: RateLimitError,
43
+ }
44
+
45
+
46
+ def to_jsonable(value: Any) -> Any:
47
+ """Recursively prepare a request payload for JSON serialization.
48
+
49
+ Resolves :class:`enum.Enum` members to their values and drops ``None`` so
50
+ that optional, unset fields are simply omitted from the request body.
51
+ """
52
+ if isinstance(value, Enum):
53
+ return value.value
54
+ if isinstance(value, dict):
55
+ mapping = cast("dict[str, Any]", value)
56
+ return {k: to_jsonable(v) for k, v in mapping.items() if v is not None}
57
+ if isinstance(value, (list, tuple)):
58
+ sequence = cast("list[Any]", value)
59
+ return [to_jsonable(v) for v in sequence]
60
+ return value
61
+
62
+
63
+ def _retry_after_seconds(raw: str | None) -> float:
64
+ """Parse a ``Retry-After`` header (delta-seconds or HTTP-date) into seconds.
65
+
66
+ Falls back to :data:`DEFAULT_RETRY_AFTER_SECONDS` when the header is absent or
67
+ unparseable. The caller clamps the result to a maximum.
68
+ """
69
+ if not raw:
70
+ return C.DEFAULT_RETRY_AFTER_SECONDS
71
+ raw = raw.strip()
72
+ try:
73
+ return max(0.0, float(raw))
74
+ except ValueError:
75
+ pass
76
+ try:
77
+ when = parsedate_to_datetime(raw)
78
+ except (TypeError, ValueError):
79
+ return C.DEFAULT_RETRY_AFTER_SECONDS
80
+ if when.tzinfo is None:
81
+ when = when.replace(tzinfo=timezone.utc)
82
+ return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
83
+
84
+
85
+ class BaseClient:
86
+ """Configuration and pure helpers shared by both clients."""
87
+
88
+ def __init__(
89
+ self,
90
+ *,
91
+ api_key: str | None,
92
+ base_url: str,
93
+ max_retries: int,
94
+ ) -> None:
95
+ resolved = api_key if api_key is not None else os.environ.get(C.API_KEY_ENV_VAR)
96
+ if not resolved:
97
+ raise BlitzError(
98
+ "No API key provided. Pass api_key=... or set the "
99
+ f"{C.API_KEY_ENV_VAR} environment variable."
100
+ )
101
+ self.api_key = resolved
102
+ self.base_url = base_url.rstrip("/") + "/"
103
+ self.max_retries = max_retries
104
+
105
+ # -- request shaping -------------------------------------------------
106
+
107
+ def _build_url(self, path: str) -> str:
108
+ return urljoin(self.base_url, path.lstrip("/"))
109
+
110
+ def _build_headers(self) -> dict[str, str]:
111
+ return {
112
+ C.API_KEY_HEADER: self.api_key,
113
+ "Content-Type": "application/json",
114
+ "Accept": "application/json",
115
+ "User-Agent": C.USER_AGENT,
116
+ }
117
+
118
+ # -- retry policy ----------------------------------------------------
119
+
120
+ @staticmethod
121
+ def _should_retry(status_code: int) -> bool:
122
+ return status_code == 429 or status_code >= 500
123
+
124
+ @staticmethod
125
+ def _should_retry_exception(exc: httpx.RequestError) -> bool:
126
+ """Whether a transport-level error is safe to retry.
127
+
128
+ Only failures where the request never reached the server are retried, so a
129
+ retry cannot double-process (or double-bill) a request. A connect timeout,
130
+ connection error, or pool timeout means nothing was sent. A *read* or *write*
131
+ timeout means the request may already have been received and processed by the
132
+ server — retrying a billable POST there could charge the caller twice — so
133
+ those are surfaced immediately instead.
134
+ """
135
+ return isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout))
136
+
137
+ def _backoff_seconds(self, attempt: int) -> float:
138
+ """Exponential backoff with full jitter (attempt starts at 1)."""
139
+ base = min(8.0, 0.5 * 2.0 ** (attempt - 1))
140
+ return base + random.uniform(0.0, 0.5)
141
+
142
+ def _retry_delay(self, response: httpx.Response, attempt: int) -> float:
143
+ if response.status_code == 429:
144
+ seconds = _retry_after_seconds(response.headers.get("retry-after"))
145
+ return min(seconds, C.MAX_RETRY_WAIT_SECONDS)
146
+ return self._backoff_seconds(attempt)
147
+
148
+ # -- response handling ----------------------------------------------
149
+
150
+ @staticmethod
151
+ def _parse_body(response: httpx.Response) -> Any:
152
+ try:
153
+ return response.json()
154
+ except ValueError:
155
+ return response.text
156
+
157
+ def _make_status_error(self, response: httpx.Response) -> APIStatusError:
158
+ body: Any = self._parse_body(response)
159
+ message: str | None = None
160
+ if isinstance(body, dict):
161
+ mapping = cast("dict[str, Any]", body)
162
+ raw = mapping.get("message") or mapping.get("error")
163
+ if isinstance(raw, str):
164
+ message = raw
165
+ if message is None:
166
+ message = f"HTTP {response.status_code} from {response.request.url}"
167
+
168
+ exc_class = _STATUS_EXCEPTIONS.get(response.status_code)
169
+ if exc_class is None:
170
+ exc_class = ServerError if response.status_code >= 500 else APIStatusError
171
+ return exc_class(message, response=response, body=cast("Any", body))
172
+
173
+ @staticmethod
174
+ def _parse_model(response: httpx.Response, cast_to: type[ResponseT]) -> ResponseT:
175
+ try:
176
+ data = response.json()
177
+ except ValueError as exc:
178
+ content_type = response.headers.get("content-type", "an unknown content type")
179
+ raise APIResponseValidationError(
180
+ f"Expected a JSON response body from {response.request.url}, got {content_type}.",
181
+ response=response,
182
+ ) from exc
183
+ try:
184
+ return cast_to.model_validate(data)
185
+ except ValidationError as exc:
186
+ # Parametrized generics (e.g. CursorPage[Person]) may lack ``__name__``.
187
+ name = getattr(cast_to, "__name__", str(cast_to))
188
+ raise APIResponseValidationError(
189
+ f"Response body did not match {name}.",
190
+ response=response,
191
+ ) from exc
blitz_api/_client.py ADDED
@@ -0,0 +1,13 @@
1
+ """The public Blitz API clients: :class:`BlitzAPI` (sync) and :class:`AsyncBlitzAPI`.
2
+
3
+ The async client is hand-written in :mod:`blitz_api._client_async`; the sync client in
4
+ :mod:`blitz_api._client_sync` is generated from it by ``scripts/gen_sync.py``. This module
5
+ re-exports both so ``from blitz_api._client import BlitzAPI`` keeps working.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._client_async import AsyncBlitzAPI
11
+ from ._client_sync import BlitzAPI
12
+
13
+ __all__ = ["AsyncBlitzAPI", "BlitzAPI"]
@@ -0,0 +1,143 @@
1
+ """The Blitz API client implementation (async source; sync is generated alongside)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import cached_property
6
+ from types import TracebackType
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from . import _constants as C
12
+ from ._base_client import BaseClient, ResponseT, to_jsonable
13
+ from ._compat import AsyncSleep, TimeoutParam
14
+ from ._exceptions import APIConnectionError, APITimeoutError
15
+ from ._pagination_base import BasePage
16
+ from ._rate_limit import AsyncRateLimiter
17
+ from .resources import (
18
+ AsyncAccountResource,
19
+ AsyncEnrichmentResource,
20
+ AsyncSearchResource,
21
+ AsyncUtilsResource,
22
+ )
23
+
24
+
25
+ class AsyncBlitzAPI(BaseClient):
26
+ """Client for the Blitz API.
27
+
28
+ ``AsyncBlitzAPI`` is the async client and ``BlitzAPI`` is the sync client; both
29
+ expose an identical method surface. See the ``blitz_api`` package docstring for a
30
+ quickstart. (This docstring is shared with the generated sync client, so it stays
31
+ flavour-neutral.)
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ api_key: str | None = None,
37
+ *,
38
+ base_url: str = C.DEFAULT_BASE_URL,
39
+ timeout: float | httpx.Timeout = C.DEFAULT_TIMEOUT,
40
+ max_retries: int = C.DEFAULT_MAX_RETRIES,
41
+ rate_limit_rps: float | None = C.DEFAULT_RATE_LIMIT_RPS,
42
+ http_client: httpx.AsyncClient | None = None,
43
+ sleep: AsyncSleep | None = None,
44
+ ) -> None:
45
+ super().__init__(api_key=api_key, base_url=base_url, max_retries=max_retries)
46
+ if http_client is not None and timeout != C.DEFAULT_TIMEOUT:
47
+ raise ValueError(
48
+ "Pass `timeout` or `http_client`, not both: a supplied http_client carries "
49
+ "its own timeout. Set the timeout on your httpx client instead."
50
+ )
51
+ self._http_client = http_client or httpx.AsyncClient(timeout=timeout)
52
+ self._owns_http_client = http_client is None
53
+ self._rate_limiter = AsyncRateLimiter(rate_limit_rps)
54
+ if sleep is None:
55
+ import asyncio
56
+
57
+ sleep = asyncio.sleep
58
+ self._sleep = sleep
59
+
60
+ async def _request(
61
+ self,
62
+ method: str,
63
+ path: str,
64
+ *,
65
+ body: Any | None,
66
+ cast_to: type[ResponseT],
67
+ timeout: TimeoutParam = None,
68
+ ) -> ResponseT:
69
+ url = self._build_url(path)
70
+ headers = self._build_headers()
71
+ json_body = to_jsonable(body) if body is not None else None
72
+
73
+ attempt = 0
74
+ while True:
75
+ await self._rate_limiter.acquire()
76
+ try:
77
+ if timeout is None:
78
+ response = await self._http_client.request(
79
+ method, url, headers=headers, json=json_body
80
+ )
81
+ else:
82
+ response = await self._http_client.request(
83
+ method, url, headers=headers, json=json_body, timeout=timeout
84
+ )
85
+ except httpx.TimeoutException as exc:
86
+ if self._should_retry_exception(exc) and attempt < self.max_retries:
87
+ attempt += 1
88
+ await self._sleep(self._backoff_seconds(attempt))
89
+ continue
90
+ raise APITimeoutError(request=exc.request) from exc
91
+ except httpx.RequestError as exc:
92
+ if self._should_retry_exception(exc) and attempt < self.max_retries:
93
+ attempt += 1
94
+ await self._sleep(self._backoff_seconds(attempt))
95
+ continue
96
+ raise APIConnectionError(
97
+ str(exc) or "Connection error.", request=exc.request
98
+ ) from exc
99
+
100
+ if response.is_success:
101
+ result = self._parse_model(response, cast_to)
102
+ if isinstance(result, BasePage):
103
+ result._bind(self, method, path, json_body, timeout)
104
+ return result
105
+
106
+ if self._should_retry(response.status_code) and attempt < self.max_retries:
107
+ attempt += 1
108
+ await self._sleep(self._retry_delay(response, attempt))
109
+ continue
110
+
111
+ raise self._make_status_error(response)
112
+
113
+ async def close(self) -> None:
114
+ """Close the underlying HTTP connection pool (if owned by this client)."""
115
+ if self._owns_http_client:
116
+ await self._http_client.aclose()
117
+
118
+ async def __aenter__(self) -> AsyncBlitzAPI:
119
+ return self
120
+
121
+ async def __aexit__(
122
+ self,
123
+ exc_type: type[BaseException] | None,
124
+ exc: BaseException | None,
125
+ tb: TracebackType | None,
126
+ ) -> None:
127
+ await self.close()
128
+
129
+ @cached_property
130
+ def account(self) -> AsyncAccountResource:
131
+ return AsyncAccountResource(self)
132
+
133
+ @cached_property
134
+ def search(self) -> AsyncSearchResource:
135
+ return AsyncSearchResource(self)
136
+
137
+ @cached_property
138
+ def enrichment(self) -> AsyncEnrichmentResource:
139
+ return AsyncEnrichmentResource(self)
140
+
141
+ @cached_property
142
+ def utils(self) -> AsyncUtilsResource:
143
+ return AsyncUtilsResource(self)
@@ -0,0 +1,145 @@
1
+ # This file is @generated by scripts/gen_sync.py from src/blitz_api/_client_async.py.
2
+ # Do not edit by hand — edit the async source and run `python scripts/gen_sync.py`.
3
+ """The Blitz API client implementation (async source; sync is generated alongside)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from functools import cached_property
8
+ from types import TracebackType
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from . import _constants as C
14
+ from ._base_client import BaseClient, ResponseT, to_jsonable
15
+ from ._compat import SyncSleep, TimeoutParam
16
+ from ._exceptions import APIConnectionError, APITimeoutError
17
+ from ._pagination_base import BasePage
18
+ from ._rate_limit import RateLimiter
19
+ from .resources import (
20
+ AccountResource,
21
+ EnrichmentResource,
22
+ SearchResource,
23
+ UtilsResource,
24
+ )
25
+
26
+
27
+ class BlitzAPI(BaseClient):
28
+ """Client for the Blitz API.
29
+
30
+ ``AsyncBlitzAPI`` is the async client and ``BlitzAPI`` is the sync client; both
31
+ expose an identical method surface. See the ``blitz_api`` package docstring for a
32
+ quickstart. (This docstring is shared with the generated sync client, so it stays
33
+ flavour-neutral.)
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str | None = None,
39
+ *,
40
+ base_url: str = C.DEFAULT_BASE_URL,
41
+ timeout: float | httpx.Timeout = C.DEFAULT_TIMEOUT,
42
+ max_retries: int = C.DEFAULT_MAX_RETRIES,
43
+ rate_limit_rps: float | None = C.DEFAULT_RATE_LIMIT_RPS,
44
+ http_client: httpx.Client | None = None,
45
+ sleep: SyncSleep | None = None,
46
+ ) -> None:
47
+ super().__init__(api_key=api_key, base_url=base_url, max_retries=max_retries)
48
+ if http_client is not None and timeout != C.DEFAULT_TIMEOUT:
49
+ raise ValueError(
50
+ "Pass `timeout` or `http_client`, not both: a supplied http_client carries "
51
+ "its own timeout. Set the timeout on your httpx client instead."
52
+ )
53
+ self._http_client = http_client or httpx.Client(timeout=timeout)
54
+ self._owns_http_client = http_client is None
55
+ self._rate_limiter = RateLimiter(rate_limit_rps)
56
+ if sleep is None:
57
+ import time
58
+
59
+ sleep = time.sleep
60
+ self._sleep = sleep
61
+
62
+ def _request(
63
+ self,
64
+ method: str,
65
+ path: str,
66
+ *,
67
+ body: Any | None,
68
+ cast_to: type[ResponseT],
69
+ timeout: TimeoutParam = None,
70
+ ) -> ResponseT:
71
+ url = self._build_url(path)
72
+ headers = self._build_headers()
73
+ json_body = to_jsonable(body) if body is not None else None
74
+
75
+ attempt = 0
76
+ while True:
77
+ self._rate_limiter.acquire()
78
+ try:
79
+ if timeout is None:
80
+ response = self._http_client.request(
81
+ method, url, headers=headers, json=json_body
82
+ )
83
+ else:
84
+ response = self._http_client.request(
85
+ method, url, headers=headers, json=json_body, timeout=timeout
86
+ )
87
+ except httpx.TimeoutException as exc:
88
+ if self._should_retry_exception(exc) and attempt < self.max_retries:
89
+ attempt += 1
90
+ self._sleep(self._backoff_seconds(attempt))
91
+ continue
92
+ raise APITimeoutError(request=exc.request) from exc
93
+ except httpx.RequestError as exc:
94
+ if self._should_retry_exception(exc) and attempt < self.max_retries:
95
+ attempt += 1
96
+ self._sleep(self._backoff_seconds(attempt))
97
+ continue
98
+ raise APIConnectionError(
99
+ str(exc) or "Connection error.", request=exc.request
100
+ ) from exc
101
+
102
+ if response.is_success:
103
+ result = self._parse_model(response, cast_to)
104
+ if isinstance(result, BasePage):
105
+ result._bind(self, method, path, json_body, timeout)
106
+ return result
107
+
108
+ if self._should_retry(response.status_code) and attempt < self.max_retries:
109
+ attempt += 1
110
+ self._sleep(self._retry_delay(response, attempt))
111
+ continue
112
+
113
+ raise self._make_status_error(response)
114
+
115
+ def close(self) -> None:
116
+ """Close the underlying HTTP connection pool (if owned by this client)."""
117
+ if self._owns_http_client:
118
+ self._http_client.close()
119
+
120
+ def __enter__(self) -> BlitzAPI:
121
+ return self
122
+
123
+ def __exit__(
124
+ self,
125
+ exc_type: type[BaseException] | None,
126
+ exc: BaseException | None,
127
+ tb: TracebackType | None,
128
+ ) -> None:
129
+ self.close()
130
+
131
+ @cached_property
132
+ def account(self) -> AccountResource:
133
+ return AccountResource(self)
134
+
135
+ @cached_property
136
+ def search(self) -> SearchResource:
137
+ return SearchResource(self)
138
+
139
+ @cached_property
140
+ def enrichment(self) -> EnrichmentResource:
141
+ return EnrichmentResource(self)
142
+
143
+ @cached_property
144
+ def utils(self) -> UtilsResource:
145
+ return UtilsResource(self)
blitz_api/_compat.py ADDED
@@ -0,0 +1,26 @@
1
+ """Type aliases that differ between the async source and the generated sync code.
2
+
3
+ The async client takes an awaitable sleep callable; the sync client takes a plain
4
+ one. ``scripts/gen_sync.py`` rewrites the token ``AsyncSleep`` to ``SyncSleep`` when
5
+ it transliterates the async source, so both flavours stay precisely typed without the
6
+ generator having to rewrite a subscripted generic (which token-level renaming can't do).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Awaitable, Callable
12
+ from typing import TYPE_CHECKING, Union
13
+
14
+ if TYPE_CHECKING:
15
+ import httpx
16
+
17
+ #: Sleep callable accepted by ``AsyncBlitzAPI`` (e.g. ``asyncio.sleep``).
18
+ AsyncSleep = Callable[[float], Awaitable[None]]
19
+
20
+ #: Sleep callable accepted by ``BlitzAPI`` (e.g. ``time.sleep``).
21
+ SyncSleep = Callable[[float], None]
22
+
23
+ #: Per-call timeout override: a number of seconds, an :class:`httpx.Timeout`, or
24
+ #: ``None`` to fall back to the client-wide timeout. (Defined with ``Union`` and a
25
+ #: forward reference so importing it never forces ``httpx`` to load at runtime.)
26
+ TimeoutParam = Union[float, "httpx.Timeout", None]
@@ -0,0 +1,36 @@
1
+ """Internal defaults shared by the sync and async clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._version import __version__
6
+
7
+ #: Production API base URL.
8
+ DEFAULT_BASE_URL = "https://api.blitz-api.ai"
9
+
10
+ #: Environment variable read when ``api_key`` is not passed explicitly.
11
+ API_KEY_ENV_VAR = "BLITZ_API_KEY"
12
+
13
+ #: Header carrying the API key (Blitz uses ``x-api-key``, not ``Authorization``).
14
+ API_KEY_HEADER = "x-api-key"
15
+
16
+ #: Default per-request timeout, in seconds.
17
+ DEFAULT_TIMEOUT = 30.0
18
+
19
+ #: Number of retries (in addition to the first attempt) for transient failures.
20
+ DEFAULT_MAX_RETRIES = 3
21
+
22
+ #: Default client-side rate limit. The API allows 5 req/s on every plan; the
23
+ #: per-key value is discoverable via ``client.account.key_info()``.
24
+ DEFAULT_RATE_LIMIT_RPS = 5.0
25
+
26
+ #: Seconds to wait after a 429 when the response has no ``Retry-After`` header.
27
+ #: Matches the behaviour of the official reference client.
28
+ DEFAULT_RETRY_AFTER_SECONDS = 60.0
29
+
30
+ #: Upper bound (seconds) on any single retry wait, including a server-supplied
31
+ #: ``Retry-After``. A safety clamp so a pathological header value (e.g.
32
+ #: ``Retry-After: 86400``) can't make the client sleep for hours.
33
+ MAX_RETRY_WAIT_SECONDS = 120.0
34
+
35
+ #: Sent as the ``User-Agent`` header so requests are attributable to this SDK.
36
+ USER_AGENT = f"blitz-api-py/{__version__}"