Skip to content

Commit 024a523

Browse files
Merge pull request #2877 from VWS-Python/adamtheturtle/switchable-transport
Add switchable HTTP transports
2 parents dfde7d1 + b977b52 commit 024a523

File tree

9 files changed

+293
-61
lines changed

9 files changed

+293
-61
lines changed

docs/source/api-reference.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ API Reference
2020
.. automodule:: vws.response
2121
:undoc-members:
2222
:members:
23+
24+
.. automodule:: vws.transports
25+
:undoc-members:
26+
:members:

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dynamic = [
3333
]
3434
dependencies = [
3535
"beartype>=0.22.9",
36+
"httpx>=0.28.0",
3637
"requests>=2.32.3",
3738
"urllib3>=2.2.3",
3839
"vws-auth-tools>=2024.7.12",
@@ -357,6 +358,8 @@ ignore_path = [
357358
# Ideally we would limit the paths to the source code where we want to ignore names,
358359
# but Vulture does not enable this.
359360
ignore_names = [
361+
# Public API classes imported by users from vws.transports
362+
"HTTPXTransport",
360363
# pytest configuration
361364
"pytest_collect_file",
362365
"pytest_collection_modifyitems",

spelling_private_dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ hmac
5454
html
5555
http
5656
https
57+
httpx
5758
iff
5859
io
5960
issuecomment

src/vws/_vws_request.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
API.
33
"""
44

5-
import requests
65
from beartype import BeartypeConf, beartype
76
from vws_auth_tools import authorization_header, rfc_1123_date
87

98
from vws.response import Response
9+
from vws.transports import Transport
1010

1111

1212
@beartype(conf=BeartypeConf(is_pep484_tower=True))
@@ -21,27 +21,30 @@ def target_api_request(
2121
base_vws_url: str,
2222
request_timeout_seconds: float | tuple[float, float],
2323
extra_headers: dict[str, str],
24+
transport: Transport,
2425
) -> Response:
2526
"""Make a request to the Vuforia Target API.
2627
27-
This uses `requests` to make a request against https://vws.vuforia.com.
28-
2928
Args:
3029
content_type: The content type of the request.
3130
server_access_key: A VWS server access key.
3231
server_secret_key: A VWS server secret key.
33-
method: The HTTP method which will be used in the request.
34-
data: The request body which will be used in the request.
35-
request_path: The path to the endpoint which will be used in the
32+
method: The HTTP method which will be used in the
33+
request.
34+
data: The request body which will be used in the
3635
request.
36+
request_path: The path to the endpoint which will be
37+
used in the request.
3738
base_vws_url: The base URL for the VWS API.
38-
request_timeout_seconds: The timeout for the request, as used by
39-
``requests.request``. This can be a float to set both the
40-
connect and read timeouts, or a (connect, read) tuple.
41-
extra_headers: Additional headers to include in the request.
39+
request_timeout_seconds: The timeout for the request.
40+
This can be a float to set both the connect and
41+
read timeouts, or a (connect, read) tuple.
42+
extra_headers: Additional headers to include in the
43+
request.
44+
transport: The HTTP transport to use for the request.
4245
4346
Returns:
44-
The response to the request made by `requests`.
47+
The response to the request.
4548
"""
4649
date_string = rfc_1123_date()
4750

@@ -64,20 +67,10 @@ def target_api_request(
6467

6568
url = base_vws_url.rstrip("/") + request_path
6669

67-
requests_response = requests.request(
70+
return transport(
6871
method=method,
6972
url=url,
7073
headers=headers,
7174
data=data,
7275
timeout=request_timeout_seconds,
7376
)
74-
75-
return Response(
76-
text=requests_response.text,
77-
url=requests_response.url,
78-
status_code=requests_response.status_code,
79-
headers=dict(requests_response.headers),
80-
request_body=requests_response.request.body,
81-
tell_position=requests_response.raw.tell(),
82-
content=bytes(requests_response.content),
83-
)

src/vws/query.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from http import HTTPMethod, HTTPStatus
77
from typing import Any, BinaryIO
88

9-
import requests
109
from beartype import BeartypeConf, beartype
1110
from urllib3.filepost import encode_multipart_formdata
1211
from vws_auth_tools import authorization_header, rfc_1123_date
@@ -24,7 +23,7 @@
2423
)
2524
from vws.include_target_data import CloudRecoIncludeTargetData
2625
from vws.reports import QueryResult, TargetData
27-
from vws.response import Response
26+
from vws.transports import RequestsTransport, Transport
2827

2928
_ImageType = io.BytesIO | BinaryIO
3029

@@ -50,21 +49,26 @@ def __init__(
5049
client_secret_key: str,
5150
base_vwq_url: str = "https://cloudreco.vuforia.com",
5251
request_timeout_seconds: float | tuple[float, float] = 30.0,
52+
transport: Transport | None = None,
5353
) -> None:
5454
"""
5555
Args:
5656
client_access_key: A VWS client access key.
5757
client_secret_key: A VWS client secret key.
5858
base_vwq_url: The base URL for the VWQ API.
59-
request_timeout_seconds: The timeout for each HTTP request, as
60-
used by ``requests.request``. This can be a float to set
61-
both the connect and read timeouts, or a (connect, read)
62-
tuple.
59+
request_timeout_seconds: The timeout for each
60+
HTTP request. This can be a float to set both
61+
the connect and read timeouts, or a
62+
(connect, read) tuple.
63+
transport: The HTTP transport to use for
64+
requests. Defaults to
65+
``RequestsTransport()``.
6366
"""
6467
self._client_access_key = client_access_key
6568
self._client_secret_key = client_secret_key
6669
self._base_vwq_url = base_vwq_url
6770
self._request_timeout_seconds = request_timeout_seconds
71+
self._transport = transport or RequestsTransport()
6872

6973
def query(
7074
self,
@@ -143,22 +147,13 @@ def query(
143147
"Content-Type": content_type_header,
144148
}
145149

146-
requests_response = requests.request(
150+
response = self._transport(
147151
method=method,
148152
url=self._base_vwq_url.rstrip("/") + request_path,
149153
headers=headers,
150154
data=content,
151155
timeout=self._request_timeout_seconds,
152156
)
153-
response = Response(
154-
text=requests_response.text,
155-
url=requests_response.url,
156-
status_code=requests_response.status_code,
157-
headers=dict(requests_response.headers),
158-
request_body=requests_response.request.body,
159-
tell_position=requests_response.raw.tell(),
160-
content=bytes(requests_response.content),
161-
)
162157

163158
if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
164159
raise RequestEntityTooLargeError(response=response)

src/vws/transports.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""HTTP transport implementations for VWS clients."""
2+
3+
from typing import Protocol, runtime_checkable
4+
5+
import httpx
6+
import requests
7+
from beartype import BeartypeConf, beartype
8+
9+
from vws.response import Response
10+
11+
12+
@runtime_checkable
13+
class Transport(Protocol):
14+
"""Protocol for HTTP transports used by VWS clients.
15+
16+
A transport is a callable that makes an HTTP request and
17+
returns a ``Response``.
18+
"""
19+
20+
def __call__(
21+
self,
22+
*,
23+
method: str,
24+
url: str,
25+
headers: dict[str, str],
26+
data: bytes,
27+
timeout: float | tuple[float, float],
28+
) -> Response:
29+
"""Make an HTTP request.
30+
31+
Args:
32+
method: The HTTP method (e.g. "GET", "POST").
33+
url: The full URL to request.
34+
headers: Headers to send with the request.
35+
data: The request body as bytes.
36+
timeout: The timeout for the request. A float
37+
sets both the connect and read timeouts. A
38+
(connect, read) tuple sets them individually.
39+
40+
Returns:
41+
A Response populated from the HTTP response.
42+
"""
43+
... # pylint: disable=unnecessary-ellipsis
44+
45+
46+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
47+
class RequestsTransport:
48+
"""HTTP transport using the ``requests`` library.
49+
50+
This is the default transport.
51+
"""
52+
53+
def __call__(
54+
self,
55+
*,
56+
method: str,
57+
url: str,
58+
headers: dict[str, str],
59+
data: bytes,
60+
timeout: float | tuple[float, float],
61+
) -> Response:
62+
"""Make an HTTP request using ``requests``.
63+
64+
Args:
65+
method: The HTTP method.
66+
url: The full URL.
67+
headers: Request headers.
68+
data: The request body.
69+
timeout: The request timeout.
70+
71+
Returns:
72+
A Response populated from the requests response.
73+
"""
74+
requests_response = requests.request(
75+
method=method,
76+
url=url,
77+
headers=headers,
78+
data=data,
79+
timeout=timeout,
80+
)
81+
82+
return Response(
83+
text=requests_response.text,
84+
url=requests_response.url,
85+
status_code=requests_response.status_code,
86+
headers=dict(requests_response.headers),
87+
request_body=requests_response.request.body,
88+
tell_position=requests_response.raw.tell(),
89+
content=bytes(requests_response.content),
90+
)
91+
92+
93+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
94+
class HTTPXTransport:
95+
"""HTTP transport using the ``httpx`` library.
96+
97+
Use this transport for environments where ``httpx`` is
98+
preferred over ``requests``.
99+
"""
100+
101+
def __call__(
102+
self,
103+
*,
104+
method: str,
105+
url: str,
106+
headers: dict[str, str],
107+
data: bytes,
108+
timeout: float | tuple[float, float],
109+
) -> Response:
110+
"""Make an HTTP request using ``httpx``.
111+
112+
Args:
113+
method: The HTTP method.
114+
url: The full URL.
115+
headers: Request headers.
116+
data: The request body.
117+
timeout: The request timeout.
118+
119+
Returns:
120+
A Response populated from the httpx response.
121+
"""
122+
if isinstance(timeout, tuple):
123+
connect_timeout, read_timeout = timeout
124+
httpx_timeout = httpx.Timeout(
125+
connect=connect_timeout,
126+
read=read_timeout,
127+
write=None,
128+
pool=None,
129+
)
130+
else:
131+
httpx_timeout = httpx.Timeout(
132+
connect=timeout,
133+
read=timeout,
134+
write=None,
135+
pool=None,
136+
)
137+
138+
httpx_response = httpx.request(
139+
method=method,
140+
url=url,
141+
headers=headers,
142+
content=data,
143+
timeout=httpx_timeout,
144+
follow_redirects=True,
145+
)
146+
147+
content = bytes(httpx_response.content)
148+
request_content = httpx_response.request.content
149+
150+
return Response(
151+
text=httpx_response.text,
152+
url=str(object=httpx_response.url),
153+
status_code=httpx_response.status_code,
154+
headers=dict(httpx_response.headers),
155+
request_body=bytes(request_content) or None,
156+
tell_position=len(content),
157+
content=content,
158+
)

src/vws/vumark_service.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
TooManyRequestsError,
2121
UnknownTargetError,
2222
)
23+
from vws.transports import RequestsTransport, Transport
2324
from vws.vumark_accept import VuMarkAccept
2425

2526

@@ -29,25 +30,31 @@ class VuMarkService:
2930

3031
def __init__(
3132
self,
33+
*,
3234
server_access_key: str,
3335
server_secret_key: str,
3436
base_vws_url: str = "https://vws.vuforia.com",
3537
request_timeout_seconds: float | tuple[float, float] = 30.0,
38+
transport: Transport | None = None,
3639
) -> None:
3740
"""
3841
Args:
3942
server_access_key: A VWS server access key.
4043
server_secret_key: A VWS server secret key.
4144
base_vws_url: The base URL for the VWS API.
42-
request_timeout_seconds: The timeout for each HTTP request, as
43-
used by ``requests.request``. This can be a float to set
44-
both the connect and read timeouts, or a (connect, read)
45-
tuple.
45+
request_timeout_seconds: The timeout for each
46+
HTTP request. This can be a float to set both
47+
the connect and read timeouts, or a
48+
(connect, read) tuple.
49+
transport: The HTTP transport to use for
50+
requests. Defaults to
51+
``RequestsTransport()``.
4652
"""
4753
self._server_access_key = server_access_key
4854
self._server_secret_key = server_secret_key
4955
self._base_vws_url = base_vws_url
5056
self._request_timeout_seconds = request_timeout_seconds
57+
self._transport = transport or RequestsTransport()
5158

5259
def generate_vumark_instance(
5360
self,
@@ -109,6 +116,7 @@ def generate_vumark_instance(
109116
base_vws_url=self._base_vws_url,
110117
request_timeout_seconds=self._request_timeout_seconds,
111118
extra_headers={"Accept": accept},
119+
transport=self._transport,
112120
)
113121

114122
if (

0 commit comments

Comments
 (0)