vws-python 2026.2.22__tar.gz → 2026.2.24__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 (60) hide show
  1. {vws_python-2026.2.22 → vws_python-2026.2.24}/CHANGELOG.rst +8 -0
  2. {vws_python-2026.2.22/src/vws_python.egg-info → vws_python-2026.2.24}/PKG-INFO +8 -7
  3. {vws_python-2026.2.22 → vws_python-2026.2.24}/conftest.py +2 -0
  4. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/api-reference.rst +4 -0
  5. {vws_python-2026.2.22 → vws_python-2026.2.24}/pyproject.toml +9 -6
  6. {vws_python-2026.2.22 → vws_python-2026.2.24}/spelling_private_dict.txt +1 -0
  7. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/_vws_request.py +15 -22
  8. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/query.py +11 -16
  9. vws_python-2026.2.24/src/vws/transports.py +158 -0
  10. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/vumark_service.py +12 -4
  11. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/vws.py +32 -19
  12. {vws_python-2026.2.22 → vws_python-2026.2.24/src/vws_python.egg-info}/PKG-INFO +8 -7
  13. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws_python.egg-info/SOURCES.txt +2 -0
  14. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws_python.egg-info/requires.txt +7 -6
  15. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/conftest.py +6 -3
  16. vws_python-2026.2.24/tests/test_transports.py +61 -0
  17. {vws_python-2026.2.22 → vws_python-2026.2.24}/.git_archival.txt +0 -0
  18. {vws_python-2026.2.22 → vws_python-2026.2.24}/.gitattributes +0 -0
  19. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/dependabot.yml +0 -0
  20. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/workflows/ci.yml +0 -0
  21. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/workflows/dependabot-merge.yml +0 -0
  22. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/workflows/lint.yml +0 -0
  23. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/workflows/publish-site.yml +0 -0
  24. {vws_python-2026.2.22 → vws_python-2026.2.24}/.github/workflows/release.yml +0 -0
  25. {vws_python-2026.2.22 → vws_python-2026.2.24}/.gitignore +0 -0
  26. {vws_python-2026.2.22 → vws_python-2026.2.24}/.pre-commit-config.yaml +0 -0
  27. {vws_python-2026.2.22 → vws_python-2026.2.24}/.prettierrc +0 -0
  28. {vws_python-2026.2.22 → vws_python-2026.2.24}/.vscode/extensions.json +0 -0
  29. {vws_python-2026.2.22 → vws_python-2026.2.24}/.vscode/settings.json +0 -0
  30. {vws_python-2026.2.22 → vws_python-2026.2.24}/CODE_OF_CONDUCT.rst +0 -0
  31. {vws_python-2026.2.22 → vws_python-2026.2.24}/LICENSE +0 -0
  32. {vws_python-2026.2.22 → vws_python-2026.2.24}/README.rst +0 -0
  33. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/__init__.py +0 -0
  34. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/changelog.rst +0 -0
  35. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/conf.py +0 -0
  36. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/contributing.rst +0 -0
  37. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/exceptions.rst +0 -0
  38. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/index.rst +0 -0
  39. {vws_python-2026.2.22 → vws_python-2026.2.24}/docs/source/release-process.rst +0 -0
  40. {vws_python-2026.2.22 → vws_python-2026.2.24}/setup.cfg +0 -0
  41. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/__init__.py +0 -0
  42. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/exceptions/__init__.py +0 -0
  43. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/exceptions/base_exceptions.py +0 -0
  44. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
  45. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/exceptions/custom_exceptions.py +0 -0
  46. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/exceptions/vws_exceptions.py +0 -0
  47. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/include_target_data.py +0 -0
  48. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/py.typed +0 -0
  49. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/reports.py +0 -0
  50. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/response.py +0 -0
  51. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws/vumark_accept.py +0 -0
  52. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws_python.egg-info/dependency_links.txt +0 -0
  53. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws_python.egg-info/not-zip-safe +0 -0
  54. {vws_python-2026.2.22 → vws_python-2026.2.24}/src/vws_python.egg-info/top_level.txt +0 -0
  55. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/__init__.py +0 -0
  56. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/test_cloud_reco_exceptions.py +0 -0
  57. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/test_query.py +0 -0
  58. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/test_vws.py +0 -0
  59. {vws_python-2026.2.22 → vws_python-2026.2.24}/tests/test_vws_exceptions.py +0 -0
  60. {vws_python-2026.2.22 → vws_python-2026.2.24}/zizmor.yml +0 -0
@@ -4,6 +4,14 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.24
8
+ ----------
9
+
10
+
11
+ 2026.02.23
12
+ ----------
13
+
14
+
7
15
  2026.02.22
8
16
  ----------
9
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python
3
- Version: 2026.2.22
3
+ Version: 2026.2.24
4
4
  Summary: Interact with the Vuforia Web Services (VWS) API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -18,6 +18,7 @@ Requires-Python: >=3.13
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: beartype>=0.22.9
21
+ Requires-Dist: httpx>=0.28.0
21
22
  Requires-Dist: requests>=2.32.3
22
23
  Requires-Dist: urllib3>=2.2.3
23
24
  Requires-Dist: vws-auth-tools>=2024.7.12
@@ -36,16 +37,16 @@ Requires-Dist: prek==0.3.3; extra == "dev"
36
37
  Requires-Dist: pydocstringformatter==0.7.5; extra == "dev"
37
38
  Requires-Dist: pydocstyle==6.3; extra == "dev"
38
39
  Requires-Dist: pygments==2.19.2; extra == "dev"
39
- Requires-Dist: pylint[spelling]==4.0.4; extra == "dev"
40
+ Requires-Dist: pylint[spelling]==4.0.5; extra == "dev"
40
41
  Requires-Dist: pylint-per-file-ignores==3.2.0; extra == "dev"
41
- Requires-Dist: pyproject-fmt==2.16.1; extra == "dev"
42
- Requires-Dist: pyrefly==0.53.0; extra == "dev"
42
+ Requires-Dist: pyproject-fmt==2.16.2; extra == "dev"
43
+ Requires-Dist: pyrefly==0.54.0; extra == "dev"
43
44
  Requires-Dist: pyright==1.1.408; extra == "dev"
44
45
  Requires-Dist: pyroma==5.0.1; extra == "dev"
45
46
  Requires-Dist: pytest==9.0.2; extra == "dev"
46
47
  Requires-Dist: pytest-cov==7.0.0; extra == "dev"
47
48
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
48
- Requires-Dist: ruff==0.15.1; extra == "dev"
49
+ Requires-Dist: ruff==0.15.2; extra == "dev"
49
50
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
50
51
  Requires-Dist: shfmt-py==3.12.0.2; extra == "dev"
51
52
  Requires-Dist: sphinx==9.1.0; extra == "dev"
@@ -57,10 +58,10 @@ Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
57
58
  Requires-Dist: sybil==9.3.0; extra == "dev"
58
59
  Requires-Dist: torch>=2.5.1; extra == "dev"
59
60
  Requires-Dist: torchvision>=0.20.1; extra == "dev"
60
- Requires-Dist: ty==0.0.17; extra == "dev"
61
+ Requires-Dist: ty==0.0.18; extra == "dev"
61
62
  Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
62
63
  Requires-Dist: vulture==2.14; extra == "dev"
63
- Requires-Dist: vws-python-mock==2026.2.22.2; extra == "dev"
64
+ Requires-Dist: vws-python-mock==2026.2.22.3; extra == "dev"
64
65
  Requires-Dist: vws-test-fixtures==2023.3.5; extra == "dev"
65
66
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
67
  Requires-Dist: zizmor==1.22.0; extra == "dev"
@@ -28,6 +28,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
28
28
 
29
29
  @pytest.fixture(name="make_image_file")
30
30
  def fixture_make_image_file(
31
+ *,
31
32
  high_quality_image: io.BytesIO,
32
33
  ) -> Generator[None]:
33
34
  """Make an image file available in the test directory.
@@ -43,6 +44,7 @@ def fixture_make_image_file(
43
44
 
44
45
  @pytest.fixture(name="mock_vws")
45
46
  def fixture_mock_vws(
47
+ *,
46
48
  monkeypatch: pytest.MonkeyPatch,
47
49
  ) -> Generator[None]:
48
50
  """Yield a mock VWS.
@@ -20,3 +20,7 @@ API Reference
20
20
  .. automodule:: vws.response
21
21
  :undoc-members:
22
22
  :members:
23
+
24
+ .. automodule:: vws.transports
25
+ :undoc-members:
26
+ :members:
@@ -33,6 +33,7 @@ dynamic = [
33
33
  ]
34
34
  dependencies = [
35
35
  "beartype>=0.22.9",
36
+ "httpx>=0.28.0",
36
37
  "requests>=2.32.3",
37
38
  "urllib3>=2.2.3",
38
39
  "vws-auth-tools>=2024.7.12",
@@ -52,16 +53,16 @@ optional-dependencies.dev = [
52
53
  "pydocstringformatter==0.7.5",
53
54
  "pydocstyle==6.3",
54
55
  "pygments==2.19.2",
55
- "pylint[spelling]==4.0.4",
56
+ "pylint[spelling]==4.0.5",
56
57
  "pylint-per-file-ignores==3.2.0",
57
- "pyproject-fmt==2.16.1",
58
- "pyrefly==0.53.0",
58
+ "pyproject-fmt==2.16.2",
59
+ "pyrefly==0.54.0",
59
60
  "pyright==1.1.408",
60
61
  "pyroma==5.0.1",
61
62
  "pytest==9.0.2",
62
63
  "pytest-cov==7.0.0",
63
64
  "pyyaml==6.0.3",
64
- "ruff==0.15.1",
65
+ "ruff==0.15.2",
65
66
  # We add shellcheck-py not only for shell scripts and shell code blocks,
66
67
  # but also because having it installed means that ``actionlint-py`` will
67
68
  # use it to lint shell commands in GitHub workflow files.
@@ -79,10 +80,10 @@ optional-dependencies.dev = [
79
80
  # See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
80
81
  "torch>=2.5.1",
81
82
  "torchvision>=0.20.1",
82
- "ty==0.0.17",
83
+ "ty==0.0.18",
83
84
  "types-requests==2.32.4.20260107",
84
85
  "vulture==2.14",
85
- "vws-python-mock==2026.2.22.2",
86
+ "vws-python-mock==2026.2.22.3",
86
87
  "vws-test-fixtures==2023.3.5",
87
88
  "yamlfix==1.19.1",
88
89
  "zizmor==1.22.0",
@@ -357,6 +358,8 @@ ignore_path = [
357
358
  # Ideally we would limit the paths to the source code where we want to ignore names,
358
359
  # but Vulture does not enable this.
359
360
  ignore_names = [
361
+ # Public API classes imported by users from vws.transports
362
+ "HTTPXTransport",
360
363
  # pytest configuration
361
364
  "pytest_collect_file",
362
365
  "pytest_collection_modifyitems",
@@ -54,6 +54,7 @@ hmac
54
54
  html
55
55
  http
56
56
  https
57
+ httpx
57
58
  iff
58
59
  io
59
60
  issuecomment
@@ -2,11 +2,11 @@
2
2
  API.
3
3
  """
4
4
 
5
- import requests
6
5
  from beartype import BeartypeConf, beartype
7
6
  from vws_auth_tools import authorization_header, rfc_1123_date
8
7
 
9
8
  from vws.response import Response
9
+ from vws.transports import Transport
10
10
 
11
11
 
12
12
  @beartype(conf=BeartypeConf(is_pep484_tower=True))
@@ -21,27 +21,30 @@ def target_api_request(
21
21
  base_vws_url: str,
22
22
  request_timeout_seconds: float | tuple[float, float],
23
23
  extra_headers: dict[str, str],
24
+ transport: Transport,
24
25
  ) -> Response:
25
26
  """Make a request to the Vuforia Target API.
26
27
 
27
- This uses `requests` to make a request against https://vws.vuforia.com.
28
-
29
28
  Args:
30
29
  content_type: The content type of the request.
31
30
  server_access_key: A VWS server access key.
32
31
  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
36
35
  request.
36
+ request_path: The path to the endpoint which will be
37
+ used in the request.
37
38
  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.
42
45
 
43
46
  Returns:
44
- The response to the request made by `requests`.
47
+ The response to the request.
45
48
  """
46
49
  date_string = rfc_1123_date()
47
50
 
@@ -64,20 +67,10 @@ def target_api_request(
64
67
 
65
68
  url = base_vws_url.rstrip("/") + request_path
66
69
 
67
- requests_response = requests.request(
70
+ return transport(
68
71
  method=method,
69
72
  url=url,
70
73
  headers=headers,
71
74
  data=data,
72
75
  timeout=request_timeout_seconds,
73
76
  )
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
- )
@@ -6,7 +6,6 @@ import json
6
6
  from http import HTTPMethod, HTTPStatus
7
7
  from typing import Any, BinaryIO
8
8
 
9
- import requests
10
9
  from beartype import BeartypeConf, beartype
11
10
  from urllib3.filepost import encode_multipart_formdata
12
11
  from vws_auth_tools import authorization_header, rfc_1123_date
@@ -24,7 +23,7 @@ from vws.exceptions.custom_exceptions import (
24
23
  )
25
24
  from vws.include_target_data import CloudRecoIncludeTargetData
26
25
  from vws.reports import QueryResult, TargetData
27
- from vws.response import Response
26
+ from vws.transports import RequestsTransport, Transport
28
27
 
29
28
  _ImageType = io.BytesIO | BinaryIO
30
29
 
@@ -50,21 +49,26 @@ class CloudRecoService:
50
49
  client_secret_key: str,
51
50
  base_vwq_url: str = "https://cloudreco.vuforia.com",
52
51
  request_timeout_seconds: float | tuple[float, float] = 30.0,
52
+ transport: Transport | None = None,
53
53
  ) -> None:
54
54
  """
55
55
  Args:
56
56
  client_access_key: A VWS client access key.
57
57
  client_secret_key: A VWS client secret key.
58
58
  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()``.
63
66
  """
64
67
  self._client_access_key = client_access_key
65
68
  self._client_secret_key = client_secret_key
66
69
  self._base_vwq_url = base_vwq_url
67
70
  self._request_timeout_seconds = request_timeout_seconds
71
+ self._transport = transport or RequestsTransport()
68
72
 
69
73
  def query(
70
74
  self,
@@ -143,22 +147,13 @@ class CloudRecoService:
143
147
  "Content-Type": content_type_header,
144
148
  }
145
149
 
146
- requests_response = requests.request(
150
+ response = self._transport(
147
151
  method=method,
148
152
  url=self._base_vwq_url.rstrip("/") + request_path,
149
153
  headers=headers,
150
154
  data=content,
151
155
  timeout=self._request_timeout_seconds,
152
156
  )
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
- )
162
157
 
163
158
  if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
164
159
  raise RequestEntityTooLargeError(response=response)
@@ -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
+ )
@@ -20,6 +20,7 @@ from vws.exceptions.vws_exceptions import (
20
20
  TooManyRequestsError,
21
21
  UnknownTargetError,
22
22
  )
23
+ from vws.transports import RequestsTransport, Transport
23
24
  from vws.vumark_accept import VuMarkAccept
24
25
 
25
26
 
@@ -29,25 +30,31 @@ class VuMarkService:
29
30
 
30
31
  def __init__(
31
32
  self,
33
+ *,
32
34
  server_access_key: str,
33
35
  server_secret_key: str,
34
36
  base_vws_url: str = "https://vws.vuforia.com",
35
37
  request_timeout_seconds: float | tuple[float, float] = 30.0,
38
+ transport: Transport | None = None,
36
39
  ) -> None:
37
40
  """
38
41
  Args:
39
42
  server_access_key: A VWS server access key.
40
43
  server_secret_key: A VWS server secret key.
41
44
  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()``.
46
52
  """
47
53
  self._server_access_key = server_access_key
48
54
  self._server_secret_key = server_secret_key
49
55
  self._base_vws_url = base_vws_url
50
56
  self._request_timeout_seconds = request_timeout_seconds
57
+ self._transport = transport or RequestsTransport()
51
58
 
52
59
  def generate_vumark_instance(
53
60
  self,
@@ -109,6 +116,7 @@ class VuMarkService:
109
116
  base_vws_url=self._base_vws_url,
110
117
  request_timeout_seconds=self._request_timeout_seconds,
111
118
  extra_headers={"Accept": accept},
119
+ transport=self._transport,
112
120
  )
113
121
 
114
122
  if (
@@ -43,6 +43,7 @@ from vws.reports import (
43
43
  TargetSummaryReport,
44
44
  )
45
45
  from vws.response import Response
46
+ from vws.transports import RequestsTransport, Transport
46
47
 
47
48
  _ImageType = io.BytesIO | BinaryIO
48
49
 
@@ -68,21 +69,26 @@ class VWS:
68
69
  server_secret_key: str,
69
70
  base_vws_url: str = "https://vws.vuforia.com",
70
71
  request_timeout_seconds: float | tuple[float, float] = 30.0,
72
+ transport: Transport | None = None,
71
73
  ) -> None:
72
74
  """
73
75
  Args:
74
76
  server_access_key: A VWS server access key.
75
77
  server_secret_key: A VWS server secret key.
76
78
  base_vws_url: The base URL for the VWS API.
77
- request_timeout_seconds: The timeout for each HTTP request, as
78
- used by ``requests.request``. This can be a float to set
79
- both the connect and read timeouts, or a (connect, read)
80
- tuple.
79
+ request_timeout_seconds: The timeout for each
80
+ HTTP request. This can be a float to set both
81
+ the connect and read timeouts, or a
82
+ (connect, read) tuple.
83
+ transport: The HTTP transport to use for
84
+ requests. Defaults to
85
+ ``RequestsTransport()``.
81
86
  """
82
87
  self._server_access_key = server_access_key
83
88
  self._server_secret_key = server_secret_key
84
89
  self._base_vws_url = base_vws_url
85
90
  self._request_timeout_seconds = request_timeout_seconds
91
+ self._transport = transport or RequestsTransport()
86
92
 
87
93
  def make_request(
88
94
  self,
@@ -96,29 +102,31 @@ class VWS:
96
102
  ) -> Response:
97
103
  """Make a request to the Vuforia Target API.
98
104
 
99
- This uses `requests` to make a request against Vuforia.
100
-
101
105
  Args:
102
- method: The HTTP method which will be used in the request.
103
- data: The request body which will be used in the request.
104
- request_path: The path to the endpoint which will be used in the
106
+ method: The HTTP method which will be used in
107
+ the request.
108
+ data: The request body which will be used in the
105
109
  request.
106
- expected_result_code: See "VWS API Result Codes" on
110
+ request_path: The path to the endpoint which
111
+ will be used in the request.
112
+ expected_result_code: See
113
+ "VWS API Result Codes" on
107
114
  https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api.
108
115
  content_type: The content type of the request.
109
- extra_headers: Additional headers to include in the request.
116
+ extra_headers: Additional headers to include in
117
+ the request.
110
118
 
111
119
  Returns:
112
- The response to the request made by `requests`.
120
+ The response to the request.
113
121
 
114
122
  Raises:
115
- ~vws.exceptions.custom_exceptions.ServerError: There is an error
116
- with Vuforia's servers.
117
- ~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
118
- rate limiting access.
119
- json.JSONDecodeError: The server did not respond with valid JSON.
120
- This may happen if the server address is not a valid Vuforia
121
- server.
123
+ ~vws.exceptions.custom_exceptions.ServerError:
124
+ There is an error with Vuforia's servers.
125
+ ~vws.exceptions.vws_exceptions.TooManyRequestsError:
126
+ Vuforia is rate limiting access.
127
+ json.JSONDecodeError: The server did not respond
128
+ with valid JSON. This may happen if the
129
+ server address is not a valid Vuforia server.
122
130
  """
123
131
  response = target_api_request(
124
132
  content_type=content_type,
@@ -130,6 +138,7 @@ class VWS:
130
138
  base_vws_url=self._base_vws_url,
131
139
  request_timeout_seconds=self._request_timeout_seconds,
132
140
  extra_headers=extra_headers or {},
141
+ transport=self._transport,
133
142
  )
134
143
 
135
144
  if (
@@ -343,6 +352,10 @@ class VWS:
343
352
  while True:
344
353
  report = self.get_target_summary_report(target_id=target_id)
345
354
  if report.status != TargetStatuses.PROCESSING:
355
+ # Guard against the target still being seen as
356
+ # processing by other endpoints due to eventual
357
+ # consistency.
358
+ time.sleep(seconds_between_requests)
346
359
  return
347
360
 
348
361
  elapsed_time = time.monotonic() - start_time
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python
3
- Version: 2026.2.22
3
+ Version: 2026.2.24
4
4
  Summary: Interact with the Vuforia Web Services (VWS) API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -18,6 +18,7 @@ Requires-Python: >=3.13
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: beartype>=0.22.9
21
+ Requires-Dist: httpx>=0.28.0
21
22
  Requires-Dist: requests>=2.32.3
22
23
  Requires-Dist: urllib3>=2.2.3
23
24
  Requires-Dist: vws-auth-tools>=2024.7.12
@@ -36,16 +37,16 @@ Requires-Dist: prek==0.3.3; extra == "dev"
36
37
  Requires-Dist: pydocstringformatter==0.7.5; extra == "dev"
37
38
  Requires-Dist: pydocstyle==6.3; extra == "dev"
38
39
  Requires-Dist: pygments==2.19.2; extra == "dev"
39
- Requires-Dist: pylint[spelling]==4.0.4; extra == "dev"
40
+ Requires-Dist: pylint[spelling]==4.0.5; extra == "dev"
40
41
  Requires-Dist: pylint-per-file-ignores==3.2.0; extra == "dev"
41
- Requires-Dist: pyproject-fmt==2.16.1; extra == "dev"
42
- Requires-Dist: pyrefly==0.53.0; extra == "dev"
42
+ Requires-Dist: pyproject-fmt==2.16.2; extra == "dev"
43
+ Requires-Dist: pyrefly==0.54.0; extra == "dev"
43
44
  Requires-Dist: pyright==1.1.408; extra == "dev"
44
45
  Requires-Dist: pyroma==5.0.1; extra == "dev"
45
46
  Requires-Dist: pytest==9.0.2; extra == "dev"
46
47
  Requires-Dist: pytest-cov==7.0.0; extra == "dev"
47
48
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
48
- Requires-Dist: ruff==0.15.1; extra == "dev"
49
+ Requires-Dist: ruff==0.15.2; extra == "dev"
49
50
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
50
51
  Requires-Dist: shfmt-py==3.12.0.2; extra == "dev"
51
52
  Requires-Dist: sphinx==9.1.0; extra == "dev"
@@ -57,10 +58,10 @@ Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
57
58
  Requires-Dist: sybil==9.3.0; extra == "dev"
58
59
  Requires-Dist: torch>=2.5.1; extra == "dev"
59
60
  Requires-Dist: torchvision>=0.20.1; extra == "dev"
60
- Requires-Dist: ty==0.0.17; extra == "dev"
61
+ Requires-Dist: ty==0.0.18; extra == "dev"
61
62
  Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
62
63
  Requires-Dist: vulture==2.14; extra == "dev"
63
- Requires-Dist: vws-python-mock==2026.2.22.2; extra == "dev"
64
+ Requires-Dist: vws-python-mock==2026.2.22.3; extra == "dev"
64
65
  Requires-Dist: vws-test-fixtures==2023.3.5; extra == "dev"
65
66
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
67
  Requires-Dist: zizmor==1.22.0; extra == "dev"
@@ -34,6 +34,7 @@ src/vws/py.typed
34
34
  src/vws/query.py
35
35
  src/vws/reports.py
36
36
  src/vws/response.py
37
+ src/vws/transports.py
37
38
  src/vws/vumark_accept.py
38
39
  src/vws/vumark_service.py
39
40
  src/vws/vws.py
@@ -52,5 +53,6 @@ tests/__init__.py
52
53
  tests/conftest.py
53
54
  tests/test_cloud_reco_exceptions.py
54
55
  tests/test_query.py
56
+ tests/test_transports.py
55
57
  tests/test_vws.py
56
58
  tests/test_vws_exceptions.py
@@ -1,4 +1,5 @@
1
1
  beartype>=0.22.9
2
+ httpx>=0.28.0
2
3
  requests>=2.32.3
3
4
  urllib3>=2.2.3
4
5
  vws-auth-tools>=2024.7.12
@@ -18,16 +19,16 @@ prek==0.3.3
18
19
  pydocstringformatter==0.7.5
19
20
  pydocstyle==6.3
20
21
  pygments==2.19.2
21
- pylint[spelling]==4.0.4
22
+ pylint[spelling]==4.0.5
22
23
  pylint-per-file-ignores==3.2.0
23
- pyproject-fmt==2.16.1
24
- pyrefly==0.53.0
24
+ pyproject-fmt==2.16.2
25
+ pyrefly==0.54.0
25
26
  pyright==1.1.408
26
27
  pyroma==5.0.1
27
28
  pytest==9.0.2
28
29
  pytest-cov==7.0.0
29
30
  pyyaml==6.0.3
30
- ruff==0.15.1
31
+ ruff==0.15.2
31
32
  shellcheck-py==0.11.0.1
32
33
  shfmt-py==3.12.0.2
33
34
  sphinx==9.1.0
@@ -39,10 +40,10 @@ sphinxcontrib-spelling==8.0.2
39
40
  sybil==9.3.0
40
41
  torch>=2.5.1
41
42
  torchvision>=0.20.1
42
- ty==0.0.17
43
+ ty==0.0.18
43
44
  types-requests==2.32.4.20260107
44
45
  vulture==2.14
45
- vws-python-mock==2026.2.22.2
46
+ vws-python-mock==2026.2.22.3
46
47
  vws-test-fixtures==2023.3.5
47
48
  yamlfix==1.19.1
48
49
  zizmor==1.22.0
@@ -35,6 +35,7 @@ def fixture_mock_vumark_database() -> Generator[VuMarkDatabase]:
35
35
 
36
36
  @pytest.fixture
37
37
  def vumark_service_client(
38
+ *,
38
39
  _mock_vumark_database: VuMarkDatabase,
39
40
  ) -> VuMarkService:
40
41
  """A ``VuMarkService`` client which connects to a mock VuMark database."""
@@ -45,14 +46,14 @@ def vumark_service_client(
45
46
 
46
47
 
47
48
  @pytest.fixture
48
- def vumark_target_id(_mock_vumark_database: VuMarkDatabase) -> str:
49
+ def vumark_target_id(*, _mock_vumark_database: VuMarkDatabase) -> str:
49
50
  """The ID of the VuMark template target."""
50
51
  (target,) = _mock_vumark_database.vumark_targets
51
52
  return target.target_id
52
53
 
53
54
 
54
55
  @pytest.fixture
55
- def vws_client(_mock_database: CloudDatabase) -> VWS:
56
+ def vws_client(*, _mock_database: CloudDatabase) -> VWS:
56
57
  """A VWS client which connects to a mock database."""
57
58
  return VWS(
58
59
  server_access_key=_mock_database.server_access_key,
@@ -61,7 +62,7 @@ def vws_client(_mock_database: CloudDatabase) -> VWS:
61
62
 
62
63
 
63
64
  @pytest.fixture
64
- def cloud_reco_client(_mock_database: CloudDatabase) -> CloudRecoService:
65
+ def cloud_reco_client(*, _mock_database: CloudDatabase) -> CloudRecoService:
65
66
  """A ``CloudRecoService`` client which connects to a mock database."""
66
67
  return CloudRecoService(
67
68
  client_access_key=_mock_database.client_access_key,
@@ -71,6 +72,7 @@ def cloud_reco_client(_mock_database: CloudDatabase) -> CloudRecoService:
71
72
 
72
73
  @pytest.fixture(name="image_file", params=["r+b", "rb"])
73
74
  def fixture_image_file(
75
+ *,
74
76
  high_quality_image: io.BytesIO,
75
77
  tmp_path: Path,
76
78
  request: pytest.FixtureRequest,
@@ -86,6 +88,7 @@ def fixture_image_file(
86
88
 
87
89
  @pytest.fixture(params=["high_quality_image", "image_file"])
88
90
  def image(
91
+ *,
89
92
  request: pytest.FixtureRequest,
90
93
  high_quality_image: io.BytesIO,
91
94
  image_file: BinaryIO,
@@ -0,0 +1,61 @@
1
+ """Tests for HTTP transport implementations."""
2
+
3
+ from http import HTTPStatus
4
+
5
+ import httpx
6
+ import respx
7
+
8
+ from vws.response import Response
9
+ from vws.transports import HTTPXTransport
10
+
11
+
12
+ class TestHTTPXTransport:
13
+ """Tests for ``HTTPXTransport``."""
14
+
15
+ @staticmethod
16
+ @respx.mock
17
+ def test_float_timeout() -> None:
18
+ """``HTTPXTransport`` works with a float timeout."""
19
+ route = respx.post(url="https://example.com/test").mock(
20
+ return_value=httpx.Response(
21
+ status_code=HTTPStatus.OK,
22
+ text="OK",
23
+ ),
24
+ )
25
+ transport = HTTPXTransport()
26
+ response = transport(
27
+ method="POST",
28
+ url="https://example.com/test",
29
+ headers={"Content-Type": "text/plain"},
30
+ data=b"hello",
31
+ timeout=30.0,
32
+ )
33
+ assert route.called
34
+ assert isinstance(response, Response)
35
+ assert response.status_code == HTTPStatus.OK
36
+ assert response.text == "OK"
37
+ assert response.tell_position == len(b"OK")
38
+
39
+ @staticmethod
40
+ @respx.mock
41
+ def test_tuple_timeout() -> None:
42
+ """``HTTPXTransport`` works with a (connect, read) timeout
43
+ tuple.
44
+ """
45
+ route = respx.post(url="https://example.com/test").mock(
46
+ return_value=httpx.Response(
47
+ status_code=HTTPStatus.OK,
48
+ text="OK",
49
+ ),
50
+ )
51
+ transport = HTTPXTransport()
52
+ response = transport(
53
+ method="POST",
54
+ url="https://example.com/test",
55
+ headers={"Content-Type": "text/plain"},
56
+ data=b"hello",
57
+ timeout=(5.0, 30.0),
58
+ )
59
+ assert route.called
60
+ assert isinstance(response, Response)
61
+ assert response.status_code == HTTPStatus.OK
File without changes
File without changes