core-https 1.1.5__tar.gz → 1.1.7__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.
- {core_https-1.1.5 → core_https-1.1.7}/PKG-INFO +9 -6
- {core_https-1.1.5 → core_https-1.1.7}/core_https/exceptions.py +10 -1
- core_https-1.1.7/core_https/requesters/aiohttp_.py +178 -0
- core_https-1.1.7/core_https/requesters/base.py +161 -0
- core_https-1.1.7/core_https/requesters/requests_.py +116 -0
- core_https-1.1.7/core_https/requesters/urllib3_.py +117 -0
- core_https-1.1.7/core_https/tests/__init__.py +0 -0
- core_https-1.1.7/core_https/tests/aiohttp_.py +106 -0
- core_https-1.1.7/core_https/tests/base.py +12 -0
- core_https-1.1.7/core_https/tests/decorators.py +180 -0
- core_https-1.1.7/core_https/tests/requests_.py +75 -0
- core_https-1.1.7/core_https/tests/urllib3_.py +189 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https/utils.py +12 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https.egg-info/PKG-INFO +9 -6
- {core_https-1.1.5 → core_https-1.1.7}/core_https.egg-info/SOURCES.txt +9 -1
- {core_https-1.1.5 → core_https-1.1.7}/core_https.egg-info/requires.txt +10 -2
- {core_https-1.1.5 → core_https-1.1.7}/pyproject.toml +11 -8
- core_https-1.1.5/core_https/requesters/base.py +0 -24
- core_https-1.1.5/core_https/requesters/url_lib3.py +0 -34
- {core_https-1.1.5 → core_https-1.1.7}/LICENSE +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/README.md +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https/__init__.py +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https/requesters/__init__.py +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https.egg-info/dependency_links.txt +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/core_https.egg-info/top_level.txt +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/setup.cfg +0 -0
- {core_https-1.1.5 → core_https-1.1.7}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-https
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: This project/library contains common elements related to HTTP & API services...
|
|
5
5
|
Author-email: Alejandro Cora González <alek.cora.glez@gmail.com>
|
|
6
6
|
Maintainer: Alejandro Cora González
|
|
@@ -16,22 +16,25 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
16
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
17
|
Classifier: Topic :: Utilities
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
-
Requires-Python: >=3.
|
|
25
|
+
Requires-Python: >=3.8
|
|
27
26
|
Description-Content-Type: text/markdown
|
|
28
27
|
License-File: LICENSE
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: aiohttp>=3.12.0; python_version >= "3.9"
|
|
29
|
+
Requires-Dist: aiohttp>=3.8.0; python_version >= "3.7" and python_version < "3.9"
|
|
30
|
+
Requires-Dist: core-mixins>=2.0.3
|
|
30
31
|
Requires-Dist: core-tests>=1.1.0
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
32
|
+
Requires-Dist: requests>=2.32.3; python_version >= "3.8"
|
|
33
|
+
Requires-Dist: requests>=2.20.0; python_version >= "3.7" and python_version < "3.8"
|
|
33
34
|
Requires-Dist: typing-extensions>=4.8.0; python_version >= "3.8" and python_version < "3.11"
|
|
34
35
|
Requires-Dist: typing-extensions>=4.2.0; python_version >= "3.7" and python_version < "3.8"
|
|
36
|
+
Requires-Dist: urllib3>=2.2.3; python_version >= "3.8"
|
|
37
|
+
Requires-Dist: urllib3>=1.26.20; python_version >= "3.7" and python_version < "3.8"
|
|
35
38
|
Provides-Extra: docs
|
|
36
39
|
Requires-Dist: pydata-sphinx-theme==0.16.1; extra == "docs"
|
|
37
40
|
Requires-Dist: Sphinx==8.2.3; extra == "docs"
|
|
@@ -27,7 +27,7 @@ class AuthenticationException(ServiceException):
|
|
|
27
27
|
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
|
-
status_code: int =
|
|
30
|
+
status_code: int = 401,
|
|
31
31
|
details: str = "Unauthorized"
|
|
32
32
|
) -> None:
|
|
33
33
|
super().__init__(status_code=status_code, details=details)
|
|
@@ -56,3 +56,12 @@ class RateLimitException(ServiceException):
|
|
|
56
56
|
details: str = "Too Many Requests"
|
|
57
57
|
) -> None:
|
|
58
58
|
super().__init__(status_code=status_code, details=details)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RetryableException(InternalServerError):
|
|
62
|
+
"""
|
|
63
|
+
Special case for exceptions that are considered retryable
|
|
64
|
+
like: 429, 502, 503 or 504. It's useful when a retry mechanism
|
|
65
|
+
is used, and you want to capture one type of error to trigger
|
|
66
|
+
the retry.
|
|
67
|
+
"""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from typing import Self
|
|
12
|
+
|
|
13
|
+
except ImportError:
|
|
14
|
+
from typing_extensions import Self
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from http import HTTPMethod
|
|
18
|
+
|
|
19
|
+
except ImportError:
|
|
20
|
+
from core_https.utils import HTTPMethod
|
|
21
|
+
|
|
22
|
+
from aiohttp import (
|
|
23
|
+
ClientResponse,
|
|
24
|
+
ClientResponseError,
|
|
25
|
+
ClientSession,
|
|
26
|
+
ClientTimeout,
|
|
27
|
+
TCPConnector,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .base import IRequester
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AioHttpRequester(IRequester):
|
|
34
|
+
"""
|
|
35
|
+
It uses `aiohttp` to make the requests.
|
|
36
|
+
|
|
37
|
+
.. code-block:: python
|
|
38
|
+
|
|
39
|
+
import aiohttp
|
|
40
|
+
from core_https.requesters.aiohttp_ import AioHttpRequester
|
|
41
|
+
from core_https.utils import HTTPMethod
|
|
42
|
+
|
|
43
|
+
requester: AioHttpRequester = AioHttpRequester(raise_for_status=True)
|
|
44
|
+
|
|
45
|
+
async def get():
|
|
46
|
+
# This is optional as the client creates one session for you if not provided.
|
|
47
|
+
session = aiohttp.ClientSession()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = await requester.request(
|
|
51
|
+
method=HTTPMethod.GET,
|
|
52
|
+
session=session,
|
|
53
|
+
url=url,
|
|
54
|
+
params={
|
|
55
|
+
"x-api-key": "..."
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return await response.text()
|
|
59
|
+
|
|
60
|
+
except Exception as error:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
finally:
|
|
64
|
+
await session.close()
|
|
65
|
+
|
|
66
|
+
res = asyncio.run(get())
|
|
67
|
+
print(res)
|
|
68
|
+
..
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
session: Optional[ClientSession] = None,
|
|
74
|
+
retries: Optional[int] = 3,
|
|
75
|
+
**kwargs
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
:param session: The session to use for requests.
|
|
79
|
+
:param retries: Retry strategy to apply. Pass zero (0) to avoid retries.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
super().__init__(**kwargs)
|
|
83
|
+
|
|
84
|
+
self._session = session
|
|
85
|
+
self._session_lock = asyncio.Lock()
|
|
86
|
+
self._owns_session = session is None
|
|
87
|
+
self._timeout = ClientTimeout(total=self.timeout)
|
|
88
|
+
self.retries = retries
|
|
89
|
+
|
|
90
|
+
async def __aenter__(self) -> Self:
|
|
91
|
+
await self._ensure_session()
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
95
|
+
await self.close()
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def engine(cls) -> str:
|
|
99
|
+
return "aiohttp"
|
|
100
|
+
|
|
101
|
+
async def _ensure_session(self) -> ClientSession:
|
|
102
|
+
""" If the session doesn't exist, it creates it """
|
|
103
|
+
|
|
104
|
+
if self._session is None:
|
|
105
|
+
async with self._session_lock:
|
|
106
|
+
if self._session is None: # Double-check after acquiring lock...
|
|
107
|
+
self._session = ClientSession(
|
|
108
|
+
timeout=self._timeout,
|
|
109
|
+
connector=TCPConnector(
|
|
110
|
+
limit=self.connector_limit,
|
|
111
|
+
limit_per_host=self.connector_limit_per_host
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
self._owns_session = True
|
|
115
|
+
|
|
116
|
+
return self._session
|
|
117
|
+
|
|
118
|
+
async def request(
|
|
119
|
+
self,
|
|
120
|
+
url: str,
|
|
121
|
+
method: HTTPMethod = HTTPMethod.GET,
|
|
122
|
+
session: Optional[ClientSession] = None,
|
|
123
|
+
headers: Optional[Dict[str, Any]] = None,
|
|
124
|
+
params: Optional[Dict[str, Any]] = None,
|
|
125
|
+
timeout: Optional[float] = None,
|
|
126
|
+
retries: Optional[int] = None,
|
|
127
|
+
backoff_factor: Optional[int] = None,
|
|
128
|
+
**kwargs
|
|
129
|
+
) -> ClientResponse:
|
|
130
|
+
"""
|
|
131
|
+
It makes the request using the session (externally provided or created
|
|
132
|
+
if required) and return the response...
|
|
133
|
+
|
|
134
|
+
:returns: `aiohttp.ClientResponse` object.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
session_ = session or await self._ensure_session()
|
|
138
|
+
kwargs_ = kwargs.copy()
|
|
139
|
+
|
|
140
|
+
if timeout is not None:
|
|
141
|
+
kwargs_["timeout"] = ClientTimeout(total=timeout)
|
|
142
|
+
|
|
143
|
+
retries = retries if retries is not None else self.retries
|
|
144
|
+
attempts = 0
|
|
145
|
+
|
|
146
|
+
backoff_factor = (
|
|
147
|
+
backoff_factor
|
|
148
|
+
if backoff_factor is not None
|
|
149
|
+
else self.backoff_factor if self.backoff_factor is not None
|
|
150
|
+
else 0.5
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
while True:
|
|
154
|
+
attempts += 1
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
response = await session_.request(
|
|
158
|
+
method=str(method),
|
|
159
|
+
url=url,
|
|
160
|
+
headers=headers,
|
|
161
|
+
params=params,
|
|
162
|
+
**kwargs_)
|
|
163
|
+
|
|
164
|
+
if self.raise_for_status:
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
|
|
167
|
+
return response
|
|
168
|
+
|
|
169
|
+
except ClientResponseError as error:
|
|
170
|
+
if attempts > retries:
|
|
171
|
+
self.raise_custom_exception(error.status, error.message)
|
|
172
|
+
|
|
173
|
+
await asyncio.sleep(backoff_factor * attempts)
|
|
174
|
+
|
|
175
|
+
async def close(self):
|
|
176
|
+
if self._session and self._owns_session:
|
|
177
|
+
await self._session.close()
|
|
178
|
+
self._session = None
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from core_mixins.interfaces.factory import IFactory
|
|
9
|
+
|
|
10
|
+
from core_https.exceptions import (
|
|
11
|
+
AuthenticationException,
|
|
12
|
+
AuthorizationException,
|
|
13
|
+
InternalServerError,
|
|
14
|
+
RateLimitException,
|
|
15
|
+
RetryableException,
|
|
16
|
+
ServiceException,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from http import HTTPMethod
|
|
21
|
+
|
|
22
|
+
except ImportError:
|
|
23
|
+
from core_https.utils import HTTPMethod
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IRequester(IFactory, ABC):
|
|
27
|
+
""" Base interface for all type of HTTP requesters """
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
encoding: str = "utf-8",
|
|
32
|
+
raise_for_status: bool = False,
|
|
33
|
+
retries: Optional[Any] = None,
|
|
34
|
+
backoff_factor: Optional[int] = None,
|
|
35
|
+
connector_limit: int = 100,
|
|
36
|
+
connector_limit_per_host: int = 30,
|
|
37
|
+
timeout: int = 10,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
:param encoding: The encoding to use when decoding.
|
|
41
|
+
:param raise_for_status: If True, `raise_for_status` will be executed.
|
|
42
|
+
:param connector_limit: Maximum number of connections in pool.
|
|
43
|
+
:param connector_limit_per_host: Maximum connections per host.
|
|
44
|
+
:param timeout: How many seconds to wait for the server to send data.
|
|
45
|
+
:param retries: Retry strategy to apply. Provide it here, to avoid passing it to each request.
|
|
46
|
+
:param backoff_factor: Delay between successive retry attempts.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if timeout <= 0:
|
|
50
|
+
raise ValueError("`timeout` must be positive!")
|
|
51
|
+
|
|
52
|
+
if connector_limit <= 0:
|
|
53
|
+
raise ValueError("`connector_limit` must be positive!")
|
|
54
|
+
|
|
55
|
+
if connector_limit_per_host <= 0:
|
|
56
|
+
raise ValueError("`connector_limit_per_host` must be positive!")
|
|
57
|
+
|
|
58
|
+
if connector_limit_per_host > connector_limit:
|
|
59
|
+
raise ValueError("`connector_limit_per_host` cannot exceed `connector_limit`!")
|
|
60
|
+
|
|
61
|
+
self.backoff_factor = backoff_factor
|
|
62
|
+
self.retries = retries
|
|
63
|
+
|
|
64
|
+
self.encoding = encoding
|
|
65
|
+
self.raise_for_status = raise_for_status
|
|
66
|
+
self.timeout = timeout
|
|
67
|
+
|
|
68
|
+
self.connector_limit = connector_limit
|
|
69
|
+
self.connector_limit_per_host = connector_limit_per_host
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def registration_key(cls) -> str:
|
|
73
|
+
return cls.engine()
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def engine(cls) -> str:
|
|
78
|
+
""" Must return the engine name like: `requests` or `urllib3` """
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def request(
|
|
82
|
+
self,
|
|
83
|
+
url: str,
|
|
84
|
+
method: HTTPMethod,
|
|
85
|
+
headers: Optional[Dict[str, str]] = None,
|
|
86
|
+
retries: Optional[Any] = None,
|
|
87
|
+
backoff_factor: Optional[int] = None,
|
|
88
|
+
**kwargs # Each `engine`have its own attributes...
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Makes the request and returns the response.
|
|
92
|
+
|
|
93
|
+
:param url: The url for the request.
|
|
94
|
+
:param method: The method (verb) for the request.
|
|
95
|
+
:param headers: The headers to add.
|
|
96
|
+
|
|
97
|
+
:param retries:
|
|
98
|
+
It defines the retry strategy. Pass `False` if you don't want to use a
|
|
99
|
+
retry at all, otherwise a default one will be provided.
|
|
100
|
+
|
|
101
|
+
:param backoff_factor: Delay between successive retry attempts.
|
|
102
|
+
|
|
103
|
+
:param kwargs:
|
|
104
|
+
Each concrete implementation will have its own attributes. Depending on
|
|
105
|
+
the requester you are using, you can pass different attributes.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def _get_response_encoding(
|
|
109
|
+
self,
|
|
110
|
+
headers: Dict[str, str],
|
|
111
|
+
default="utf-8",
|
|
112
|
+
) -> str:
|
|
113
|
+
headers_ = {
|
|
114
|
+
k.lower(): v
|
|
115
|
+
for k, v in headers.items()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# First trying "charset" header directly (rare)...
|
|
119
|
+
charset = headers_.get("charset")
|
|
120
|
+
if charset:
|
|
121
|
+
return charset.strip()
|
|
122
|
+
|
|
123
|
+
# Then checking `Content-Type` for "charset="
|
|
124
|
+
content_type = headers_.get("content-type", "")
|
|
125
|
+
match = re.search(r"charset=([^\s;]+)", content_type, re.IGNORECASE)
|
|
126
|
+
if match:
|
|
127
|
+
return match.group(1).strip()
|
|
128
|
+
|
|
129
|
+
return self.encoding or default
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def raise_custom_exception(status_code: int, details: str):
|
|
133
|
+
"""
|
|
134
|
+
:raises: `ServiceException` for other 4XX status_code.
|
|
135
|
+
:raises: `AuthenticationException` for status_code == 401.
|
|
136
|
+
:raises: `AuthorizationException` for status_code == 403.
|
|
137
|
+
:raises: `RateLimitException` for status_code == 429.
|
|
138
|
+
:raises: `InternalServerError` for status_code >= 500.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
error_cls = ServiceException
|
|
142
|
+
|
|
143
|
+
if status_code == 401:
|
|
144
|
+
error_cls = AuthenticationException
|
|
145
|
+
|
|
146
|
+
elif status_code == 403:
|
|
147
|
+
error_cls = AuthorizationException
|
|
148
|
+
|
|
149
|
+
elif status_code == 429:
|
|
150
|
+
error_cls = RateLimitException
|
|
151
|
+
|
|
152
|
+
elif status_code in (429, 502, 503, 504):
|
|
153
|
+
error_cls = RetryableException
|
|
154
|
+
|
|
155
|
+
elif status_code >= 500:
|
|
156
|
+
error_cls = InternalServerError
|
|
157
|
+
|
|
158
|
+
raise error_cls(
|
|
159
|
+
status_code=status_code,
|
|
160
|
+
details=details,
|
|
161
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
import urllib3
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
|
|
9
|
+
from core_https.requesters.base import IRequester
|
|
10
|
+
from core_https.utils import HTTPMethod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RequestsRequester(IRequester):
|
|
14
|
+
"""
|
|
15
|
+
It uses `requests` to make the requests.
|
|
16
|
+
|
|
17
|
+
.. code-block:: python
|
|
18
|
+
|
|
19
|
+
from core_https.requesters.requests_ import RequestsRequester
|
|
20
|
+
from core_https.utils import HTTPMethod
|
|
21
|
+
|
|
22
|
+
requester: RequestsRequester = RequestsRequester()
|
|
23
|
+
|
|
24
|
+
response = requester.request(
|
|
25
|
+
method=HTTPMethod.GET,
|
|
26
|
+
url=url,
|
|
27
|
+
params={
|
|
28
|
+
"x-api-key": "..."
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
print(response.json())
|
|
32
|
+
..
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
session: Optional[requests.Session] = None,
|
|
38
|
+
retries: Optional[urllib3.Retry] = None,
|
|
39
|
+
backoff_factor: Optional[int] = None,
|
|
40
|
+
**kwargs
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
:param session: Session to use for the requests.
|
|
44
|
+
:param retries: Retry strategy to apply. Pass zero (0) to avoid retries.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
super().__init__(**kwargs)
|
|
48
|
+
self.retries = retries
|
|
49
|
+
|
|
50
|
+
backoff_factor = (
|
|
51
|
+
backoff_factor
|
|
52
|
+
if backoff_factor is not None
|
|
53
|
+
else self.backoff_factor if self.backoff_factor is not None
|
|
54
|
+
else 0.5
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if self.retries is None:
|
|
58
|
+
self.retries = urllib3.Retry(
|
|
59
|
+
status_forcelist=[429, 502, 503, 504],
|
|
60
|
+
backoff_factor=backoff_factor,
|
|
61
|
+
total=3)
|
|
62
|
+
|
|
63
|
+
adapter = HTTPAdapter(
|
|
64
|
+
pool_connections=self.connector_limit,
|
|
65
|
+
pool_maxsize=self.connector_limit_per_host,
|
|
66
|
+
max_retries=self.retries)
|
|
67
|
+
|
|
68
|
+
if session is None:
|
|
69
|
+
session = requests.Session()
|
|
70
|
+
session.mount("https://", adapter)
|
|
71
|
+
session.mount("http://", adapter)
|
|
72
|
+
|
|
73
|
+
self._session = session
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def engine(cls) -> str:
|
|
77
|
+
return "requests"
|
|
78
|
+
|
|
79
|
+
def request(
|
|
80
|
+
self,
|
|
81
|
+
url: str,
|
|
82
|
+
method: HTTPMethod = HTTPMethod.GET,
|
|
83
|
+
session: Optional[requests.Session] = None,
|
|
84
|
+
headers: Optional[Dict] = None,
|
|
85
|
+
params: Optional[Dict] = None,
|
|
86
|
+
timeout: Optional[float] = None,
|
|
87
|
+
**kwargs
|
|
88
|
+
) -> requests.Response:
|
|
89
|
+
"""
|
|
90
|
+
It makes the request using the session (externally provided or created
|
|
91
|
+
if required) and return the response...
|
|
92
|
+
|
|
93
|
+
:returns: `requests.Response` object.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
session_ = session or self._session
|
|
97
|
+
if params is None:
|
|
98
|
+
params = {}
|
|
99
|
+
|
|
100
|
+
response = session_.request(
|
|
101
|
+
method=method.value,
|
|
102
|
+
url=url,
|
|
103
|
+
headers=headers,
|
|
104
|
+
params=params,
|
|
105
|
+
timeout=timeout,
|
|
106
|
+
**kwargs)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
if self.raise_for_status:
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
|
|
112
|
+
except requests.exceptions.HTTPError:
|
|
113
|
+
status_code, info = response.status_code, response.text
|
|
114
|
+
self.raise_custom_exception(status_code, info)
|
|
115
|
+
|
|
116
|
+
return response
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from http import HTTPMethod
|
|
9
|
+
|
|
10
|
+
except ImportError:
|
|
11
|
+
from core_https.utils import HTTPMethod
|
|
12
|
+
|
|
13
|
+
from urllib3 import BaseHTTPResponse
|
|
14
|
+
from urllib3 import PoolManager
|
|
15
|
+
from urllib3 import Retry
|
|
16
|
+
|
|
17
|
+
from .base import IRequester
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Urllib3Requester(IRequester):
|
|
21
|
+
"""
|
|
22
|
+
It uses `urllib3` to make the requests.
|
|
23
|
+
|
|
24
|
+
.. code-block:: python
|
|
25
|
+
|
|
26
|
+
from core_https.requesters.urllib3_ import Urllib3Requester
|
|
27
|
+
from core_https.utils import HTTPMethod
|
|
28
|
+
|
|
29
|
+
requester: Urllib3Requester = IRequester.get_class(Urllib3Requester.engine())()
|
|
30
|
+
response = requester.request(method=HTTPMethod.GET, url="https://google.com")
|
|
31
|
+
print(response.data.decode())
|
|
32
|
+
..
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
pool_manager: Optional[PoolManager] = None,
|
|
38
|
+
retries: Optional[Retry] = None,
|
|
39
|
+
**kwargs
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
:param pool_manager: The pool manager to use or one will be created.
|
|
43
|
+
:param retries: Retry strategy to apply. Pass zero (0) to avoid retries.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
super().__init__(**kwargs)
|
|
47
|
+
|
|
48
|
+
if not pool_manager:
|
|
49
|
+
pool_manager = PoolManager()
|
|
50
|
+
|
|
51
|
+
self._http: PoolManager = pool_manager
|
|
52
|
+
self.retries = retries
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def engine(cls):
|
|
56
|
+
return "urllib3"
|
|
57
|
+
|
|
58
|
+
def request(
|
|
59
|
+
self,
|
|
60
|
+
url: str,
|
|
61
|
+
method: HTTPMethod = HTTPMethod.GET,
|
|
62
|
+
headers: Optional[Dict] = None,
|
|
63
|
+
fields: Optional[Dict] = None,
|
|
64
|
+
retries: Optional[Retry] = None,
|
|
65
|
+
backoff_factor: Optional[int] = None,
|
|
66
|
+
timeout: Optional[float] = None,
|
|
67
|
+
**kwargs
|
|
68
|
+
) -> BaseHTTPResponse:
|
|
69
|
+
"""
|
|
70
|
+
:raises: `ServiceException` for other 4XX status_code.
|
|
71
|
+
:raises: `AuthenticationException` for status_code == 401.
|
|
72
|
+
:raises: `AuthorizationException` for status_code == 403.
|
|
73
|
+
:raises: `InternalServerError` for status_code >= 500.
|
|
74
|
+
|
|
75
|
+
:returns: `HTTPResponse` object.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
backoff_factor = (
|
|
79
|
+
backoff_factor
|
|
80
|
+
if backoff_factor is not None
|
|
81
|
+
else self.backoff_factor if self.backoff_factor is not None
|
|
82
|
+
else 0.5
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
retries_ = retries or self.retries
|
|
86
|
+
if retries_ is None:
|
|
87
|
+
retries_ = Retry(
|
|
88
|
+
status_forcelist=[429, 502, 503, 504],
|
|
89
|
+
backoff_factor=backoff_factor,
|
|
90
|
+
total=3)
|
|
91
|
+
|
|
92
|
+
response = self._http.request(
|
|
93
|
+
method=str(method),
|
|
94
|
+
url=url,
|
|
95
|
+
headers=headers,
|
|
96
|
+
fields=fields,
|
|
97
|
+
timeout=timeout or self.timeout,
|
|
98
|
+
retries=retries_,
|
|
99
|
+
**kwargs)
|
|
100
|
+
|
|
101
|
+
status_code = response.status
|
|
102
|
+
|
|
103
|
+
if status_code >= 400 and self.raise_for_status:
|
|
104
|
+
info = response.data.decode(self._get_response_encoding(response.headers))
|
|
105
|
+
headers_ = {k.lower(): v for k, v in response.headers.items()}
|
|
106
|
+
|
|
107
|
+
if "application/json" in headers_.get("content-type", ""):
|
|
108
|
+
if hasattr(response, "json"):
|
|
109
|
+
info = response.json()
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
with suppress(json.JSONDecodeError):
|
|
113
|
+
info = json.loads(info)
|
|
114
|
+
|
|
115
|
+
self.raise_custom_exception(status_code, info)
|
|
116
|
+
|
|
117
|
+
return response
|
|
File without changes
|