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.
Files changed (29) hide show
  1. {core_https-2.0.3 → core_https-3.0.0}/PKG-INFO +12 -4
  2. {core_https-2.0.3 → core_https-3.0.0}/README.rst +6 -0
  3. {core_https-2.0.3 → core_https-3.0.0}/core_https/__init__.py +3 -1
  4. {core_https-2.0.3 → core_https-3.0.0}/core_https/exceptions.py +9 -2
  5. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_.py +22 -6
  6. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_rate_limit.py +9 -4
  7. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/aiohttp_throttle.py +13 -3
  8. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/base.py +9 -5
  9. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/requests_.py +9 -8
  10. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/urllib3_.py +11 -6
  11. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/decorators.py +12 -6
  12. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/requests_.py +2 -1
  13. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/urllib3_.py +6 -7
  14. {core_https-2.0.3 → core_https-3.0.0}/core_https/utils.py +16 -14
  15. {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/PKG-INFO +12 -4
  16. {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/requires.txt +3 -3
  17. core_https-3.0.0/pyproject.toml +133 -0
  18. core_https-2.0.3/pyproject.toml +0 -68
  19. {core_https-2.0.3 → core_https-3.0.0}/LICENSE +0 -0
  20. {core_https-2.0.3 → core_https-3.0.0}/core_https/py.typed +0 -0
  21. {core_https-2.0.3 → core_https-3.0.0}/core_https/requesters/__init__.py +0 -0
  22. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/__init__.py +0 -0
  23. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/aiohttp_.py +0 -0
  24. {core_https-2.0.3 → core_https-3.0.0}/core_https/tests/base.py +0 -0
  25. {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/SOURCES.txt +0 -0
  26. {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/dependency_links.txt +0 -0
  27. {core_https-2.0.3 → core_https-3.0.0}/core_https.egg-info/top_level.txt +0 -0
  28. {core_https-2.0.3 → core_https-3.0.0}/setup.cfg +0 -0
  29. {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: 2.0.3
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>=2.2.2
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.0.1; extra == "dev"
34
- Requires-Dist: core-tests>=2.0.2; extra == "dev"
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 # 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
@@ -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.aiohttp_throttle import AioHttpThrottleRequester
8
+ from core_https.requesters.aiohttp_ import AioHttpRequester
7
9
 
8
10
 
9
- class AioHttpRateLimitRequester(AioHttpThrottleRequester):
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__(max_concurrency=max_concurrency, **kwargs)
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 = asyncio.Semaphore(self.max_concurrency)
40
+ self._semaphore: Optional[asyncio.Semaphore] = None
38
41
 
39
42
  @classmethod
40
- def registration_key(cls) -> str:
41
- return cls.__name__
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 # 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.3
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>=2.2.2
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.0.1; extra == "dev"
34
- Requires-Dist: core-tests>=2.0.2; extra == "dev"
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.2.2
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.0.1
9
- core-tests>=2.0.2
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"
@@ -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