sahiapi 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.
- sahiapi/__init__.py +88 -0
- sahiapi/py.typed +0 -0
- sahiapi/sahi/__init__.py +0 -0
- sahiapi/sahi/account.py +28 -0
- sahiapi/sahi/auth.py +163 -0
- sahiapi/sahi/base.py +32 -0
- sahiapi/sahi/client.py +306 -0
- sahiapi/sahi/constants.py +115 -0
- sahiapi/sahi/env.py +39 -0
- sahiapi/sahi/exceptions.py +412 -0
- sahiapi/sahi/feed.py +668 -0
- sahiapi/sahi/funds.py +134 -0
- sahiapi/sahi/http.py +301 -0
- sahiapi/sahi/instruments.py +315 -0
- sahiapi/sahi/marketdata.py +268 -0
- sahiapi/sahi/orders.py +225 -0
- sahiapi/sahi/portfolio.py +49 -0
- sahiapi/sahi/proto/__init__.py +0 -0
- sahiapi/sahi/proto/codec.py +126 -0
- sahiapi/sahi/proto/marketdata.proto +138 -0
- sahiapi/sahi/proto/marketdata_pb2.py +55 -0
- sahiapi/sahi/ws_client.py +355 -0
- sahiapi-0.1.0.dist-info/METADATA +97 -0
- sahiapi-0.1.0.dist-info/RECORD +26 -0
- sahiapi-0.1.0.dist-info/WHEEL +4 -0
- sahiapi-0.1.0.dist-info/licenses/LICENSE +7 -0
sahiapi/sahi/funds.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Funds endpoints: available balances and pre-trade margin checks.
|
|
2
|
+
|
|
3
|
+
Mirrors the Funds page of the API reference. All three methods need the
|
|
4
|
+
``funds.read`` scope. The margin checks take the same order body as
|
|
5
|
+
:meth:`place_order`, so one order object serves both placing and sizing.
|
|
6
|
+
Balances and margins are integer paise, returned unchanged.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import EndpointGroup
|
|
14
|
+
from .orders import build_order_body
|
|
15
|
+
|
|
16
|
+
__all__ = ["_Funds"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Funds(EndpointGroup):
|
|
20
|
+
"""Funds and margin methods mixed into :class:`SahiAPI`."""
|
|
21
|
+
|
|
22
|
+
def get_funds(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
23
|
+
"""Return available balances (``GET /v1/funds``).
|
|
24
|
+
|
|
25
|
+
Budgeted at one request per second, the tightest limit on the page:
|
|
26
|
+
read it when you need it, not in a polling loop.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The funds dict, with ``available``, ``balance``, ``utilised`` and
|
|
30
|
+
``option_premium`` sections, all in paise.
|
|
31
|
+
"""
|
|
32
|
+
return self._request( # type: ignore[no-any-return]
|
|
33
|
+
"GET", "/v1/funds", timeout=timeout, retry_on_rate_limit=True
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get_order_margin(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
exchange_segment: str,
|
|
40
|
+
symbol: str,
|
|
41
|
+
txn_type: str,
|
|
42
|
+
qty: int,
|
|
43
|
+
order_type: str,
|
|
44
|
+
product: str,
|
|
45
|
+
validity: str,
|
|
46
|
+
price: int,
|
|
47
|
+
trigger_price: int | None = None,
|
|
48
|
+
disclosed_qty: int | None = None,
|
|
49
|
+
market_protection: float | None = None,
|
|
50
|
+
variant: str | None = None,
|
|
51
|
+
timeout: float | None = None,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Quote the margin a single order would need (``POST /v1/margins/order``).
|
|
54
|
+
|
|
55
|
+
Takes the same fields as :meth:`place_order`, all keyword-only; ``qty`` is
|
|
56
|
+
not capped at the freeze quantity here, so you can size a full position
|
|
57
|
+
before slicing it. Nothing is placed and the call is not IP-checked.
|
|
58
|
+
|
|
59
|
+
A ``429`` is raised as :class:`~sahiapi.SahiRateLimitException`, not
|
|
60
|
+
retried: the SDK retries only ``GET`` requests, and this is a ``POST``.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
``{"required_margin": ..., "available_margin": ...}``, in paise.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
SahiServerException: ``503 UNDER_MAINTENANCE`` between 04:45 and
|
|
67
|
+
07:00 IST, while the OMS runs its beginning-of-day processing.
|
|
68
|
+
Retry after 07:00 IST; the exception's generic hint of a short
|
|
69
|
+
wait does not apply here.
|
|
70
|
+
"""
|
|
71
|
+
body = build_order_body(
|
|
72
|
+
exchange_segment,
|
|
73
|
+
symbol,
|
|
74
|
+
txn_type,
|
|
75
|
+
qty,
|
|
76
|
+
order_type,
|
|
77
|
+
product,
|
|
78
|
+
validity,
|
|
79
|
+
price,
|
|
80
|
+
trigger_price=trigger_price,
|
|
81
|
+
disclosed_qty=disclosed_qty,
|
|
82
|
+
market_protection=market_protection,
|
|
83
|
+
variant=variant,
|
|
84
|
+
)
|
|
85
|
+
# A margin quote is a read in POST clothing, but the core only retries a
|
|
86
|
+
# GET on 429: a 429 here surfaces as SahiRateLimitException for the caller.
|
|
87
|
+
return self._request( # type: ignore[no-any-return]
|
|
88
|
+
"POST", "/v1/margins/order", json=body, timeout=timeout
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def get_basket_margin(
|
|
92
|
+
self, legs: list[dict[str, Any]], *, timeout: float | None = None
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""Quote the margin a basket would need, netted (``POST /v1/margins/basket``).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
legs: 1 to 10 order bodies, each shaped exactly like a
|
|
98
|
+
:meth:`place_order` request. Every leg is built through the same
|
|
99
|
+
body builder :meth:`place_order` and :meth:`get_order_margin`
|
|
100
|
+
use, so a key the API does not define — a misspelled
|
|
101
|
+
``market_protection``, say — raises :class:`TypeError` naming the
|
|
102
|
+
leg rather than being dropped in transit. One bad leg refuses the
|
|
103
|
+
whole request, and the server's message names it by zero-based
|
|
104
|
+
index.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
``{"required_margin": ..., "available_margin": ...}``, in paise, with
|
|
108
|
+
the legs netted against each other and existing positions.
|
|
109
|
+
``available_margin`` is quoted from a single margin pool, so for a
|
|
110
|
+
basket that mixes pools (an equity delivery leg beside a commodity
|
|
111
|
+
option, say) the comparison can overstate what the account can fund.
|
|
112
|
+
|
|
113
|
+
A ``429`` is raised, not retried, as on :meth:`get_order_margin`.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
TypeError: If a leg is missing a required field or carries one the
|
|
117
|
+
API does not define. The message names the leg by zero-based
|
|
118
|
+
index, as the server's own leg errors do.
|
|
119
|
+
SahiServerException: ``503 UNDER_MAINTENANCE`` between 04:45 and
|
|
120
|
+
07:00 IST, as on :meth:`get_order_margin`. Retry after 07:00 IST.
|
|
121
|
+
"""
|
|
122
|
+
# The server ignores fields it does not know, so a misspelled key would
|
|
123
|
+
# otherwise be quoted at a value the caller never asked for; building each
|
|
124
|
+
# leg here gives the basket the check get_order_margin already has.
|
|
125
|
+
body: list[dict[str, Any]] = []
|
|
126
|
+
for index, leg in enumerate(legs):
|
|
127
|
+
try:
|
|
128
|
+
body.append(build_order_body(**leg))
|
|
129
|
+
except TypeError as e:
|
|
130
|
+
raise TypeError(f"leg {index}: {e}") from None
|
|
131
|
+
# Not retried on 429: only a GET may opt in (see get_order_margin).
|
|
132
|
+
return self._request( # type: ignore[no-any-return]
|
|
133
|
+
"POST", "/v1/margins/basket", json={"legs": body}, timeout=timeout
|
|
134
|
+
)
|
sahiapi/sahi/http.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""The HTTP core shared by every REST call.
|
|
2
|
+
|
|
3
|
+
:class:`HttpClient` wraps a single :class:`httpx.Client`. It owns the concerns
|
|
4
|
+
that are identical for every endpoint so the domain modules stay a thin list of
|
|
5
|
+
methods:
|
|
6
|
+
|
|
7
|
+
* the base URL and, optionally, the ``Authorization: Bearer`` header,
|
|
8
|
+
* a ``User-Agent`` and a per-request ``X-Request-Id``,
|
|
9
|
+
* turning a non-2xx response into the right :class:`SahiAPIException` via
|
|
10
|
+
:func:`exception_for`, carrying the server's request id,
|
|
11
|
+
* a deliberately minimal retry policy.
|
|
12
|
+
|
|
13
|
+
Retries are conservative on purpose. A ``GET`` that hits ``429`` with a short
|
|
14
|
+
``Retry-After`` is retried once, because reads are idempotent and the header
|
|
15
|
+
tells us exactly how long to wait. The wait is capped: the API's rate limits
|
|
16
|
+
include per-minute and per-day windows whose ``Retry-After`` can run to hours,
|
|
17
|
+
and a library call must never block that long on the caller's behalf — those
|
|
18
|
+
are raised for the caller to decide. Nothing else is retried: an order write
|
|
19
|
+
must never be sent twice, and the auth host sends no ``Retry-After`` anyway.
|
|
20
|
+
The core enforces this: opting anything but a ``GET`` into the retry raises
|
|
21
|
+
:class:`SahiConfigurationException` before the request is sent.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
from types import TracebackType
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
from . import exceptions
|
|
35
|
+
from .exceptions import exception_for
|
|
36
|
+
|
|
37
|
+
__all__ = ["HttpClient"]
|
|
38
|
+
|
|
39
|
+
_REQUEST_ID_HEADER = "X-Request-Id"
|
|
40
|
+
|
|
41
|
+
# Longest Retry-After the single automatic retry will honour. Covers the
|
|
42
|
+
# per-second windows a burst of reads trips; longer waits come from the
|
|
43
|
+
# per-minute or per-day windows and are the caller's decision.
|
|
44
|
+
_MAX_AUTO_RETRY_SECONDS = 5
|
|
45
|
+
|
|
46
|
+
# How long an idle pooled connection is kept. httpx's default is 5s, which makes
|
|
47
|
+
# a client that polls every 30s, or idles between signals and then places an
|
|
48
|
+
# order, pay a fresh TCP and TLS handshake on the call that matters most. Keep
|
|
49
|
+
# this below the API host's load-balancer idle timeout so pooled connections are
|
|
50
|
+
# never stale.
|
|
51
|
+
_KEEPALIVE_EXPIRY_SECONDS = 30.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sdk_version() -> str:
|
|
55
|
+
# Reuse the package's resolved __version__ rather than looking it up again.
|
|
56
|
+
# Imported lazily to avoid a cycle: this module is imported while the package
|
|
57
|
+
# __init__ is still executing.
|
|
58
|
+
from sahiapi import __version__
|
|
59
|
+
|
|
60
|
+
return __version__
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _user_agent() -> str:
|
|
64
|
+
py = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
65
|
+
return f"sahiapi/{_sdk_version()} python/{py}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _new_request_id() -> str:
|
|
69
|
+
"""A per-request id, prefixed so it is recognisable in server logs."""
|
|
70
|
+
return f"sahiapi-{uuid.uuid4().hex}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_retry_after(value: str | None) -> int | None:
|
|
74
|
+
"""Parse a ``Retry-After`` header expressed in whole seconds."""
|
|
75
|
+
if not value:
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
return int(value.strip())
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _auto_retry_wait(error: exceptions.SahiAPIException) -> int | None:
|
|
84
|
+
"""Seconds to wait before the automatic retry, or ``None`` if ``error`` gets none.
|
|
85
|
+
|
|
86
|
+
Only a ``429`` whose ``Retry-After`` is at most ``_MAX_AUTO_RETRY_SECONDS``
|
|
87
|
+
is waited out on the caller's behalf.
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(error, exceptions.SahiRateLimitException):
|
|
90
|
+
return None
|
|
91
|
+
if error.retry_after is None or error.retry_after > _MAX_AUTO_RETRY_SECONDS:
|
|
92
|
+
return None
|
|
93
|
+
return error.retry_after
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class HttpClient:
|
|
97
|
+
"""A configured HTTP client for one Sahi host.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
base_url: The host base URL, without a trailing slash.
|
|
101
|
+
host: Which host this is — :data:`sahiapi.sahi.exceptions.HOST_API` or
|
|
102
|
+
``HOST_AUTH`` — so errors map to the right class.
|
|
103
|
+
access_token: Bearer token to send on every request. ``None`` for the
|
|
104
|
+
auth host, whose token endpoint takes no ``Authorization`` header.
|
|
105
|
+
timeout: Default timeout in seconds. httpx applies it to each phase of
|
|
106
|
+
a request separately (connect, write, read, pool), so it bounds no
|
|
107
|
+
single phase to more than this, not the call as a whole.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
base_url: str,
|
|
113
|
+
*,
|
|
114
|
+
host: exceptions.Host,
|
|
115
|
+
access_token: str | None = None,
|
|
116
|
+
timeout: float = 10.0,
|
|
117
|
+
) -> None:
|
|
118
|
+
self._host = host
|
|
119
|
+
self._default_timeout = timeout
|
|
120
|
+
headers = {
|
|
121
|
+
"User-Agent": _user_agent(),
|
|
122
|
+
"Accept": "application/json",
|
|
123
|
+
}
|
|
124
|
+
if access_token is not None:
|
|
125
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
126
|
+
self._client = httpx.Client(
|
|
127
|
+
base_url=base_url,
|
|
128
|
+
headers=headers,
|
|
129
|
+
timeout=timeout,
|
|
130
|
+
limits=httpx.Limits(keepalive_expiry=_KEEPALIVE_EXPIRY_SECONDS),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# -- lifecycle ------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def set_access_token(self, access_token: str) -> None:
|
|
136
|
+
"""Replace the Bearer token used on subsequent requests."""
|
|
137
|
+
self._client.headers["Authorization"] = f"Bearer {access_token}"
|
|
138
|
+
|
|
139
|
+
def close(self) -> None:
|
|
140
|
+
"""Close the underlying connection pool."""
|
|
141
|
+
self._client.close()
|
|
142
|
+
|
|
143
|
+
def __enter__(self) -> HttpClient:
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __exit__(
|
|
147
|
+
self,
|
|
148
|
+
exc_type: type[BaseException] | None,
|
|
149
|
+
exc: BaseException | None,
|
|
150
|
+
tb: TracebackType | None,
|
|
151
|
+
) -> None:
|
|
152
|
+
self.close()
|
|
153
|
+
|
|
154
|
+
# -- requests -------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def request(
|
|
157
|
+
self,
|
|
158
|
+
method: str,
|
|
159
|
+
path: str,
|
|
160
|
+
*,
|
|
161
|
+
params: dict[str, Any] | None = None,
|
|
162
|
+
json: Any = None,
|
|
163
|
+
timeout: float | None = None,
|
|
164
|
+
retry_on_rate_limit: bool = False,
|
|
165
|
+
) -> Any:
|
|
166
|
+
"""Send a request and return the parsed JSON body.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
method: HTTP method.
|
|
170
|
+
path: Path relative to the host base URL (for example ``/v1/orders``).
|
|
171
|
+
params: Query parameters. Sequence values become repeated params.
|
|
172
|
+
json: JSON body to send.
|
|
173
|
+
timeout: Per-call timeout override, per phase like the client
|
|
174
|
+
default; falls back to the client default.
|
|
175
|
+
retry_on_rate_limit: When true, a single retry is made on a ``429``
|
|
176
|
+
whose ``Retry-After`` is at most ``_MAX_AUTO_RETRY_SECONDS``.
|
|
177
|
+
Only a ``GET`` may opt in; anything else is refused.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The decoded JSON body, or ``None`` for an empty ``204`` response.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
SahiConfigurationException: If ``retry_on_rate_limit`` is set on a
|
|
184
|
+
method other than ``GET``.
|
|
185
|
+
SahiConnectionException: If the connection could not be opened.
|
|
186
|
+
SahiTimeoutException: If the request timed out.
|
|
187
|
+
SahiAPIException: For any non-2xx response, as the matching
|
|
188
|
+
subclass, or for a 2xx whose body is not JSON.
|
|
189
|
+
"""
|
|
190
|
+
if retry_on_rate_limit and method.upper() != "GET":
|
|
191
|
+
raise exceptions.SahiConfigurationException(
|
|
192
|
+
f"retry_on_rate_limit is only allowed on GET, not {method.upper()} {path}: "
|
|
193
|
+
"a retried write could be applied twice."
|
|
194
|
+
)
|
|
195
|
+
request_id = _new_request_id()
|
|
196
|
+
headers = {_REQUEST_ID_HEADER: request_id}
|
|
197
|
+
effective_timeout = self._default_timeout if timeout is None else timeout
|
|
198
|
+
|
|
199
|
+
attempted_retry = False
|
|
200
|
+
while True:
|
|
201
|
+
response = self._send_once(
|
|
202
|
+
method,
|
|
203
|
+
path,
|
|
204
|
+
params=params,
|
|
205
|
+
json=json,
|
|
206
|
+
headers=headers,
|
|
207
|
+
timeout=effective_timeout,
|
|
208
|
+
request_id=request_id,
|
|
209
|
+
)
|
|
210
|
+
if response.is_success:
|
|
211
|
+
return self._decode(response, request_id)
|
|
212
|
+
|
|
213
|
+
error = self._error_from(response, request_id)
|
|
214
|
+
if retry_on_rate_limit and not attempted_retry:
|
|
215
|
+
wait = _auto_retry_wait(error)
|
|
216
|
+
if wait is not None:
|
|
217
|
+
attempted_retry = True
|
|
218
|
+
self._sleep(wait)
|
|
219
|
+
continue
|
|
220
|
+
raise error
|
|
221
|
+
|
|
222
|
+
# -- helpers --------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def _send_once(
|
|
225
|
+
self,
|
|
226
|
+
method: str,
|
|
227
|
+
path: str,
|
|
228
|
+
*,
|
|
229
|
+
params: dict[str, Any] | None,
|
|
230
|
+
json: Any,
|
|
231
|
+
headers: dict[str, str],
|
|
232
|
+
timeout: float,
|
|
233
|
+
request_id: str,
|
|
234
|
+
) -> httpx.Response:
|
|
235
|
+
"""Send one attempt, mapping httpx's transport errors to Sahi exceptions."""
|
|
236
|
+
try:
|
|
237
|
+
return self._client.request(
|
|
238
|
+
method,
|
|
239
|
+
path,
|
|
240
|
+
params=params,
|
|
241
|
+
json=json,
|
|
242
|
+
headers=headers,
|
|
243
|
+
timeout=timeout,
|
|
244
|
+
)
|
|
245
|
+
except (httpx.ConnectError, httpx.ConnectTimeout) as e: # DNS, refused, TLS
|
|
246
|
+
raise exceptions.SahiConnectionException(
|
|
247
|
+
f"could not connect to {self._client.base_url}: {e}. "
|
|
248
|
+
"Check network access and the base URL.",
|
|
249
|
+
request_id=request_id,
|
|
250
|
+
) from e
|
|
251
|
+
except httpx.TimeoutException as e:
|
|
252
|
+
raise exceptions.SahiTimeoutException("request timed out", request_id=request_id) from e
|
|
253
|
+
except httpx.HTTPError as e: # protocol errors after the connection opened
|
|
254
|
+
raise exceptions.SahiAPIException(
|
|
255
|
+
f"could not complete the request to {self._client.base_url}: {e}.",
|
|
256
|
+
request_id=request_id,
|
|
257
|
+
) from e
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _decode(response: httpx.Response, request_id: str) -> Any:
|
|
261
|
+
if response.status_code == 204 or not response.content:
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
return response.json()
|
|
265
|
+
except ValueError as e:
|
|
266
|
+
# A 2xx that is not JSON: typically a proxy or WAF interstitial.
|
|
267
|
+
raise exceptions.SahiAPIException(
|
|
268
|
+
f"expected a JSON body but got {response.headers.get('content-type', 'unknown')} "
|
|
269
|
+
f"with status {response.status_code}",
|
|
270
|
+
status=response.status_code,
|
|
271
|
+
request_id=response.headers.get(_REQUEST_ID_HEADER) or request_id,
|
|
272
|
+
) from e
|
|
273
|
+
|
|
274
|
+
def _error_from(self, response: httpx.Response, request_id: str) -> exceptions.SahiAPIException:
|
|
275
|
+
code: str | None = None
|
|
276
|
+
message: str | None = None
|
|
277
|
+
try:
|
|
278
|
+
body = response.json()
|
|
279
|
+
if isinstance(body, dict):
|
|
280
|
+
raw_code = body.get("code")
|
|
281
|
+
# A gateway may send a numeric code; the mapper needs a string.
|
|
282
|
+
code = raw_code if isinstance(raw_code, str) else None
|
|
283
|
+
message = body.get("message")
|
|
284
|
+
except ValueError:
|
|
285
|
+
message = response.text or None
|
|
286
|
+
|
|
287
|
+
server_request_id = response.headers.get(_REQUEST_ID_HEADER) or request_id
|
|
288
|
+
retry_after = _parse_retry_after(response.headers.get("Retry-After"))
|
|
289
|
+
return exception_for(
|
|
290
|
+
response.status_code,
|
|
291
|
+
host=self._host,
|
|
292
|
+
code=code,
|
|
293
|
+
message=message,
|
|
294
|
+
request_id=server_request_id,
|
|
295
|
+
retry_after=retry_after,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def _sleep(seconds: float) -> None:
|
|
300
|
+
# Kept as its own method so the wait can be substituted.
|
|
301
|
+
time.sleep(seconds)
|