core-https 2.0.3__tar.gz → 3.0.0__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-2.0.3 → core_https-3.0.0}/PKG-INFO +12 -4
- {core_https-2.0.3 → core_https-3.0.0}/README.rst +6 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/__init__.py +3 -1
- {core_https-2.0.3 → core_https-3.0.0}/core_https/exceptions.py +9 -2
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_.py +22 -6
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_rate_limit.py +9 -4
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_throttle.py +13 -3
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/base.py +9 -5
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/requests_.py +9 -8
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/urllib3_.py +11 -6
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/decorators.py +12 -6
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/requests_.py +2 -1
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/urllib3_.py +6 -7
- {core_https-2.0.3 → core_https-3.0.0}/core_https/utils.py +16 -14
- {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/PKG-INFO +12 -4
- {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/requires.txt +3 -3
- core_https-3.0.0/pyproject.toml +133 -0
- core_https-2.0.3/pyproject.toml +0 -68
- {core_https-2.0.3 → core_https-3.0.0}/LICENSE +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/py.typed +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/__init__.py +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/__init__.py +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/aiohttp_.py +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/base.py +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/SOURCES.txt +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/dependency_links.txt +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/top_level.txt +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/setup.cfg +0 -0
- {core_https-2.0.3 → core_https-3.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-https
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
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
|
|
@@ -21,17 +21,19 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
26
|
Requires-Python: >=3.9
|
|
25
27
|
Description-Content-Type: text/x-rst
|
|
26
28
|
License-File: LICENSE
|
|
27
29
|
Requires-Dist: aiohttp<4.0.0,>=3.12.0
|
|
28
|
-
Requires-Dist: core-mixins>=
|
|
30
|
+
Requires-Dist: core-mixins>=3.0.1
|
|
29
31
|
Requires-Dist: requests<3.0.0,>=2.32.3
|
|
30
32
|
Requires-Dist: urllib3<3.0.0,>=2.2.3
|
|
31
33
|
Provides-Extra: dev
|
|
32
34
|
Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "dev"
|
|
33
|
-
Requires-Dist: core-dev-tools>=1.
|
|
34
|
-
Requires-Dist: core-tests>=2.0.
|
|
35
|
+
Requires-Dist: core-dev-tools>=1.2.1; extra == "dev"
|
|
36
|
+
Requires-Dist: core-tests>=2.0.5; extra == "dev"
|
|
35
37
|
Requires-Dist: types-requests>=2.32.0.20250602; extra == "dev"
|
|
36
38
|
Provides-Extra: extras
|
|
37
39
|
Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "extras"
|
|
@@ -137,7 +139,13 @@ Check tests and coverage
|
|
|
137
139
|
|
|
138
140
|
.. code-block:: shell
|
|
139
141
|
|
|
142
|
+
# Unit tests (mocked, no network):
|
|
140
143
|
python manager.py run-tests
|
|
144
|
+
|
|
145
|
+
# Functional tests (real HTTP requests — requires network):
|
|
146
|
+
python manager.py run-tests --test-type functional --pattern "*.py"
|
|
147
|
+
|
|
148
|
+
# Coverage:
|
|
141
149
|
python manager.py run-coverage
|
|
142
150
|
|
|
143
151
|
|
|
@@ -98,7 +98,13 @@ Check tests and coverage
|
|
|
98
98
|
|
|
99
99
|
.. code-block:: shell
|
|
100
100
|
|
|
101
|
+
# Unit tests (mocked, no network):
|
|
101
102
|
python manager.py run-tests
|
|
103
|
+
|
|
104
|
+
# Functional tests (real HTTP requests — requires network):
|
|
105
|
+
python manager.py run-tests --test-type functional --pattern "*.py"
|
|
106
|
+
|
|
107
|
+
# Coverage:
|
|
102
108
|
python manager.py run-coverage
|
|
103
109
|
|
|
104
110
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Public API for the core_https package: HTTP status codes and status info enum."""
|
|
4
|
+
|
|
3
5
|
from core_mixins import StrEnum
|
|
4
6
|
|
|
5
7
|
try:
|
|
6
8
|
from http import HTTPStatus as _HTTPStatus
|
|
7
9
|
|
|
8
10
|
except ImportError:
|
|
9
|
-
from .utils import HTTPStatus as _HTTPStatus
|
|
11
|
+
from .utils import HTTPStatus as _HTTPStatus
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
@@ -49,7 +49,14 @@ See Also:
|
|
|
49
49
|
- HTTP status codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
from typing import
|
|
52
|
+
from typing import TypedDict
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ErrorInfo(TypedDict):
|
|
56
|
+
"""Structured error payload returned by :meth:`InternalServerError.get_error_info`."""
|
|
57
|
+
|
|
58
|
+
type: str
|
|
59
|
+
details: str
|
|
53
60
|
|
|
54
61
|
|
|
55
62
|
class InternalServerError(Exception):
|
|
@@ -75,7 +82,7 @@ class InternalServerError(Exception):
|
|
|
75
82
|
self.status_code = status_code
|
|
76
83
|
self.details = details
|
|
77
84
|
|
|
78
|
-
def get_error_info(self) ->
|
|
85
|
+
def get_error_info(self) -> ErrorInfo:
|
|
79
86
|
"""
|
|
80
87
|
Get structured error information for logging or serialization.
|
|
81
88
|
:returns: Dictionary containing error type and details.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Asynchronous HTTP requester implementation backed by aiohttp."""
|
|
4
|
+
|
|
3
5
|
from __future__ import annotations
|
|
4
6
|
|
|
5
7
|
import asyncio
|
|
8
|
+
from contextlib import suppress
|
|
6
9
|
from typing import Any
|
|
7
10
|
from typing import Dict
|
|
8
11
|
from typing import Optional
|
|
@@ -16,6 +19,7 @@ from aiohttp import (
|
|
|
16
19
|
)
|
|
17
20
|
from core_mixins import Self
|
|
18
21
|
|
|
22
|
+
from core_https.exceptions import RetryableException
|
|
19
23
|
from .base import HTTPMethod
|
|
20
24
|
from .base import IRequester
|
|
21
25
|
|
|
@@ -106,7 +110,7 @@ class AioHttpRequester(IRequester):
|
|
|
106
110
|
super().__init__(**kwargs)
|
|
107
111
|
|
|
108
112
|
self._session = session
|
|
109
|
-
self._session_lock
|
|
113
|
+
self._session_lock: Optional[asyncio.Lock] = None
|
|
110
114
|
self._owns_session = session is None
|
|
111
115
|
self._timeout = ClientTimeout(total=self.timeout)
|
|
112
116
|
self.retries = retries
|
|
@@ -144,6 +148,9 @@ class AioHttpRequester(IRequester):
|
|
|
144
148
|
"""
|
|
145
149
|
|
|
146
150
|
if self._session is None:
|
|
151
|
+
if self._session_lock is None:
|
|
152
|
+
self._session_lock = asyncio.Lock()
|
|
153
|
+
|
|
147
154
|
async with self._session_lock:
|
|
148
155
|
if self._session is None: # Double-check after acquiring lock...
|
|
149
156
|
self._session = ClientSession(
|
|
@@ -265,10 +272,17 @@ class AioHttpRequester(IRequester):
|
|
|
265
272
|
return response
|
|
266
273
|
|
|
267
274
|
except ClientResponseError as error:
|
|
268
|
-
if attempts > retries:
|
|
275
|
+
if error.status not in self.RETRYABLE_ERRORS or attempts > retries:
|
|
269
276
|
self.raise_custom_exception(error.status, error.message)
|
|
270
277
|
|
|
271
|
-
|
|
278
|
+
with suppress(RetryableException):
|
|
279
|
+
self.raise_custom_exception(
|
|
280
|
+
error.status,
|
|
281
|
+
error.message,
|
|
282
|
+
within_retry=True,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await asyncio.sleep(backoff_factor * (2 ** (attempts - 1)))
|
|
272
286
|
|
|
273
287
|
async def close(self) -> None:
|
|
274
288
|
"""
|
|
@@ -279,8 +293,9 @@ class AioHttpRequester(IRequester):
|
|
|
279
293
|
in the constructor, it will not be closed as the caller is responsible
|
|
280
294
|
for managing its lifecycle.
|
|
281
295
|
|
|
282
|
-
The session reference is
|
|
283
|
-
|
|
296
|
+
The session reference is cleared only when the session was created
|
|
297
|
+
internally. External sessions retain their reference so the requester
|
|
298
|
+
remains usable after close() without silently creating a new session.
|
|
284
299
|
|
|
285
300
|
Note:
|
|
286
301
|
This method is automatically called when using the async context
|
|
@@ -290,4 +305,5 @@ class AioHttpRequester(IRequester):
|
|
|
290
305
|
|
|
291
306
|
if self._session and self._owns_session:
|
|
292
307
|
await self._session.close()
|
|
293
|
-
|
|
308
|
+
self._session = None
|
|
309
|
+
self._session_lock = None
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Rate-limited aiohttp requester using aiolimiter.AsyncLimiter."""
|
|
4
|
+
|
|
3
5
|
from aiohttp import ClientResponse
|
|
4
6
|
from aiolimiter import AsyncLimiter
|
|
5
7
|
|
|
6
|
-
from core_https.requesters.
|
|
8
|
+
from core_https.requesters.aiohttp_ import AioHttpRequester
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
class AioHttpRateLimitRequester(
|
|
11
|
+
class AioHttpRateLimitRequester(AioHttpRequester):
|
|
10
12
|
"""
|
|
11
13
|
An HTTP requester that enforces a simple *rate limit* using
|
|
12
14
|
:class:`aiolimiter.AsyncLimiter`. This class restricts how many
|
|
@@ -17,7 +19,6 @@ class AioHttpRateLimitRequester(AioHttpThrottleRequester):
|
|
|
17
19
|
|
|
18
20
|
def __init__(
|
|
19
21
|
self,
|
|
20
|
-
max_concurrency: int,
|
|
21
22
|
max_rate: int,
|
|
22
23
|
time_period: float,
|
|
23
24
|
**kwargs
|
|
@@ -36,7 +37,7 @@ class AioHttpRateLimitRequester(AioHttpThrottleRequester):
|
|
|
36
37
|
if time_period <= 0:
|
|
37
38
|
raise ValueError("`time_period` must be positive!")
|
|
38
39
|
|
|
39
|
-
super().__init__(
|
|
40
|
+
super().__init__(**kwargs)
|
|
40
41
|
|
|
41
42
|
self.max_rate = max_rate
|
|
42
43
|
self.time_period = time_period
|
|
@@ -46,6 +47,10 @@ class AioHttpRateLimitRequester(AioHttpThrottleRequester):
|
|
|
46
47
|
time_period=self.time_period,
|
|
47
48
|
)
|
|
48
49
|
|
|
50
|
+
@classmethod
|
|
51
|
+
def engine(cls) -> str:
|
|
52
|
+
return "aiohttp_rate_limit"
|
|
53
|
+
|
|
49
54
|
async def request(self, *args, **kwargs) -> ClientResponse:
|
|
50
55
|
"""
|
|
51
56
|
Execute an HTTP request subject to the configured rate limit. The
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Concurrency-throttled aiohttp requester using asyncio.Semaphore."""
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
6
|
+
from typing import Optional
|
|
4
7
|
|
|
5
8
|
from aiohttp import ClientResponse
|
|
6
9
|
|
|
@@ -34,11 +37,11 @@ class AioHttpThrottleRequester(AioHttpRequester):
|
|
|
34
37
|
|
|
35
38
|
super().__init__(**kwargs)
|
|
36
39
|
self.max_concurrency = max_concurrency
|
|
37
|
-
self._semaphore
|
|
40
|
+
self._semaphore: Optional[asyncio.Semaphore] = None
|
|
38
41
|
|
|
39
42
|
@classmethod
|
|
40
|
-
def
|
|
41
|
-
return
|
|
43
|
+
def engine(cls) -> str:
|
|
44
|
+
return "aiohttp_throttle"
|
|
42
45
|
|
|
43
46
|
async def request(self, *args, **kwargs) -> ClientResponse:
|
|
44
47
|
"""
|
|
@@ -50,5 +53,12 @@ class AioHttpThrottleRequester(AioHttpRequester):
|
|
|
50
53
|
:raises: Any exception raised by the underlying session.
|
|
51
54
|
"""
|
|
52
55
|
|
|
56
|
+
if self._semaphore is None:
|
|
57
|
+
self._semaphore = asyncio.Semaphore(self.max_concurrency)
|
|
58
|
+
|
|
53
59
|
async with self._semaphore:
|
|
54
60
|
return await super().request(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
async def close(self) -> None:
|
|
63
|
+
await super().close()
|
|
64
|
+
self._semaphore = None
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Abstract base class and shared utilities for all HTTP requester implementations."""
|
|
4
|
+
|
|
3
5
|
import re
|
|
4
6
|
from abc import ABC
|
|
5
7
|
from abc import abstractmethod
|
|
6
|
-
from typing import Any, Dict, Optional
|
|
8
|
+
from typing import Any, Dict, Mapping, Optional
|
|
7
9
|
|
|
8
10
|
from core_mixins.interfaces.factory import IFactory
|
|
9
11
|
|
|
@@ -19,7 +21,7 @@ from core_https.exceptions import (
|
|
|
19
21
|
try:
|
|
20
22
|
from http import HTTPMethod as _HTTPMethod
|
|
21
23
|
except ImportError:
|
|
22
|
-
from core_https.utils import HTTPMethod as _HTTPMethod
|
|
24
|
+
from core_https.utils import HTTPMethod as _HTTPMethod
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
# Type alias that works with both standard library
|
|
@@ -77,6 +79,8 @@ class IRequester(IFactory, ABC):
|
|
|
77
79
|
- :class:`core_mixins.interfaces.factory.IFactory`: Base factory interface
|
|
78
80
|
"""
|
|
79
81
|
|
|
82
|
+
RETRYABLE_ERRORS = (429, 502, 503, 504)
|
|
83
|
+
|
|
80
84
|
def __init__(
|
|
81
85
|
self,
|
|
82
86
|
encoding: str = "utf-8",
|
|
@@ -255,7 +259,7 @@ class IRequester(IFactory, ABC):
|
|
|
255
259
|
|
|
256
260
|
def _get_response_encoding(
|
|
257
261
|
self,
|
|
258
|
-
headers:
|
|
262
|
+
headers: Mapping[str, str],
|
|
259
263
|
default: str = "utf-8",
|
|
260
264
|
) -> str:
|
|
261
265
|
"""
|
|
@@ -312,8 +316,8 @@ class IRequester(IFactory, ABC):
|
|
|
312
316
|
|
|
313
317
|
return self.encoding or default
|
|
314
318
|
|
|
315
|
-
@staticmethod
|
|
316
319
|
def raise_custom_exception(
|
|
320
|
+
self,
|
|
317
321
|
status_code: int,
|
|
318
322
|
details: str,
|
|
319
323
|
within_retry: bool = False,
|
|
@@ -379,7 +383,7 @@ class IRequester(IFactory, ABC):
|
|
|
379
383
|
# next condition.
|
|
380
384
|
error_cls = RateLimitException
|
|
381
385
|
|
|
382
|
-
elif status_code in
|
|
386
|
+
elif status_code in self.RETRYABLE_ERRORS and within_retry:
|
|
383
387
|
error_cls = RetryableException
|
|
384
388
|
|
|
385
389
|
elif 400 <= status_code < 500:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Synchronous HTTP requester implementation backed by the requests library."""
|
|
4
|
+
|
|
3
5
|
from typing import Dict, Optional
|
|
4
6
|
|
|
5
7
|
import requests
|
|
@@ -44,7 +46,7 @@ class RequestsRequester(IRequester):
|
|
|
44
46
|
:param retries: Retry strategy to apply. Pass zero (0) to avoid retries.
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
|
-
super().__init__(**kwargs)
|
|
49
|
+
super().__init__(backoff_factor=backoff_factor, **kwargs)
|
|
48
50
|
self.retries = retries
|
|
49
51
|
|
|
50
52
|
backoff_factor = (
|
|
@@ -57,7 +59,7 @@ class RequestsRequester(IRequester):
|
|
|
57
59
|
|
|
58
60
|
if self.retries is None:
|
|
59
61
|
self.retries = urllib3.Retry(
|
|
60
|
-
status_forcelist=
|
|
62
|
+
status_forcelist=IRequester.RETRYABLE_ERRORS,
|
|
61
63
|
backoff_factor=backoff_factor,
|
|
62
64
|
total=3,
|
|
63
65
|
)
|
|
@@ -70,8 +72,9 @@ class RequestsRequester(IRequester):
|
|
|
70
72
|
|
|
71
73
|
if session is None:
|
|
72
74
|
session = requests.Session()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
|
|
76
|
+
session.mount("https://", adapter)
|
|
77
|
+
session.mount("http://", adapter)
|
|
75
78
|
|
|
76
79
|
self._session = session
|
|
77
80
|
|
|
@@ -79,7 +82,7 @@ class RequestsRequester(IRequester):
|
|
|
79
82
|
def engine(cls) -> str:
|
|
80
83
|
return "requests"
|
|
81
84
|
|
|
82
|
-
def request(
|
|
85
|
+
def request(
|
|
83
86
|
self,
|
|
84
87
|
url: str,
|
|
85
88
|
method: HTTPMethod = HTTPMethod.GET,
|
|
@@ -99,15 +102,13 @@ class RequestsRequester(IRequester):
|
|
|
99
102
|
"""
|
|
100
103
|
|
|
101
104
|
session_ = session or self._session
|
|
102
|
-
if params is None:
|
|
103
|
-
params = {}
|
|
104
105
|
|
|
105
106
|
response = session_.request(
|
|
106
107
|
method=str(method.value),
|
|
107
108
|
url=url,
|
|
108
109
|
headers=headers,
|
|
109
110
|
params=params,
|
|
110
|
-
timeout=timeout,
|
|
111
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
111
112
|
**kwargs,
|
|
112
113
|
)
|
|
113
114
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
+
"""Low-level synchronous HTTP requester implementation backed by urllib3."""
|
|
4
|
+
|
|
3
5
|
import json
|
|
4
6
|
from contextlib import suppress
|
|
5
7
|
from typing import Dict, Optional
|
|
@@ -40,7 +42,7 @@ class Urllib3Requester(IRequester):
|
|
|
40
42
|
|
|
41
43
|
super().__init__(**kwargs)
|
|
42
44
|
|
|
43
|
-
if
|
|
45
|
+
if pool_manager is None:
|
|
44
46
|
pool_manager = PoolManager()
|
|
45
47
|
|
|
46
48
|
self._http: PoolManager = pool_manager
|
|
@@ -78,10 +80,10 @@ class Urllib3Requester(IRequester):
|
|
|
78
80
|
else 0.5
|
|
79
81
|
)
|
|
80
82
|
|
|
81
|
-
retries_ = retries
|
|
83
|
+
retries_ = retries if retries is not None else self.retries
|
|
82
84
|
if retries_ is None:
|
|
83
85
|
retries_ = Retry(
|
|
84
|
-
status_forcelist=
|
|
86
|
+
status_forcelist=self.RETRYABLE_ERRORS,
|
|
85
87
|
backoff_factor=backoff_factor,
|
|
86
88
|
total=3,
|
|
87
89
|
)
|
|
@@ -91,7 +93,7 @@ class Urllib3Requester(IRequester):
|
|
|
91
93
|
url=url,
|
|
92
94
|
headers=headers,
|
|
93
95
|
fields=fields,
|
|
94
|
-
timeout=timeout
|
|
96
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
95
97
|
retries=retries_,
|
|
96
98
|
**kwargs,
|
|
97
99
|
)
|
|
@@ -99,7 +101,7 @@ class Urllib3Requester(IRequester):
|
|
|
99
101
|
status_code = response.status
|
|
100
102
|
|
|
101
103
|
if status_code >= 400 and self.raise_for_status:
|
|
102
|
-
info = response.data.decode(self._get_response_encoding(response.headers))
|
|
104
|
+
info = response.data.decode(self._get_response_encoding(response.headers))
|
|
103
105
|
headers_ = {k.lower(): v for k, v in response.headers.items()}
|
|
104
106
|
|
|
105
107
|
if "application/json" in headers_.get("content-type", ""):
|
|
@@ -110,6 +112,9 @@ class Urllib3Requester(IRequester):
|
|
|
110
112
|
with suppress(json.JSONDecodeError):
|
|
111
113
|
info = json.loads(info)
|
|
112
114
|
|
|
113
|
-
self.raise_custom_exception(
|
|
115
|
+
self.raise_custom_exception(
|
|
116
|
+
status_code,
|
|
117
|
+
info if isinstance(info, str) else json.dumps(info),
|
|
118
|
+
)
|
|
114
119
|
|
|
115
120
|
return response
|
|
@@ -57,11 +57,14 @@ def patch_aiohttp(
|
|
|
57
57
|
Args:
|
|
58
58
|
url: The URL that the mock should respond to. Defaults to "https://example.com".
|
|
59
59
|
method: HTTP method for the mock request. Defaults to "GET".
|
|
60
|
-
json_response: JSON data to return in the response body.
|
|
60
|
+
json_response: JSON data to return in the response body.
|
|
61
|
+
Mutually exclusive with text_response and content.
|
|
61
62
|
status: HTTP status code for the mock response. Defaults to 200.
|
|
62
63
|
headers: Dictionary of response headers.
|
|
63
|
-
text_response: Plain text data to return.
|
|
64
|
-
|
|
64
|
+
text_response: Plain text data to return.
|
|
65
|
+
Mutually exclusive with json_response and content.
|
|
66
|
+
content: Raw bytes to return as response content.
|
|
67
|
+
Mutually exclusive with json_response and text_response.
|
|
65
68
|
content_type: MIME type of the response. Defaults to "application/json".
|
|
66
69
|
charset: Character encoding for the response. Defaults to "utf-8".
|
|
67
70
|
raise_for_status_exception: Exception to raise when response.raise_for_status() is called.
|
|
@@ -142,10 +145,13 @@ def patch_requests(
|
|
|
142
145
|
url: The URL that the mock should respond to. Defaults to "https://example.com".
|
|
143
146
|
encoding: Character encoding for the response. Defaults to "utf-8".
|
|
144
147
|
headers: Dictionary of response headers.
|
|
145
|
-
json_response: JSON data to return in the response body.
|
|
146
|
-
|
|
148
|
+
json_response: JSON data to return in the response body.
|
|
149
|
+
Mutually exclusive with text_response and content.
|
|
150
|
+
text_response: Plain text data to return.
|
|
151
|
+
Mutually exclusive with json_response and content.
|
|
147
152
|
status_code: HTTP status code for the mock response. Defaults to 200.
|
|
148
|
-
content: Raw bytes to return as response content.
|
|
153
|
+
content: Raw bytes to return as response content.
|
|
154
|
+
Mutually exclusive with json_response and text_response.
|
|
149
155
|
raise_for_status_exception: Exception to raise when response.raise_for_status() is called.
|
|
150
156
|
|
|
151
157
|
Returns:
|
|
@@ -123,7 +123,8 @@ class BaseRequestsTestCases(BaseHttpTestCases):
|
|
|
123
123
|
url: The URL that the mock response represents. Defaults to "https://example.com".
|
|
124
124
|
encoding: Character encoding for text content. Defaults to "utf-8".
|
|
125
125
|
headers: Dictionary of response headers. Auto-generated if not provided.
|
|
126
|
-
json_response: JSON data to return from the .json() method.
|
|
126
|
+
json_response: JSON data to return from the .json() method.
|
|
127
|
+
Mutually exclusive with text_response.
|
|
127
128
|
text_response: Plain text content. Auto-generated from json_response if not provided.
|
|
128
129
|
status_code: HTTP status code for the response. Defaults to 200.
|
|
129
130
|
content: Raw bytes content. Auto-generated from text_response if not provided.
|
|
@@ -269,10 +269,9 @@ class BaseUrllib3TestCases(BaseHttpTestCases):
|
|
|
269
269
|
"""Mock read method."""
|
|
270
270
|
if preload_content:
|
|
271
271
|
return data.decode() if decode_content else data
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return res.decode() if decode_content else res
|
|
272
|
+
# Simulate streaming read...
|
|
273
|
+
res = data[:amt] if amt else data
|
|
274
|
+
return res.decode() if decode_content else res
|
|
276
275
|
|
|
277
276
|
def mock_read1(amt=None):
|
|
278
277
|
"""Mock read1 method (reads up to amt bytes)."""
|
|
@@ -289,7 +288,7 @@ class BaseUrllib3TestCases(BaseHttpTestCases):
|
|
|
289
288
|
line = lines[0] + b"\n" if len(lines) > 1 else lines[0]
|
|
290
289
|
return line[:size] if size > 0 else line
|
|
291
290
|
|
|
292
|
-
def mock_readlines(
|
|
291
|
+
def mock_readlines(_hint=-1):
|
|
293
292
|
return data.split(b"\n")
|
|
294
293
|
|
|
295
294
|
# Assign read methods...
|
|
@@ -300,7 +299,7 @@ class BaseUrllib3TestCases(BaseHttpTestCases):
|
|
|
300
299
|
mock.readlines = mock_readlines
|
|
301
300
|
|
|
302
301
|
# Stream methods
|
|
303
|
-
def mock_stream(amt=2**16,
|
|
302
|
+
def mock_stream(amt=2**16, _decode_content=None):
|
|
304
303
|
if not data:
|
|
305
304
|
return
|
|
306
305
|
|
|
@@ -337,7 +336,7 @@ class BaseUrllib3TestCases(BaseHttpTestCases):
|
|
|
337
336
|
mock.__exit__ = Mock(side_effect=mock_close())
|
|
338
337
|
|
|
339
338
|
# Iterator support
|
|
340
|
-
def mock_iter(
|
|
339
|
+
def mock_iter(_self):
|
|
341
340
|
"""Support for iteration over response."""
|
|
342
341
|
yield from data.split(b"\n")
|
|
343
342
|
|
|
@@ -50,8 +50,6 @@ See Also:
|
|
|
50
50
|
from enum import Enum
|
|
51
51
|
from typing import Dict
|
|
52
52
|
|
|
53
|
-
from core_mixins import Self
|
|
54
|
-
|
|
55
53
|
|
|
56
54
|
class HTTPStatus(Enum):
|
|
57
55
|
"""
|
|
@@ -163,7 +161,7 @@ class HTTPStatus(Enum):
|
|
|
163
161
|
|
|
164
162
|
def __repr__(self):
|
|
165
163
|
"""Return string representation for debugging."""
|
|
166
|
-
return f"<{self.__class__.__name__}.{self._value_}>"
|
|
164
|
+
return f"<{self.__class__.__name__}.{self.name}: {self._value_}>"
|
|
167
165
|
|
|
168
166
|
def __str__(self):
|
|
169
167
|
"""Return string representation of the status code."""
|
|
@@ -204,7 +202,7 @@ class HTTPStatus(Enum):
|
|
|
204
202
|
return self.is_client_error() or self.is_server_error()
|
|
205
203
|
|
|
206
204
|
@classmethod
|
|
207
|
-
def by_code(cls, code: int) ->
|
|
205
|
+
def by_code(cls, code: int) -> "HTTPStatus":
|
|
208
206
|
"""
|
|
209
207
|
Find HTTP status by numeric code.
|
|
210
208
|
|
|
@@ -222,11 +220,10 @@ class HTTPStatus(Enum):
|
|
|
222
220
|
print(status.description) # "Not Found"
|
|
223
221
|
"""
|
|
224
222
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
raise ValueError(f"No HTTPStatus found for code: {code}")
|
|
223
|
+
try:
|
|
224
|
+
return _HTTP_STATUS_BY_CODE[code]
|
|
225
|
+
except KeyError as exc:
|
|
226
|
+
raise ValueError(f"No HTTPStatus found for code: {code}") from exc
|
|
230
227
|
|
|
231
228
|
@classmethod
|
|
232
229
|
def as_dict(cls) -> Dict[int, str]:
|
|
@@ -247,6 +244,9 @@ class HTTPStatus(Enum):
|
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
|
|
247
|
+
_HTTP_STATUS_BY_CODE: Dict[int, HTTPStatus] = {m.value: m for m in HTTPStatus}
|
|
248
|
+
|
|
249
|
+
|
|
250
250
|
class HTTPMethod(Enum):
|
|
251
251
|
"""
|
|
252
252
|
HTTP method enumeration with backward compatibility.
|
|
@@ -332,7 +332,7 @@ class HTTPMethod(Enum):
|
|
|
332
332
|
return self in (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.POST)
|
|
333
333
|
|
|
334
334
|
@classmethod
|
|
335
|
-
def by_name(cls, name: str) ->
|
|
335
|
+
def by_name(cls, name: str) -> "HTTPMethod":
|
|
336
336
|
"""
|
|
337
337
|
Find HTTP method by name (case-insensitive).
|
|
338
338
|
|
|
@@ -352,8 +352,10 @@ class HTTPMethod(Enum):
|
|
|
352
352
|
|
|
353
353
|
name = name.upper()
|
|
354
354
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
try:
|
|
356
|
+
return _HTTP_METHOD_BY_NAME[name]
|
|
357
|
+
except KeyError as exc:
|
|
358
|
+
raise ValueError(f"No HTTPMethod found for name: {name}") from exc
|
|
359
|
+
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
_HTTP_METHOD_BY_NAME: Dict[str, HTTPMethod] = {m.value: m for m in HTTPMethod}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-https
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
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
|
|
@@ -21,17 +21,19 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
26
|
Requires-Python: >=3.9
|
|
25
27
|
Description-Content-Type: text/x-rst
|
|
26
28
|
License-File: LICENSE
|
|
27
29
|
Requires-Dist: aiohttp<4.0.0,>=3.12.0
|
|
28
|
-
Requires-Dist: core-mixins>=
|
|
30
|
+
Requires-Dist: core-mixins>=3.0.1
|
|
29
31
|
Requires-Dist: requests<3.0.0,>=2.32.3
|
|
30
32
|
Requires-Dist: urllib3<3.0.0,>=2.2.3
|
|
31
33
|
Provides-Extra: dev
|
|
32
34
|
Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "dev"
|
|
33
|
-
Requires-Dist: core-dev-tools>=1.
|
|
34
|
-
Requires-Dist: core-tests>=2.0.
|
|
35
|
+
Requires-Dist: core-dev-tools>=1.2.1; extra == "dev"
|
|
36
|
+
Requires-Dist: core-tests>=2.0.5; extra == "dev"
|
|
35
37
|
Requires-Dist: types-requests>=2.32.0.20250602; extra == "dev"
|
|
36
38
|
Provides-Extra: extras
|
|
37
39
|
Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "extras"
|
|
@@ -137,7 +139,13 @@ Check tests and coverage
|
|
|
137
139
|
|
|
138
140
|
.. code-block:: shell
|
|
139
141
|
|
|
142
|
+
# Unit tests (mocked, no network):
|
|
140
143
|
python manager.py run-tests
|
|
144
|
+
|
|
145
|
+
# Functional tests (real HTTP requests — requires network):
|
|
146
|
+
python manager.py run-tests --test-type functional --pattern "*.py"
|
|
147
|
+
|
|
148
|
+
# Coverage:
|
|
141
149
|
python manager.py run-coverage
|
|
142
150
|
|
|
143
151
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
aiohttp<4.0.0,>=3.12.0
|
|
2
|
-
core-mixins>=
|
|
2
|
+
core-mixins>=3.0.1
|
|
3
3
|
requests<3.0.0,>=2.32.3
|
|
4
4
|
urllib3<3.0.0,>=2.2.3
|
|
5
5
|
|
|
6
6
|
[dev]
|
|
7
7
|
aiolimiter<2.0.0,>=1.2.1
|
|
8
|
-
core-dev-tools>=1.
|
|
9
|
-
core-tests>=2.0.
|
|
8
|
+
core-dev-tools>=1.2.1
|
|
9
|
+
core-tests>=2.0.5
|
|
10
10
|
types-requests>=2.32.0.20250602
|
|
11
11
|
|
|
12
12
|
[extras]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Other project metadata fields as specified in:
|
|
2
|
+
# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
|
|
3
|
+
# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
|
4
|
+
|
|
5
|
+
[build-system]
|
|
6
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
7
|
+
build-backend = "setuptools.build_meta"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "core-https"
|
|
11
|
+
description = "This project/library contains common elements related to HTTP & API services..."
|
|
12
|
+
version = "3.0.0"
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{name = "Alejandro Cora González", email = "alek.cora.glez@gmail.com"}
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
maintainers = [
|
|
19
|
+
{name = "Alejandro Cora González"}
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
requires-python = ">=3.9"
|
|
23
|
+
license = "MIT"
|
|
24
|
+
readme = "README.rst"
|
|
25
|
+
|
|
26
|
+
classifiers = [
|
|
27
|
+
# Classifiers -> https://pypi.org/classifiers/
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Development Status :: 5 - Production/Stable",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Programming Language :: Python :: 3.14",
|
|
40
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
dependencies = [
|
|
44
|
+
"aiohttp>=3.12.0,<4.0.0",
|
|
45
|
+
"core-mixins>=3.0.1",
|
|
46
|
+
"requests>=2.32.3,<3.0.0",
|
|
47
|
+
"urllib3>=2.2.3,<3.0.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
dev = [
|
|
52
|
+
"aiolimiter>=1.2.1,<2.0.0",
|
|
53
|
+
"core-dev-tools>=1.2.1",
|
|
54
|
+
"core-tests>=2.0.5",
|
|
55
|
+
"types-requests>=2.32.0.20250602",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
extras = [
|
|
59
|
+
"aiolimiter>=1.2.1,<2.0.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[project.urls]
|
|
63
|
+
Homepage = "https://gitlab.com/bytecode-solutions/core/core-https"
|
|
64
|
+
Repository = "https://gitlab.com/bytecode-solutions/core/core-https"
|
|
65
|
+
Documentation = "https://core-https.readthedocs.io/en/latest/"
|
|
66
|
+
Issues = "https://gitlab.com/bytecode-solutions/core/core-https/-/issues"
|
|
67
|
+
Changelog = "https://gitlab.com/bytecode-solutions/core/core-https/-/blob/master/CHANGELOG.md"
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.package-data]
|
|
70
|
+
"core_https" = ["py.typed"]
|
|
71
|
+
|
|
72
|
+
[tool.pyright]
|
|
73
|
+
pythonVersion = "3.11"
|
|
74
|
+
# core_mixins ships no stubs; pyright cannot prove StrEnum is a class and
|
|
75
|
+
# raises reportGeneralTypeIssues on `class StatusInfo(StrEnum)`.
|
|
76
|
+
# __init__.py is a thin re-export/enum file — ignoring it is safe.
|
|
77
|
+
ignore = ["core_https/__init__.py"]
|
|
78
|
+
|
|
79
|
+
[tool.mypy]
|
|
80
|
+
python_version = "3.11"
|
|
81
|
+
|
|
82
|
+
# The try/except ImportError compatibility shims assign either the stdlib
|
|
83
|
+
# symbol (e.g. http.HTTPStatus) or the backport from core_https.utils to the
|
|
84
|
+
# same name. mypy sees the two types as incompatible; this is a known
|
|
85
|
+
# false-positive for the try/except compat pattern.
|
|
86
|
+
[[tool.mypy.overrides]]
|
|
87
|
+
module = ["core_https", "core_https.requesters.base"]
|
|
88
|
+
disable_error_code = ["assignment"]
|
|
89
|
+
|
|
90
|
+
# urllib3.HTTPHeaderDict is a MutableMapping[str, str], not a plain dict.
|
|
91
|
+
# _get_response_encoding only needs Mapping semantics (.get / .items); the
|
|
92
|
+
# annotation is intentionally wide in the implementation, but mypy flags the
|
|
93
|
+
# call site because it cannot see through the urllib3 stub.
|
|
94
|
+
[[tool.mypy.overrides]]
|
|
95
|
+
module = "core_https.requesters.urllib3_"
|
|
96
|
+
disable_error_code = ["arg-type"]
|
|
97
|
+
|
|
98
|
+
[tool.pylint.messages_control]
|
|
99
|
+
disable = [
|
|
100
|
+
# HTTP API methods intentionally have many parameters (url, method,
|
|
101
|
+
# headers, timeout, retries, …). Splitting them would harm usability.
|
|
102
|
+
"too-many-arguments", # R0913
|
|
103
|
+
"too-many-positional-arguments", # R0917
|
|
104
|
+
|
|
105
|
+
# get_urllib3_mock builds a comprehensive mock object; it genuinely
|
|
106
|
+
# needs many local variables and statements.
|
|
107
|
+
"too-many-locals", # R0914
|
|
108
|
+
"too-many-statements", # R0915
|
|
109
|
+
|
|
110
|
+
# Test helpers intentionally access private urllib3/aiohttp internals
|
|
111
|
+
# (e.g. _request_url, _connection) to set up realistic mocks.
|
|
112
|
+
"protected-access", # W0212
|
|
113
|
+
|
|
114
|
+
# Async requesters (aiohttp_*.py) override abstract sync `request()`
|
|
115
|
+
# with `async def request()`. The override is intentional.
|
|
116
|
+
"invalid-overridden-method", # W0236
|
|
117
|
+
|
|
118
|
+
# The backoff_factor resolution pattern is intentionally repeated
|
|
119
|
+
# across Urllib3Requester and RequestsRequester.
|
|
120
|
+
"duplicate-code", # R0801
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[tool.ty.src]
|
|
124
|
+
# core_mixins ships no stubs; ty cannot prove StrEnum is a class and raises
|
|
125
|
+
# unsupported-base on `class StatusInfo(StrEnum)`. __init__.py is a thin
|
|
126
|
+
# re-export/enum file — ignoring it is safe.
|
|
127
|
+
exclude = ["core_https/__init__.py"]
|
|
128
|
+
|
|
129
|
+
[tool.ty.environment]
|
|
130
|
+
# Check against Python 3.11 so that stdlib symbols introduced in 3.11
|
|
131
|
+
# (http.HTTPMethod, typing.Self, enum.StrEnum) are available, avoiding
|
|
132
|
+
# false-positive unresolved-import errors in the try/except compat shims.
|
|
133
|
+
python-version = "3.11"
|
core_https-2.0.3/pyproject.toml
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Other project metadata fields as specified in:
|
|
2
|
-
# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
|
|
3
|
-
# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
|
4
|
-
|
|
5
|
-
[build-system]
|
|
6
|
-
requires = ["setuptools>=61.0.0", "wheel"]
|
|
7
|
-
build-backend = "setuptools.build_meta"
|
|
8
|
-
|
|
9
|
-
[project]
|
|
10
|
-
name = "core-https"
|
|
11
|
-
description = "This project/library contains common elements related to HTTP & API services..."
|
|
12
|
-
version = "2.0.3"
|
|
13
|
-
|
|
14
|
-
authors = [
|
|
15
|
-
{name = "Alejandro Cora González", email = "alek.cora.glez@gmail.com"}
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
maintainers = [
|
|
19
|
-
{name = "Alejandro Cora González"}
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
requires-python = ">=3.9"
|
|
23
|
-
license = "MIT"
|
|
24
|
-
readme = "README.rst"
|
|
25
|
-
|
|
26
|
-
classifiers = [
|
|
27
|
-
# Classifiers -> https://pypi.org/classifiers/
|
|
28
|
-
"Intended Audience :: Developers",
|
|
29
|
-
"Development Status :: 5 - Production/Stable",
|
|
30
|
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
-
"Topic :: Utilities",
|
|
32
|
-
"Programming Language :: Python :: 3",
|
|
33
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
-
"Programming Language :: Python :: 3.9",
|
|
35
|
-
"Programming Language :: Python :: 3.10",
|
|
36
|
-
"Programming Language :: Python :: 3.11",
|
|
37
|
-
"Programming Language :: Python :: 3.12",
|
|
38
|
-
"Programming Language :: Python :: 3.13",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
dependencies = [
|
|
42
|
-
"aiohttp>=3.12.0,<4.0.0",
|
|
43
|
-
"core-mixins>=2.2.2",
|
|
44
|
-
"requests>=2.32.3,<3.0.0",
|
|
45
|
-
"urllib3>=2.2.3,<3.0.0",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
[project.optional-dependencies]
|
|
49
|
-
dev = [
|
|
50
|
-
"aiolimiter>=1.2.1,<2.0.0",
|
|
51
|
-
"core-dev-tools>=1.0.1",
|
|
52
|
-
"core-tests>=2.0.2",
|
|
53
|
-
"types-requests>=2.32.0.20250602",
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
extras = [
|
|
57
|
-
"aiolimiter>=1.2.1,<2.0.0",
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
[project.urls]
|
|
61
|
-
Homepage = "https://gitlab.com/bytecode-solutions/core/core-https"
|
|
62
|
-
Repository = "https://gitlab.com/bytecode-solutions/core/core-https"
|
|
63
|
-
Documentation = "https://core-https.readthedocs.io/en/latest/"
|
|
64
|
-
Issues = "https://gitlab.com/bytecode-solutions/core/core-https/-/issues"
|
|
65
|
-
Changelog = "https://gitlab.com/bytecode-solutions/core/core-https/-/blob/master/CHANGELOG.md"
|
|
66
|
-
|
|
67
|
-
[tool.setuptools.package-data]
|
|
68
|
-
"core_https" = ["py.typed"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|