Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: urllib3 instead of curl #2134

Merged
merged 40 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
89cc03d
feature(urllib3): add urllib3 client
spawn-guy Oct 1, 2024
ee8710e
test(urllib3): test urllib3 client
spawn-guy Oct 1, 2024
8fc3940
test(urllib3): update http test for urllib3
spawn-guy Oct 1, 2024
6b9029a
test(urllib3): use urllib3 client instead of curl
spawn-guy Oct 1, 2024
cee8a9a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 1, 2024
5eecb41
style(urllib3): remove unused imports
spawn-guy Oct 1, 2024
78f4699
style(urllib3): fix pre-commit errors
spawn-guy Oct 1, 2024
05a3134
Merge branch 'main' into feture_urllib3
auvipy Oct 1, 2024
f268a6f
ci(urllib3): remove pycurl dependency
spawn-guy Oct 2, 2024
dc162ac
docs(urllib3): add docs
spawn-guy Oct 2, 2024
6fe3cc5
style(urllib3): fix failing gh-workflow py3.8
spawn-guy Oct 2, 2024
bb0d50f
style(urllib3): add mention of ProxyManager
spawn-guy Oct 2, 2024
de101e8
style(urllib3): fix pre-commit issues
spawn-guy Oct 2, 2024
6cc986d
Merge branch 'celery:main' into feture_urllib3
spawn-guy Oct 2, 2024
80c0ef6
Merge branch 'celery:main' into feture_urllib3
spawn-guy Oct 2, 2024
43ff836
Merge branch 'main' into feture_urllib3
spawn-guy Oct 3, 2024
ea86b85
style(pycurl): remove curl-related code
spawn-guy Oct 3, 2024
8f53eb7
Merge branch 'main' into feture_urllib3
auvipy Oct 6, 2024
f87d164
feat(urllib3): add missing request features (header, auth, ssl, proxy…
spawn-guy Oct 8, 2024
1e31395
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 8, 2024
41d30c8
fix(urllib3): improve styling
spawn-guy Oct 8, 2024
9797ddf
test(urllib3): add new tests
spawn-guy Oct 8, 2024
7d7cfd9
Merge branch 'main' into feture_urllib3
spawn-guy Oct 8, 2024
cec7a03
fix(urllib3): fix request auth
spawn-guy Oct 8, 2024
0769d07
fix(aws): validate certificate on request
spawn-guy Oct 8, 2024
e6f0164
style(): add missing exports
spawn-guy Oct 8, 2024
e9e2be7
feat(aws): add ssl certificate verification from boto
spawn-guy Oct 8, 2024
0fd6ea1
feat(urllib): try to use certifi.where() if request.ca_certs are not …
spawn-guy Oct 8, 2024
d2f3dd2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 8, 2024
f3ccaa0
Merge branch 'main' into feture_urllib3
spawn-guy Oct 9, 2024
e228472
ci(pydocstyle): add missing docstring in public class
spawn-guy Oct 9, 2024
fc20023
test(urllib3): improve test case
spawn-guy Oct 9, 2024
34aa3c4
ci(pydocstyle): fix multi-line docstring summary should start at the …
spawn-guy Oct 9, 2024
23858ed
feat(urllib3): remove assert_hostname
spawn-guy Oct 9, 2024
8bf4d99
Merge branch 'main' into feture_urllib3
spawn-guy Oct 13, 2024
ca9211d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 13, 2024
6d62a97
Merge branch 'main' into feture_urllib3
Nusnus Oct 13, 2024
91ac2af
test(boto): add test for get_cert_path returning .pem file path
spawn-guy Oct 14, 2024
34b1fc9
test(urllib3): add test for _get_pool_key_parts method
spawn-guy Oct 14, 2024
e5ae986
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions kombu/asynchronous/http/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from kombu.asynchronous import get_event_loop
from kombu.asynchronous.http.base import Headers, Request, Response
from kombu.asynchronous.http.base import BaseClient, Headers, Request, Response
from kombu.asynchronous.hub import Hub

if TYPE_CHECKING:
from kombu.asynchronous.http.curl import CurlClient

__all__ = ('Client', 'Headers', 'Response', 'Request')


def Client(hub: Hub | None = None, **kwargs: int) -> CurlClient:
def Client(hub: Hub | None = None, **kwargs: int) -> BaseClient:
"""Create new HTTP client."""
from .curl import CurlClient
return CurlClient(hub, **kwargs)
from .urllib3_client import Urllib3Client
return Urllib3Client(hub, **kwargs)


def get_client(hub: Hub | None = None, **kwargs: int) -> CurlClient:
def get_client(hub: Hub | None = None, **kwargs: int) -> BaseClient:
"""Get or create HTTP client bound to the current event loop."""
hub = hub or get_event_loop()
try:
Expand Down
94 changes: 94 additions & 0 deletions kombu/asynchronous/http/urllib3_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations

from collections import deque
from io import BytesIO

import urllib3

from kombu.asynchronous.hub import Hub, get_event_loop
from kombu.exceptions import HttpError

from .base import BaseClient

__all__ = ('Urllib3Client',)

DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; urllib3)'
EXTRA_METHODS = frozenset(['DELETE', 'OPTIONS', 'PATCH'])


class Urllib3Client(BaseClient):
"""Urllib3 HTTP Client."""

def __init__(self, hub: Hub | None = None, max_clients: int = 10):
hub = hub or get_event_loop()
super().__init__(hub)
self.max_clients = max_clients
self._http = urllib3.PoolManager(maxsize=max_clients)
self._pending = deque()
self._timeout_check_tref = self.hub.call_repeatedly(
1.0, self._timeout_check,
)

def close(self):
self._timeout_check_tref.cancel()
self._http.clear()

def add_request(self, request):
self._pending.append(request)
self._process_queue()
return request

def _timeout_check(self):
self._process_pending_requests()

def _process_pending_requests(self):
while self._pending:
request = self._pending.popleft()
self._process_request(request)

Check warning on line 47 in kombu/asynchronous/http/urllib3_client.py

View check run for this annotation

Codecov / codecov/patch

kombu/asynchronous/http/urllib3_client.py#L46-L47

Added lines #L46 - L47 were not covered by tests

def _process_request(self, request):
method = request.method
url = request.url
headers = request.headers
body = request.body

try:
response = self._http.request(
method,
url,
headers=headers,
body=body,
preload_content=False
)
buffer = BytesIO(response.data)
response_obj = self.Response(
request=request,
code=response.status,
headers=response.headers,
buffer=buffer,
effective_url=response.geturl(),
error=None
)
except urllib3.exceptions.HTTPError as e:
response_obj = self.Response(

Check warning on line 73 in kombu/asynchronous/http/urllib3_client.py

View check run for this annotation

Codecov / codecov/patch

kombu/asynchronous/http/urllib3_client.py#L72-L73

Added lines #L72 - L73 were not covered by tests
request=request,
code=599,
headers={},
buffer=None,
effective_url=None,
error=HttpError(599, str(e))
)

request.on_ready(response_obj)

def _process_queue(self):
self._process_pending_requests()

Check warning on line 85 in kombu/asynchronous/http/urllib3_client.py

View check run for this annotation

Codecov / codecov/patch

kombu/asynchronous/http/urllib3_client.py#L85

Added line #L85 was not covered by tests

def on_readable(self, fd):
pass

def on_writable(self, fd):
pass

def _setup_request(self, curl, request, buffer, headers):
pass

Check warning on line 94 in kombu/asynchronous/http/urllib3_client.py

View check run for this annotation

Codecov / codecov/patch

kombu/asynchronous/http/urllib3_client.py#L94

Added line #L94 was not covered by tests
2 changes: 1 addition & 1 deletion t/unit/asynchronous/http/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def test_as_context(self):
class test_Client:

def test_get_client(self, hub):
pytest.importorskip('pycurl')
pytest.importorskip('urllib3')
client = http.get_client()
assert client.hub is hub
client2 = http.get_client(hub)
Expand Down
108 changes: 108 additions & 0 deletions t/unit/asynchronous/http/test_urllib3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from __future__ import annotations

from io import BytesIO
from unittest.mock import Mock, patch

import pytest

import t.skip
from kombu.asynchronous.http.urllib3_client import Urllib3Client


@t.skip.if_pypy
@pytest.mark.usefixtures('hub')
class test_Urllib3Client:
class Client(Urllib3Client):
Urllib3 = Mock(name='Urllib3')

def test_max_clients_set(self):
x = self.Client(max_clients=303)
assert x.max_clients == 303

def test_init(self):
with patch(
'kombu.asynchronous.http.urllib3_client.urllib3.PoolManager'
) as _PoolManager:
x = self.Client()
assert x._http is not None
assert x._pending is not None
assert x._timeout_check_tref

_PoolManager.assert_called_with(maxsize=x.max_clients)

def test_close(self):
with patch(
'kombu.asynchronous.http.urllib3_client.urllib3.PoolManager'
):
x = self.Client()
x._timeout_check_tref = Mock(name='timeout_check_tref')
x.close()
x._timeout_check_tref.cancel.assert_called_with()
x._http.clear.assert_called_with()

def test_add_request(self):
with patch(
'kombu.asynchronous.http.urllib3_client.urllib3.PoolManager'
):
x = self.Client()
x._process_queue = Mock(name='_process_queue')
request = Mock(name='request')
x.add_request(request)
assert request in x._pending
x._process_queue.assert_called_with()

def test_timeout_check(self):
with patch(
'kombu.asynchronous.http.urllib3_client.urllib3.PoolManager'
):
hub = Mock(name='hub')
x = self.Client(hub)
x._process_pending_requests = Mock(name='process_pending')
x._timeout_check()
x._process_pending_requests.assert_called_with()

def test_process_request(self):
with (patch(
'kombu.asynchronous.http.urllib3_client.urllib3.PoolManager'
) as _PoolManager):
x = self.Client()
request = Mock(
name='request',
method='GET',
url='http://example.com',
headers={},
body=None
)
response = Mock(
name='response',
status=200,
headers={},
data=b'content'
)
response.geturl.return_value = 'http://example.com'
_PoolManager.return_value.request.return_value = response

x._process_request(request)
response_obj = x.Response(
request=request,
code=200,
headers={},
buffer=BytesIO(b'content'),
effective_url='http://example.com',
error=None
)
request.on_ready.assert_called()
called_response = request.on_ready.call_args[0][0]
assert called_response.code == response_obj.code
assert called_response.headers == response_obj.headers
assert (
called_response.buffer.getvalue() ==
response_obj.buffer.getvalue()
)
assert called_response.effective_url == response_obj.effective_url
assert called_response.error == response_obj.error

def test_on_readable_on_writable(self):
x = self.Client()
x.on_readable(Mock(name='fd'))
x.on_writable(Mock(name='fd'))
Loading