etlplus 0.5.4__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.
- etlplus/__init__.py +43 -0
- etlplus/__main__.py +22 -0
- etlplus/__version__.py +14 -0
- etlplus/api/README.md +237 -0
- etlplus/api/__init__.py +136 -0
- etlplus/api/auth.py +432 -0
- etlplus/api/config.py +633 -0
- etlplus/api/endpoint_client.py +885 -0
- etlplus/api/errors.py +170 -0
- etlplus/api/pagination/__init__.py +47 -0
- etlplus/api/pagination/client.py +188 -0
- etlplus/api/pagination/config.py +440 -0
- etlplus/api/pagination/paginator.py +775 -0
- etlplus/api/rate_limiting/__init__.py +38 -0
- etlplus/api/rate_limiting/config.py +343 -0
- etlplus/api/rate_limiting/rate_limiter.py +266 -0
- etlplus/api/request_manager.py +589 -0
- etlplus/api/retry_manager.py +430 -0
- etlplus/api/transport.py +325 -0
- etlplus/api/types.py +172 -0
- etlplus/cli/__init__.py +15 -0
- etlplus/cli/app.py +1367 -0
- etlplus/cli/handlers.py +775 -0
- etlplus/cli/main.py +616 -0
- etlplus/config/__init__.py +56 -0
- etlplus/config/connector.py +372 -0
- etlplus/config/jobs.py +311 -0
- etlplus/config/pipeline.py +339 -0
- etlplus/config/profile.py +78 -0
- etlplus/config/types.py +204 -0
- etlplus/config/utils.py +120 -0
- etlplus/ddl.py +197 -0
- etlplus/enums.py +414 -0
- etlplus/extract.py +218 -0
- etlplus/file.py +657 -0
- etlplus/load.py +336 -0
- etlplus/mixins.py +62 -0
- etlplus/py.typed +0 -0
- etlplus/run.py +368 -0
- etlplus/run_helpers.py +843 -0
- etlplus/templates/__init__.py +5 -0
- etlplus/templates/ddl.sql.j2 +128 -0
- etlplus/templates/view.sql.j2 +69 -0
- etlplus/transform.py +1049 -0
- etlplus/types.py +227 -0
- etlplus/utils.py +638 -0
- etlplus/validate.py +493 -0
- etlplus/validation/__init__.py +44 -0
- etlplus/validation/utils.py +389 -0
- etlplus-0.5.4.dist-info/METADATA +616 -0
- etlplus-0.5.4.dist-info/RECORD +55 -0
- etlplus-0.5.4.dist-info/WHEEL +5 -0
- etlplus-0.5.4.dist-info/entry_points.txt +2 -0
- etlplus-0.5.4.dist-info/licenses/LICENSE +21 -0
- etlplus-0.5.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""
|
|
2
|
+
:mod:`etlplus.api.request_manager` module.
|
|
3
|
+
|
|
4
|
+
HTTP request orchestration with retries and session lifecycle control.
|
|
5
|
+
|
|
6
|
+
This module wraps ``requests`` sessions with retry-aware helpers that manage
|
|
7
|
+
timeouts, HTTP adapters, and context-managed session lifecycles.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from dataclasses import field
|
|
16
|
+
from functools import partial
|
|
17
|
+
from typing import Any
|
|
18
|
+
from typing import cast
|
|
19
|
+
|
|
20
|
+
import requests # type: ignore[import]
|
|
21
|
+
from requests import Response # type: ignore[import]
|
|
22
|
+
|
|
23
|
+
from ..types import JSONData
|
|
24
|
+
from ..types import JSONDict
|
|
25
|
+
from ..types import Timeout
|
|
26
|
+
from .errors import ApiAuthError
|
|
27
|
+
from .errors import ApiRequestError
|
|
28
|
+
from .retry_manager import RetryInput
|
|
29
|
+
from .retry_manager import RetryManager
|
|
30
|
+
from .transport import HTTPAdapterMountConfig
|
|
31
|
+
from .transport import build_session_with_adapters
|
|
32
|
+
|
|
33
|
+
# SECTION: TYPE ALIASES ==================================================== #
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ``requests`` accepts either a scalar timeout or a ``(connect, read)`` pair.
|
|
37
|
+
type TimeoutPair = tuple[Timeout, Timeout]
|
|
38
|
+
type TimeoutInput = Timeout | TimeoutPair
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# SECTION: CONSTANTS ======================================================== #
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_MISSING = object()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# SECTION: CLASSES ========================================================== #
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class RequestManager:
|
|
52
|
+
"""
|
|
53
|
+
Encapsulate HTTP dispatch, retries, and session lifecycle.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
retry : RetryInput, optional
|
|
58
|
+
Retry policy to apply to requests. Default is ``None``.
|
|
59
|
+
retry_network_errors : bool, optional
|
|
60
|
+
Whether to retry on network errors. Default is ``False``.
|
|
61
|
+
default_timeout : TimeoutInput, optional
|
|
62
|
+
Default timeout for requests (seconds or ``(connect, read)`` tuple).
|
|
63
|
+
session : requests.Session | None, optional
|
|
64
|
+
Optional pre-configured session to use. Default is ``None``.
|
|
65
|
+
session_factory : Callable[[], requests.Session] | None, optional
|
|
66
|
+
Optional factory for lazily creating sessions when ``session`` is
|
|
67
|
+
``None``.
|
|
68
|
+
retry_cap : float, optional
|
|
69
|
+
Maximum backoff cap in seconds. Default is 30.0.
|
|
70
|
+
session_adapters : Sequence[HTTPAdapterMountConfig] | None, optional
|
|
71
|
+
Adapter mount configurations used when lazily building a session via
|
|
72
|
+
:func:`etlplus.api.transport.build_session_with_adapters`.
|
|
73
|
+
|
|
74
|
+
Attributes
|
|
75
|
+
----------
|
|
76
|
+
retry : RetryInput
|
|
77
|
+
Retry policy to apply to requests.
|
|
78
|
+
retry_network_errors : bool
|
|
79
|
+
Whether to retry on network errors.
|
|
80
|
+
default_timeout : TimeoutInput
|
|
81
|
+
Default timeout for requests (seconds or ``(connect, read)`` tuple).
|
|
82
|
+
session : requests.Session | None
|
|
83
|
+
Optional pre-configured session to use.
|
|
84
|
+
session_factory : Callable[[], requests.Session] | None
|
|
85
|
+
Optional factory for creating sessions.
|
|
86
|
+
retry_cap : float
|
|
87
|
+
Maximum backoff cap in seconds for :class:`RetryManager` sleeps.
|
|
88
|
+
session_adapters : Sequence[HTTPAdapterMountConfig] | None
|
|
89
|
+
Adapter mount configurations used when lazily building a session.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
# -- Attributes -- #
|
|
93
|
+
|
|
94
|
+
retry: RetryInput = None
|
|
95
|
+
retry_network_errors: bool = False
|
|
96
|
+
default_timeout: TimeoutInput = 10.0
|
|
97
|
+
session: requests.Session | None = None
|
|
98
|
+
session_factory: Callable[[], requests.Session] | None = None
|
|
99
|
+
retry_cap: float = 30.0
|
|
100
|
+
session_adapters: Sequence[HTTPAdapterMountConfig] | None = None
|
|
101
|
+
|
|
102
|
+
def __post_init__(self) -> None:
|
|
103
|
+
if self.session_adapters:
|
|
104
|
+
self.session_adapters = tuple(self.session_adapters)
|
|
105
|
+
|
|
106
|
+
# -- Internal Attributes -- #
|
|
107
|
+
|
|
108
|
+
_ctx_session: Any | None = field(default=None, init=False, repr=False)
|
|
109
|
+
_ctx_owns_session: bool = field(default=False, init=False, repr=False)
|
|
110
|
+
|
|
111
|
+
# -- Magic Methods (Context Manager Protocol) -- #
|
|
112
|
+
|
|
113
|
+
def __enter__(self) -> RequestManager:
|
|
114
|
+
"""
|
|
115
|
+
Enter the runtime context and ensure a session is available.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
RequestManager
|
|
120
|
+
The manager instance with an active session context.
|
|
121
|
+
"""
|
|
122
|
+
if self._ctx_session is not None:
|
|
123
|
+
return self
|
|
124
|
+
if self.session is not None:
|
|
125
|
+
self._ctx_session = self.session
|
|
126
|
+
self._ctx_owns_session = False
|
|
127
|
+
return self
|
|
128
|
+
sess, owns_session = self._instantiate_session()
|
|
129
|
+
if sess is None:
|
|
130
|
+
sess = requests.Session()
|
|
131
|
+
owns_session = True
|
|
132
|
+
self._ctx_session = sess
|
|
133
|
+
self._ctx_owns_session = owns_session
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(
|
|
137
|
+
self,
|
|
138
|
+
exc_type: type[BaseException] | None,
|
|
139
|
+
exc: BaseException | None,
|
|
140
|
+
tb: Any,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Exit the runtime context and close owned sessions.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
exc_type : type[BaseException] | None
|
|
148
|
+
Exception type if raised, else ``None``.
|
|
149
|
+
exc : BaseException | None
|
|
150
|
+
Exception instance if raised, else ``None``.
|
|
151
|
+
tb : Any
|
|
152
|
+
Traceback if an exception was raised, else ``None``.
|
|
153
|
+
"""
|
|
154
|
+
if self._ctx_session is None:
|
|
155
|
+
return
|
|
156
|
+
if self._ctx_owns_session:
|
|
157
|
+
try:
|
|
158
|
+
self._ctx_session.close()
|
|
159
|
+
except AttributeError:
|
|
160
|
+
pass
|
|
161
|
+
self._ctx_session = None
|
|
162
|
+
self._ctx_owns_session = False
|
|
163
|
+
|
|
164
|
+
# -- Instance Methods -- #
|
|
165
|
+
|
|
166
|
+
def get(
|
|
167
|
+
self,
|
|
168
|
+
url: str,
|
|
169
|
+
request_callable: Callable[..., JSONData] | None = None,
|
|
170
|
+
**kwargs: Any,
|
|
171
|
+
) -> JSONData:
|
|
172
|
+
"""
|
|
173
|
+
Perform a GET request.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
url : str
|
|
178
|
+
Target URL.
|
|
179
|
+
request_callable : Callable[..., JSONData] | None, optional
|
|
180
|
+
Optional callable compatible with ``requests.Session.request``.
|
|
181
|
+
**kwargs : Any
|
|
182
|
+
Additional keyword arguments for the request.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
JSONData
|
|
187
|
+
Parsed JSON response data.
|
|
188
|
+
"""
|
|
189
|
+
return self.request(
|
|
190
|
+
'GET',
|
|
191
|
+
url,
|
|
192
|
+
request_callable=request_callable,
|
|
193
|
+
**kwargs,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def post(
|
|
197
|
+
self,
|
|
198
|
+
url: str,
|
|
199
|
+
request_callable: Callable[..., JSONData] | None = None,
|
|
200
|
+
**kwargs: Any,
|
|
201
|
+
) -> JSONData:
|
|
202
|
+
"""
|
|
203
|
+
Perform a POST request.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
url : str
|
|
208
|
+
Target URL.
|
|
209
|
+
request_callable : Callable[..., JSONData] | None, optional
|
|
210
|
+
Optional callable compatible with ``requests.Session.request``.
|
|
211
|
+
**kwargs : Any
|
|
212
|
+
Additional keyword arguments for the request.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
JSONData
|
|
217
|
+
Parsed JSON response data.
|
|
218
|
+
"""
|
|
219
|
+
return self.request(
|
|
220
|
+
'POST',
|
|
221
|
+
url,
|
|
222
|
+
request_callable=request_callable,
|
|
223
|
+
**kwargs,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def request(
|
|
227
|
+
self,
|
|
228
|
+
method: str,
|
|
229
|
+
url: str,
|
|
230
|
+
*,
|
|
231
|
+
request_callable: Callable[..., JSONData] | None = None,
|
|
232
|
+
**kw: Any,
|
|
233
|
+
) -> JSONData:
|
|
234
|
+
"""
|
|
235
|
+
Perform a request with retries.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
method : str
|
|
240
|
+
HTTP method (e.g., 'GET', 'POST').
|
|
241
|
+
url : str
|
|
242
|
+
Target URL.
|
|
243
|
+
request_callable : Callable[..., JSONData] | None, optional
|
|
244
|
+
Optional callable compatible with ``requests.Session.request``.
|
|
245
|
+
**kw : Any
|
|
246
|
+
Additional keyword arguments for the request.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
JSONData
|
|
251
|
+
Parsed JSON response data.
|
|
252
|
+
|
|
253
|
+
Raises
|
|
254
|
+
------
|
|
255
|
+
ApiAuthError
|
|
256
|
+
If authentication fails (HTTP 401 or 403) and retries are
|
|
257
|
+
exhausted.
|
|
258
|
+
ApiRequestError
|
|
259
|
+
If the request ultimately fails for a non-authentication reason.
|
|
260
|
+
"""
|
|
261
|
+
method_normalized = self._normalize_http_method(method)
|
|
262
|
+
|
|
263
|
+
call_kwargs = dict(kw)
|
|
264
|
+
supplied_timeout = call_kwargs.pop('timeout', _MISSING)
|
|
265
|
+
timeout = self._resolve_timeout(supplied_timeout)
|
|
266
|
+
user_session = call_kwargs.pop('session', None)
|
|
267
|
+
session, owns_session = self._resolve_session_for_call(user_session)
|
|
268
|
+
fetch = partial(
|
|
269
|
+
self.request_once,
|
|
270
|
+
method_normalized,
|
|
271
|
+
session=session,
|
|
272
|
+
timeout=timeout,
|
|
273
|
+
request_callable=request_callable,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
policy = self.retry
|
|
278
|
+
if not policy:
|
|
279
|
+
try:
|
|
280
|
+
return fetch(url, **call_kwargs)
|
|
281
|
+
except requests.RequestException as exc: # pragma: no cover
|
|
282
|
+
status = getattr(
|
|
283
|
+
getattr(exc, 'response', None),
|
|
284
|
+
'status_code',
|
|
285
|
+
None,
|
|
286
|
+
)
|
|
287
|
+
if status in {401, 403}:
|
|
288
|
+
raise ApiAuthError(
|
|
289
|
+
url=url,
|
|
290
|
+
status=status,
|
|
291
|
+
attempts=1,
|
|
292
|
+
retried=False,
|
|
293
|
+
retry_policy=None,
|
|
294
|
+
cause=exc,
|
|
295
|
+
) from exc
|
|
296
|
+
raise ApiRequestError(
|
|
297
|
+
url=url,
|
|
298
|
+
status=status,
|
|
299
|
+
attempts=1,
|
|
300
|
+
retried=False,
|
|
301
|
+
retry_policy=None,
|
|
302
|
+
cause=exc,
|
|
303
|
+
) from exc
|
|
304
|
+
|
|
305
|
+
retry_mgr = RetryManager(
|
|
306
|
+
policy=policy,
|
|
307
|
+
retry_network_errors=self.retry_network_errors,
|
|
308
|
+
cap=self.retry_cap,
|
|
309
|
+
)
|
|
310
|
+
return retry_mgr.run_with_retry(fetch, url, **call_kwargs)
|
|
311
|
+
finally:
|
|
312
|
+
if owns_session and session is not None:
|
|
313
|
+
try:
|
|
314
|
+
session.close()
|
|
315
|
+
except AttributeError: # pragma: no cover - defensive
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
def request_once(
|
|
319
|
+
self,
|
|
320
|
+
method: str,
|
|
321
|
+
url: str,
|
|
322
|
+
*,
|
|
323
|
+
session: requests.Session | None,
|
|
324
|
+
timeout: TimeoutInput,
|
|
325
|
+
request_callable: Callable[..., JSONData] | None = None,
|
|
326
|
+
**kwargs: Any,
|
|
327
|
+
) -> JSONData:
|
|
328
|
+
"""
|
|
329
|
+
Perform a single request without retries.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
method : str
|
|
334
|
+
HTTP method (e.g., 'GET', 'POST').
|
|
335
|
+
url : str
|
|
336
|
+
Target URL.
|
|
337
|
+
session : requests.Session | None
|
|
338
|
+
Optional HTTP session to use.
|
|
339
|
+
timeout : TimeoutInput
|
|
340
|
+
Timeout for the request (seconds or ``(connect, read)`` tuple).
|
|
341
|
+
request_callable : Callable[..., JSONData] | None, optional
|
|
342
|
+
Optional custom request function.
|
|
343
|
+
**kwargs : Any
|
|
344
|
+
Additional keyword arguments for the request.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
JSONData
|
|
349
|
+
Parsed JSON response data.
|
|
350
|
+
"""
|
|
351
|
+
method_normalized = self._normalize_http_method(method)
|
|
352
|
+
if request_callable is not None:
|
|
353
|
+
return request_callable(
|
|
354
|
+
method_normalized,
|
|
355
|
+
url,
|
|
356
|
+
session=session,
|
|
357
|
+
timeout=timeout,
|
|
358
|
+
**kwargs,
|
|
359
|
+
)
|
|
360
|
+
response = self._send_http_request(
|
|
361
|
+
method_normalized,
|
|
362
|
+
url,
|
|
363
|
+
session=session,
|
|
364
|
+
timeout=timeout,
|
|
365
|
+
**kwargs,
|
|
366
|
+
)
|
|
367
|
+
response.raise_for_status()
|
|
368
|
+
return self._parse_response_payload(response)
|
|
369
|
+
|
|
370
|
+
# -- Internal Instance Methods -- #
|
|
371
|
+
|
|
372
|
+
def _build_adapter_session(self) -> requests.Session | None:
|
|
373
|
+
"""
|
|
374
|
+
Build a session configured with HTTP adapters when provided.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
requests.Session | None
|
|
379
|
+
Configured session with HTTP adapters, or ``None``.
|
|
380
|
+
"""
|
|
381
|
+
if not self.session_adapters:
|
|
382
|
+
return None
|
|
383
|
+
try:
|
|
384
|
+
return build_session_with_adapters(tuple(self.session_adapters))
|
|
385
|
+
except (ValueError, TypeError, AttributeError):
|
|
386
|
+
return requests.Session()
|
|
387
|
+
|
|
388
|
+
def _instantiate_session(self) -> tuple[requests.Session | None, bool]:
|
|
389
|
+
"""
|
|
390
|
+
Create a session from factory/adapters when available.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
tuple[requests.Session | None, bool]
|
|
395
|
+
Pair of ``(session, owns_session)`` where ``owns_session``
|
|
396
|
+
indicates whether this manager is responsible for closing the
|
|
397
|
+
session after the request completes.
|
|
398
|
+
"""
|
|
399
|
+
if self.session_factory is not None:
|
|
400
|
+
try:
|
|
401
|
+
session = self.session_factory()
|
|
402
|
+
except (RuntimeError, TypeError, ValueError): # pragma: no cover
|
|
403
|
+
return None, False
|
|
404
|
+
if session is not None:
|
|
405
|
+
return session, True
|
|
406
|
+
return None, False
|
|
407
|
+
adapter_session = self._build_adapter_session()
|
|
408
|
+
if adapter_session is not None:
|
|
409
|
+
return adapter_session, True
|
|
410
|
+
return None, False
|
|
411
|
+
|
|
412
|
+
def _parse_response_payload(
|
|
413
|
+
self,
|
|
414
|
+
response: Response,
|
|
415
|
+
) -> JSONData:
|
|
416
|
+
"""
|
|
417
|
+
Parse the response payload into JSONData.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
response : Response
|
|
422
|
+
The HTTP response object.
|
|
423
|
+
|
|
424
|
+
Returns
|
|
425
|
+
-------
|
|
426
|
+
JSONData
|
|
427
|
+
Parsed JSON response data.
|
|
428
|
+
"""
|
|
429
|
+
content_type = response.headers.get('content-type', '').lower()
|
|
430
|
+
if 'application/json' in content_type:
|
|
431
|
+
try:
|
|
432
|
+
payload: Any = response.json()
|
|
433
|
+
except ValueError:
|
|
434
|
+
return {
|
|
435
|
+
'content': response.text,
|
|
436
|
+
'content_type': content_type,
|
|
437
|
+
}
|
|
438
|
+
if isinstance(payload, dict):
|
|
439
|
+
return cast(JSONDict, payload)
|
|
440
|
+
if isinstance(payload, list):
|
|
441
|
+
if all(isinstance(item, dict) for item in payload):
|
|
442
|
+
return cast(JSONData, payload)
|
|
443
|
+
return [{'value': item} for item in payload]
|
|
444
|
+
return {'value': payload}
|
|
445
|
+
return {
|
|
446
|
+
'content': response.text,
|
|
447
|
+
'content_type': content_type,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
def _resolve_request_callable(
|
|
451
|
+
self,
|
|
452
|
+
session: requests.Session | None,
|
|
453
|
+
) -> Callable[..., Response]:
|
|
454
|
+
"""
|
|
455
|
+
Resolve the request callable from the given session or default to
|
|
456
|
+
:func:`requests.request`.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
session : requests.Session | None
|
|
461
|
+
Optional session object to use for the request.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
Callable[..., Response]
|
|
466
|
+
Callable to perform the HTTP request.
|
|
467
|
+
|
|
468
|
+
Raises
|
|
469
|
+
------
|
|
470
|
+
TypeError
|
|
471
|
+
If the provided session does not have a callable 'request' method.
|
|
472
|
+
"""
|
|
473
|
+
if session is not None:
|
|
474
|
+
request_callable = getattr(session, 'request', None)
|
|
475
|
+
if callable(request_callable):
|
|
476
|
+
return request_callable
|
|
477
|
+
raise TypeError('Session must expose a callable "request" method')
|
|
478
|
+
return requests.request
|
|
479
|
+
|
|
480
|
+
def _resolve_timeout(
|
|
481
|
+
self,
|
|
482
|
+
timeout: TimeoutInput | object,
|
|
483
|
+
) -> TimeoutInput:
|
|
484
|
+
"""
|
|
485
|
+
Resolve the timeout value, defaulting to the instance's
|
|
486
|
+
``default_timeout`` if not provided.
|
|
487
|
+
|
|
488
|
+
Parameters
|
|
489
|
+
----------
|
|
490
|
+
timeout : TimeoutInput | object
|
|
491
|
+
Supplied timeout (seconds or ``(connect, read)`` tuple) or
|
|
492
|
+
sentinel.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
TimeoutInput
|
|
497
|
+
Resolved timeout value (seconds or ``(connect, read)`` tuple).
|
|
498
|
+
"""
|
|
499
|
+
if timeout is _MISSING:
|
|
500
|
+
return cast(TimeoutInput, self.default_timeout)
|
|
501
|
+
return cast(TimeoutInput, timeout)
|
|
502
|
+
|
|
503
|
+
def _resolve_session_for_call(
|
|
504
|
+
self,
|
|
505
|
+
explicit: requests.Session | None,
|
|
506
|
+
) -> tuple[requests.Session | None, bool]:
|
|
507
|
+
"""
|
|
508
|
+
Determine which session should service the current request.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
explicit : requests.Session | None
|
|
513
|
+
Session provided directly by the caller.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
tuple[requests.Session | None, bool]
|
|
518
|
+
Pair of ``(session, owns_session)`` where ``owns_session``
|
|
519
|
+
indicates whether this manager is responsible for closing the
|
|
520
|
+
session after the request completes.
|
|
521
|
+
"""
|
|
522
|
+
if explicit is not None:
|
|
523
|
+
return explicit, False
|
|
524
|
+
if self._ctx_session is not None:
|
|
525
|
+
return self._ctx_session, False
|
|
526
|
+
if self.session is not None:
|
|
527
|
+
return self.session, False
|
|
528
|
+
session, owns_session = self._instantiate_session()
|
|
529
|
+
if session is not None:
|
|
530
|
+
return session, owns_session
|
|
531
|
+
return None, False
|
|
532
|
+
|
|
533
|
+
def _send_http_request(
|
|
534
|
+
self,
|
|
535
|
+
method: str,
|
|
536
|
+
url: str,
|
|
537
|
+
*,
|
|
538
|
+
session: requests.Session | None,
|
|
539
|
+
timeout: TimeoutInput,
|
|
540
|
+
**kwargs: Any,
|
|
541
|
+
) -> Response:
|
|
542
|
+
"""
|
|
543
|
+
Send the actual HTTP request using the specified method and URL.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
method : str
|
|
548
|
+
HTTP method to use for the request.
|
|
549
|
+
url : str
|
|
550
|
+
Target URL for the request.
|
|
551
|
+
session : requests.Session | None
|
|
552
|
+
Optional session object to use for the request.
|
|
553
|
+
timeout : TimeoutInput
|
|
554
|
+
Timeout value (seconds or ``(connect, read)`` tuple).
|
|
555
|
+
**kwargs : Any
|
|
556
|
+
Additional keyword arguments for the request.
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
Response
|
|
561
|
+
The HTTP response object.
|
|
562
|
+
"""
|
|
563
|
+
call_kwargs = {**kwargs, 'timeout': timeout}
|
|
564
|
+
method_normalized = self._normalize_http_method(method)
|
|
565
|
+
request_callable = self._resolve_request_callable(session)
|
|
566
|
+
return request_callable(method_normalized, url, **call_kwargs)
|
|
567
|
+
|
|
568
|
+
# -- Internal Static Methods -- #
|
|
569
|
+
|
|
570
|
+
@staticmethod
|
|
571
|
+
def _normalize_http_method(
|
|
572
|
+
method: str | None,
|
|
573
|
+
) -> str:
|
|
574
|
+
"""
|
|
575
|
+
Normalize the HTTP method to uppercase, defaulting to 'GET' if not
|
|
576
|
+
provided.
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
method : str | None
|
|
581
|
+
HTTP method to normalize.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
str
|
|
586
|
+
Normalized HTTP method.
|
|
587
|
+
"""
|
|
588
|
+
candidate = (method or '').strip().upper()
|
|
589
|
+
return candidate or 'GET'
|