core-https 2.0.2__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.
Files changed (30) hide show
  1. {core_https-2.0.2 → core_https-3.0.0}/PKG-INFO +18 -7
  2. {core_https-2.0.2 → core_https-3.0.0}/README.rst +6 -0
  3. {core_https-2.0.2 → core_https-3.0.0}/core_https/__init__.py +3 -1
  4. {core_https-2.0.2 → core_https-3.0.0}/core_https/exceptions.py +9 -2
  5. {core_https-2.0.2 → core_https-3.0.0}/core_https/requesters/aiohttp_.py +22 -6
  6. core_https-3.0.0/core_https/requesters/aiohttp_rate_limit.py +65 -0
  7. core_https-3.0.0/core_https/requesters/aiohttp_throttle.py +64 -0
  8. {core_https-2.0.2 → core_https-3.0.0}/core_https/requesters/base.py +9 -5
  9. {core_https-2.0.2 → core_https-3.0.0}/core_https/requesters/requests_.py +9 -8
  10. {core_https-2.0.2 → core_https-3.0.0}/core_https/requesters/urllib3_.py +11 -6
  11. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/decorators.py +12 -6
  12. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/requests_.py +2 -1
  13. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/urllib3_.py +6 -7
  14. {core_https-2.0.2 → core_https-3.0.0}/core_https/utils.py +16 -14
  15. {core_https-2.0.2 → core_https-3.0.0}/core_https.egg-info/PKG-INFO +18 -7
  16. {core_https-2.0.2 → core_https-3.0.0}/core_https.egg-info/SOURCES.txt +2 -0
  17. core_https-3.0.0/core_https.egg-info/requires.txt +13 -0
  18. core_https-3.0.0/pyproject.toml +133 -0
  19. core_https-2.0.2/core_https.egg-info/requires.txt +0 -11
  20. core_https-2.0.2/pyproject.toml +0 -63
  21. {core_https-2.0.2 → core_https-3.0.0}/LICENSE +0 -0
  22. {core_https-2.0.2 → core_https-3.0.0}/core_https/py.typed +0 -0
  23. {core_https-2.0.2 → core_https-3.0.0}/core_https/requesters/__init__.py +0 -0
  24. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/__init__.py +0 -0
  25. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/aiohttp_.py +0 -0
  26. {core_https-2.0.2 → core_https-3.0.0}/core_https/tests/base.py +0 -0
  27. {core_https-2.0.2 → core_https-3.0.0}/core_https.egg-info/dependency_links.txt +0 -0
  28. {core_https-2.0.2 → core_https-3.0.0}/core_https.egg-info/top_level.txt +0 -0
  29. {core_https-2.0.2 → core_https-3.0.0}/setup.cfg +0 -0
  30. {core_https-2.0.2 → 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: 2.0.2
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,22 @@ 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
- Requires-Dist: aiohttp<4.0.0,>=3.12.0; python_version >= "3.9"
28
- Requires-Dist: core-mixins>=2.2.2
29
- Requires-Dist: requests<3.0.0,>=2.32.3; python_version >= "3.9"
30
- Requires-Dist: urllib3<3.0.0,>=2.2.3; python_version >= "3.9"
29
+ Requires-Dist: aiohttp<4.0.0,>=3.12.0
30
+ Requires-Dist: core-mixins>=3.0.1
31
+ Requires-Dist: requests<3.0.0,>=2.32.3
32
+ Requires-Dist: urllib3<3.0.0,>=2.2.3
31
33
  Provides-Extra: dev
32
- Requires-Dist: core-dev-tools>=1.0.1; extra == "dev"
33
- Requires-Dist: core-tests>=2.0.2; extra == "dev"
34
+ Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "dev"
35
+ Requires-Dist: core-dev-tools>=1.2.1; extra == "dev"
36
+ Requires-Dist: core-tests>=2.0.5; extra == "dev"
34
37
  Requires-Dist: types-requests>=2.32.0.20250602; extra == "dev"
38
+ Provides-Extra: extras
39
+ Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "extras"
35
40
  Dynamic: license-file
36
41
 
37
42
  core-https
@@ -134,7 +139,13 @@ Check tests and coverage
134
139
 
135
140
  .. code-block:: shell
136
141
 
142
+ # Unit tests (mocked, no network):
137
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:
138
149
  python manager.py run-coverage
139
150
 
140
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 # type: ignore
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 Dict
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) -> Dict[str, str]:
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 = asyncio.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
- await asyncio.sleep(backoff_factor * attempts)
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 always cleared after calling this method,
283
- regardless of whether it was closed or not.
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
- self._session = None
308
+ self._session = None
309
+ self._session_lock = None
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Rate-limited aiohttp requester using aiolimiter.AsyncLimiter."""
4
+
5
+ from aiohttp import ClientResponse
6
+ from aiolimiter import AsyncLimiter
7
+
8
+ from core_https.requesters.aiohttp_ import AioHttpRequester
9
+
10
+
11
+ class AioHttpRateLimitRequester(AioHttpRequester):
12
+ """
13
+ An HTTP requester that enforces a simple *rate limit* using
14
+ :class:`aiolimiter.AsyncLimiter`. This class restricts how many
15
+ requests may be *started* within a given time window. It does **not**
16
+ limit concurrency; multiple requests may still run in parallel
17
+ if they acquire permission within the same window.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ max_rate: int,
23
+ time_period: float,
24
+ **kwargs
25
+ ) -> None:
26
+ """
27
+ Initialize the rate-limited requester.
28
+
29
+ :param max_rate: Maximum number of requests allowed within the time window.
30
+ :param time_period: Duration (in seconds) of the rate-limiting window.
31
+ :param kwargs: Additional keyword arguments passed to :class:`AioHttpRequester`.
32
+ """
33
+
34
+ if max_rate <= 0:
35
+ raise ValueError("`max_rate` must be positive!")
36
+
37
+ if time_period <= 0:
38
+ raise ValueError("`time_period` must be positive!")
39
+
40
+ super().__init__(**kwargs)
41
+
42
+ self.max_rate = max_rate
43
+ self.time_period = time_period
44
+
45
+ self._limiter = AsyncLimiter(
46
+ max_rate=self.max_rate,
47
+ time_period=self.time_period,
48
+ )
49
+
50
+ @classmethod
51
+ def engine(cls) -> str:
52
+ return "aiohttp_rate_limit"
53
+
54
+ async def request(self, *args, **kwargs) -> ClientResponse:
55
+ """
56
+ Execute an HTTP request subject to the configured rate limit. The
57
+ coroutine will wait (non-blocking) until a rate slot becomes available,
58
+ and then delegate the call to the base :class:`AioHttpRequester`.
59
+
60
+ :return: The HTTP response object.
61
+ :raises Exception: Any exception raised by the underlying HTTP client.
62
+ """
63
+
64
+ async with self._limiter:
65
+ return await super().request(*args, **kwargs)
@@ -0,0 +1,64 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Concurrency-throttled aiohttp requester using asyncio.Semaphore."""
4
+
5
+ import asyncio
6
+ from typing import Optional
7
+
8
+ from aiohttp import ClientResponse
9
+
10
+ from core_https.requesters.aiohttp_ import AioHttpRequester
11
+
12
+
13
+ class AioHttpThrottleRequester(AioHttpRequester):
14
+ """
15
+ An `AioHttpRequester` implementation that limits the number of
16
+ concurrent in-flight HTTP requests using an `asyncio.Semaphore`. This
17
+ throttler enforces *max_concurrency* at the coroutine level within a
18
+ single event loop. Each call to :meth:`request` must acquire a semaphore
19
+ permit before dispatching the actual HTTP request.
20
+
21
+ **Notes:**
22
+
23
+ - The throttling mechanism limits only concurrent *coroutine execution*. It
24
+ does not enforce rate limiting (requests per second).
25
+ - If you override :meth:`request` in a subclass, be aware that
26
+ `await super().request(...)` calls the parent implementation (so the parent
27
+ semaphore *will* be used for the actual HTTP call). However, any code you
28
+ run **before** or **after** that `super()` call executes outside the parent's
29
+ semaphore (and therefore is not throttled).
30
+ """
31
+
32
+ def __init__(self, max_concurrency: int, **kwargs) -> None:
33
+ """
34
+ :param max_concurrency: Maximum number of concurrent requests allowed.
35
+ :param kwargs: Passed through to :class:`AioHttpRequester`.
36
+ """
37
+
38
+ super().__init__(**kwargs)
39
+ self.max_concurrency = max_concurrency
40
+ self._semaphore: Optional[asyncio.Semaphore] = None
41
+
42
+ @classmethod
43
+ def engine(cls) -> str:
44
+ return "aiohttp_throttle"
45
+
46
+ async def request(self, *args, **kwargs) -> ClientResponse:
47
+ """
48
+ Execute an HTTP request with concurrency throttling. It acquires
49
+ a semaphore permit before delegating the actual request to the
50
+ underlying :class:`AioHttpRequester` implementation.
51
+
52
+ :returns: The aiohttp response object.
53
+ :raises: Any exception raised by the underlying session.
54
+ """
55
+
56
+ if self._semaphore is None:
57
+ self._semaphore = asyncio.Semaphore(self.max_concurrency)
58
+
59
+ async with self._semaphore:
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 # type: ignore
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: Dict[str, str],
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 (429, 502, 503, 504) and within_retry:
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=[429, 502, 503, 504],
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
- session.mount("https://", adapter)
74
- session.mount("http://", adapter)
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( # type: ignore[override]
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 not pool_manager:
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 or self.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=[429, 502, 503, 504],
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 or self.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)) # type: ignore[arg-type]
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(status_code, info)
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. Mutually exclusive with text_response and content.
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. Mutually exclusive with json_response and content.
64
- content: Raw bytes to return as response content. Mutually exclusive with json_response and text_response.
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. Mutually exclusive with text_response and content.
146
- text_response: Plain text data to return. Mutually exclusive with json_response and content.
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. Mutually exclusive with json_response and text_response.
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. Mutually exclusive with text_response.
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
- else:
273
- # Simulate streaming read...
274
- res = data[:amt] if amt else data
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(hint=-1):
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, decode_content=None):
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(self):
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) -> Self:
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
- for status in cls:
226
- if status.value == code:
227
- return status
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) -> Self:
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
- for http_method in cls:
356
- if http_method.value == name:
357
- return http_method
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
- raise ValueError(f"No HTTPMethod found for name: {name}")
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: 2.0.2
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,22 @@ 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
- Requires-Dist: aiohttp<4.0.0,>=3.12.0; python_version >= "3.9"
28
- Requires-Dist: core-mixins>=2.2.2
29
- Requires-Dist: requests<3.0.0,>=2.32.3; python_version >= "3.9"
30
- Requires-Dist: urllib3<3.0.0,>=2.2.3; python_version >= "3.9"
29
+ Requires-Dist: aiohttp<4.0.0,>=3.12.0
30
+ Requires-Dist: core-mixins>=3.0.1
31
+ Requires-Dist: requests<3.0.0,>=2.32.3
32
+ Requires-Dist: urllib3<3.0.0,>=2.2.3
31
33
  Provides-Extra: dev
32
- Requires-Dist: core-dev-tools>=1.0.1; extra == "dev"
33
- Requires-Dist: core-tests>=2.0.2; extra == "dev"
34
+ Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "dev"
35
+ Requires-Dist: core-dev-tools>=1.2.1; extra == "dev"
36
+ Requires-Dist: core-tests>=2.0.5; extra == "dev"
34
37
  Requires-Dist: types-requests>=2.32.0.20250602; extra == "dev"
38
+ Provides-Extra: extras
39
+ Requires-Dist: aiolimiter<2.0.0,>=1.2.1; extra == "extras"
35
40
  Dynamic: license-file
36
41
 
37
42
  core-https
@@ -134,7 +139,13 @@ Check tests and coverage
134
139
 
135
140
  .. code-block:: shell
136
141
 
142
+ # Unit tests (mocked, no network):
137
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:
138
149
  python manager.py run-coverage
139
150
 
140
151
 
@@ -13,6 +13,8 @@ core_https.egg-info/requires.txt
13
13
  core_https.egg-info/top_level.txt
14
14
  core_https/requesters/__init__.py
15
15
  core_https/requesters/aiohttp_.py
16
+ core_https/requesters/aiohttp_rate_limit.py
17
+ core_https/requesters/aiohttp_throttle.py
16
18
  core_https/requesters/base.py
17
19
  core_https/requesters/requests_.py
18
20
  core_https/requesters/urllib3_.py
@@ -0,0 +1,13 @@
1
+ aiohttp<4.0.0,>=3.12.0
2
+ core-mixins>=3.0.1
3
+ requests<3.0.0,>=2.32.3
4
+ urllib3<3.0.0,>=2.2.3
5
+
6
+ [dev]
7
+ aiolimiter<2.0.0,>=1.2.1
8
+ core-dev-tools>=1.2.1
9
+ core-tests>=2.0.5
10
+ types-requests>=2.32.0.20250602
11
+
12
+ [extras]
13
+ aiolimiter<2.0.0,>=1.2.1
@@ -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"
@@ -1,11 +0,0 @@
1
- core-mixins>=2.2.2
2
-
3
- [:python_version >= "3.9"]
4
- aiohttp<4.0.0,>=3.12.0
5
- requests<3.0.0,>=2.32.3
6
- urllib3<3.0.0,>=2.2.3
7
-
8
- [dev]
9
- core-dev-tools>=1.0.1
10
- core-tests>=2.0.2
11
- types-requests>=2.32.0.20250602
@@ -1,63 +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.2"
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; python_version >= '3.9'",
43
- "core-mixins>=2.2.2",
44
- "requests>=2.32.3,<3.0.0; python_version >= '3.9'",
45
- "urllib3>=2.2.3,<3.0.0; python_version >= '3.9'",
46
- ]
47
-
48
- [project.optional-dependencies]
49
- dev = [
50
- "core-dev-tools>=1.0.1",
51
- "core-tests>=2.0.2",
52
- "types-requests>=2.32.0.20250602",
53
- ]
54
-
55
- [project.urls]
56
- Homepage = "https://gitlab.com/bytecode-solutions/core/core-https"
57
- Repository = "https://gitlab.com/bytecode-solutions/core/core-https"
58
- Documentation = "https://core-https.readthedocs.io/en/latest/"
59
- Issues = "https://gitlab.com/bytecode-solutions/core/core-https/-/issues"
60
- Changelog = "https://gitlab.com/bytecode-solutions/core/core-https/-/blob/master/CHANGELOG.md"
61
-
62
- [tool.setuptools.package-data]
63
- "core_https" = ["py.typed"]
File without changes
File without changes
File without changes