vws-python 2026.2.23__tar.gz → 2026.2.25__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 (69) hide show
  1. {vws_python-2026.2.23 → vws_python-2026.2.25}/CHANGELOG.rst +8 -0
  2. {vws_python-2026.2.23/src/vws_python.egg-info → vws_python-2026.2.25}/PKG-INFO +9 -7
  3. {vws_python-2026.2.23 → vws_python-2026.2.25}/conftest.py +2 -0
  4. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/api-reference.rst +16 -0
  5. {vws_python-2026.2.23 → vws_python-2026.2.25}/pyproject.toml +11 -6
  6. {vws_python-2026.2.23 → vws_python-2026.2.25}/spelling_private_dict.txt +3 -0
  7. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/__init__.py +6 -0
  8. vws_python-2026.2.25/src/vws/_async_vws_request.py +77 -0
  9. vws_python-2026.2.25/src/vws/_image_utils.py +18 -0
  10. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/_vws_request.py +16 -23
  11. vws_python-2026.2.25/src/vws/async_query.py +226 -0
  12. vws_python-2026.2.25/src/vws/async_vumark_service.py +170 -0
  13. vws_python-2026.2.25/src/vws/async_vws.py +692 -0
  14. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/query.py +15 -31
  15. vws_python-2026.2.25/src/vws/transports.py +278 -0
  16. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vumark_service.py +12 -4
  17. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vws.py +30 -33
  18. {vws_python-2026.2.23 → vws_python-2026.2.25/src/vws_python.egg-info}/PKG-INFO +9 -7
  19. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/SOURCES.txt +11 -0
  20. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/requires.txt +8 -6
  21. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/conftest.py +59 -5
  22. vws_python-2026.2.25/tests/test_async_cloud_reco_exceptions.py +120 -0
  23. vws_python-2026.2.25/tests/test_async_query.py +225 -0
  24. vws_python-2026.2.25/tests/test_async_vws.py +500 -0
  25. vws_python-2026.2.25/tests/test_async_vws_exceptions.py +304 -0
  26. vws_python-2026.2.25/tests/test_transports.py +206 -0
  27. {vws_python-2026.2.23 → vws_python-2026.2.25}/.git_archival.txt +0 -0
  28. {vws_python-2026.2.23 → vws_python-2026.2.25}/.gitattributes +0 -0
  29. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/dependabot.yml +0 -0
  30. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/ci.yml +0 -0
  31. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/dependabot-merge.yml +0 -0
  32. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/lint.yml +0 -0
  33. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/publish-site.yml +0 -0
  34. {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/release.yml +0 -0
  35. {vws_python-2026.2.23 → vws_python-2026.2.25}/.gitignore +0 -0
  36. {vws_python-2026.2.23 → vws_python-2026.2.25}/.pre-commit-config.yaml +0 -0
  37. {vws_python-2026.2.23 → vws_python-2026.2.25}/.prettierrc +0 -0
  38. {vws_python-2026.2.23 → vws_python-2026.2.25}/.vscode/extensions.json +0 -0
  39. {vws_python-2026.2.23 → vws_python-2026.2.25}/.vscode/settings.json +0 -0
  40. {vws_python-2026.2.23 → vws_python-2026.2.25}/CODE_OF_CONDUCT.rst +0 -0
  41. {vws_python-2026.2.23 → vws_python-2026.2.25}/LICENSE +0 -0
  42. {vws_python-2026.2.23 → vws_python-2026.2.25}/README.rst +0 -0
  43. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/__init__.py +0 -0
  44. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/changelog.rst +0 -0
  45. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/conf.py +0 -0
  46. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/contributing.rst +0 -0
  47. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/exceptions.rst +0 -0
  48. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/index.rst +0 -0
  49. {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/release-process.rst +0 -0
  50. {vws_python-2026.2.23 → vws_python-2026.2.25}/setup.cfg +0 -0
  51. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/__init__.py +0 -0
  52. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/base_exceptions.py +0 -0
  53. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
  54. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/custom_exceptions.py +0 -0
  55. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/vws_exceptions.py +0 -0
  56. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/include_target_data.py +0 -0
  57. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/py.typed +0 -0
  58. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/reports.py +0 -0
  59. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/response.py +0 -0
  60. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vumark_accept.py +0 -0
  61. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/dependency_links.txt +0 -0
  62. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/not-zip-safe +0 -0
  63. {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/top_level.txt +0 -0
  64. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/__init__.py +0 -0
  65. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_cloud_reco_exceptions.py +0 -0
  66. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_query.py +0 -0
  67. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_vws.py +0 -0
  68. {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_vws_exceptions.py +0 -0
  69. {vws_python-2026.2.23 → vws_python-2026.2.25}/zizmor.yml +0 -0
@@ -4,6 +4,14 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.25
8
+ ----------
9
+
10
+
11
+ 2026.02.24
12
+ ----------
13
+
14
+
7
15
  2026.02.23
8
16
  ----------
9
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python
3
- Version: 2026.2.23
3
+ Version: 2026.2.25
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,17 @@ 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"
47
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
46
48
  Requires-Dist: pytest-cov==7.0.0; extra == "dev"
47
49
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
48
- Requires-Dist: ruff==0.15.1; extra == "dev"
50
+ Requires-Dist: ruff==0.15.2; extra == "dev"
49
51
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
50
52
  Requires-Dist: shfmt-py==3.12.0.2; extra == "dev"
51
53
  Requires-Dist: sphinx==9.1.0; extra == "dev"
@@ -57,10 +59,10 @@ Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
57
59
  Requires-Dist: sybil==9.3.0; extra == "dev"
58
60
  Requires-Dist: torch>=2.5.1; extra == "dev"
59
61
  Requires-Dist: torchvision>=0.20.1; extra == "dev"
60
- Requires-Dist: ty==0.0.17; extra == "dev"
62
+ Requires-Dist: ty==0.0.18; extra == "dev"
61
63
  Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
62
64
  Requires-Dist: vulture==2.14; extra == "dev"
63
- Requires-Dist: vws-python-mock==2026.2.22.2; extra == "dev"
65
+ Requires-Dist: vws-python-mock==2026.2.22.3; extra == "dev"
64
66
  Requires-Dist: vws-test-fixtures==2023.3.5; extra == "dev"
65
67
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
66
68
  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.
@@ -5,6 +5,18 @@ API Reference
5
5
  :undoc-members:
6
6
  :members:
7
7
 
8
+ .. automodule:: vws.async_vws
9
+ :undoc-members:
10
+ :members:
11
+
12
+ .. automodule:: vws.async_query
13
+ :undoc-members:
14
+ :members:
15
+
16
+ .. automodule:: vws.async_vumark_service
17
+ :undoc-members:
18
+ :members:
19
+
8
20
  .. automodule:: vws.reports
9
21
  :undoc-members:
10
22
  :members:
@@ -20,3 +32,7 @@ API Reference
20
32
  .. automodule:: vws.response
21
33
  :undoc-members:
22
34
  :members:
35
+
36
+ .. automodule:: vws.transports
37
+ :undoc-members:
38
+ :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,17 @@ 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",
63
+ "pytest-asyncio==1.3.0",
62
64
  "pytest-cov==7.0.0",
63
65
  "pyyaml==6.0.3",
64
- "ruff==0.15.1",
66
+ "ruff==0.15.2",
65
67
  # We add shellcheck-py not only for shell scripts and shell code blocks,
66
68
  # but also because having it installed means that ``actionlint-py`` will
67
69
  # use it to lint shell commands in GitHub workflow files.
@@ -79,10 +81,10 @@ optional-dependencies.dev = [
79
81
  # See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
80
82
  "torch>=2.5.1",
81
83
  "torchvision>=0.20.1",
82
- "ty==0.0.17",
84
+ "ty==0.0.18",
83
85
  "types-requests==2.32.4.20260107",
84
86
  "vulture==2.14",
85
- "vws-python-mock==2026.2.22.2",
87
+ "vws-python-mock==2026.2.22.3",
86
88
  "vws-test-fixtures==2023.3.5",
87
89
  "yamlfix==1.19.1",
88
90
  "zizmor==1.22.0",
@@ -357,6 +359,9 @@ ignore_path = [
357
359
  # Ideally we would limit the paths to the source code where we want to ignore names,
358
360
  # but Vulture does not enable this.
359
361
  ignore_names = [
362
+ # Public API classes imported by users from vws.transports
363
+ "AsyncHTTPXTransport",
364
+ "HTTPXTransport",
360
365
  # pytest configuration
361
366
  "pytest_collect_file",
362
367
  "pytest_collection_modifyitems",
@@ -27,6 +27,8 @@ admin
27
27
  api
28
28
  args
29
29
  ascii
30
+ async
31
+ asyncio
30
32
  beartype
31
33
  bool
32
34
  boolean
@@ -54,6 +56,7 @@ hmac
54
56
  html
55
57
  http
56
58
  https
59
+ httpx
57
60
  iff
58
61
  io
59
62
  issuecomment
@@ -1,11 +1,17 @@
1
1
  """A library for Vuforia Web Services."""
2
2
 
3
+ from .async_query import AsyncCloudRecoService
4
+ from .async_vumark_service import AsyncVuMarkService
5
+ from .async_vws import AsyncVWS
3
6
  from .query import CloudRecoService
4
7
  from .vumark_service import VuMarkService
5
8
  from .vws import VWS
6
9
 
7
10
  __all__ = [
8
11
  "VWS",
12
+ "AsyncCloudRecoService",
13
+ "AsyncVWS",
14
+ "AsyncVuMarkService",
9
15
  "CloudRecoService",
10
16
  "VuMarkService",
11
17
  ]
@@ -0,0 +1,77 @@
1
+ """Internal helper for making authenticated async requests to the
2
+ Vuforia Target API.
3
+ """
4
+
5
+ from beartype import BeartypeConf, beartype
6
+ from vws_auth_tools import authorization_header, rfc_1123_date
7
+
8
+ from vws.response import Response
9
+ from vws.transports import AsyncTransport
10
+
11
+
12
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
13
+ async def async_target_api_request(
14
+ *,
15
+ content_type: str,
16
+ server_access_key: str,
17
+ server_secret_key: str,
18
+ method: str,
19
+ data: bytes,
20
+ request_path: str,
21
+ base_vws_url: str,
22
+ request_timeout_seconds: float | tuple[float, float],
23
+ extra_headers: dict[str, str],
24
+ transport: AsyncTransport,
25
+ ) -> Response:
26
+ """Make an async request to the Vuforia Target API.
27
+
28
+ Args:
29
+ content_type: The content type of the request.
30
+ server_access_key: A VWS server access key.
31
+ server_secret_key: A VWS server secret key.
32
+ method: The HTTP method which will be used in the
33
+ request.
34
+ data: The request body which will be used in the
35
+ request.
36
+ request_path: The path to the endpoint which will be
37
+ used in the request.
38
+ base_vws_url: The base URL for the VWS API.
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 async HTTP transport to use for the
45
+ request.
46
+
47
+ Returns:
48
+ The response to the request.
49
+ """
50
+ date_string = rfc_1123_date()
51
+
52
+ signature_string = authorization_header(
53
+ access_key=server_access_key,
54
+ secret_key=server_secret_key,
55
+ method=method,
56
+ content=data,
57
+ content_type=content_type,
58
+ date=date_string,
59
+ request_path=request_path,
60
+ )
61
+
62
+ headers = {
63
+ "Authorization": signature_string,
64
+ "Date": date_string,
65
+ "Content-Type": content_type,
66
+ **extra_headers,
67
+ }
68
+
69
+ url = base_vws_url.rstrip("/") + request_path
70
+
71
+ return await transport(
72
+ method=method,
73
+ url=url,
74
+ headers=headers,
75
+ data=data,
76
+ request_timeout=request_timeout_seconds,
77
+ )
@@ -0,0 +1,18 @@
1
+ """Image utility functions shared across VWS modules."""
2
+
3
+ import io
4
+ from typing import BinaryIO
5
+
6
+ from beartype import beartype
7
+
8
+ ImageType = io.BytesIO | BinaryIO
9
+
10
+
11
+ @beartype
12
+ def get_image_data(image: ImageType) -> bytes:
13
+ """Get the data of an image file."""
14
+ original_tell = image.tell()
15
+ image.seek(0)
16
+ image_data = image.read()
17
+ image.seek(original_tell)
18
+ return image_data
@@ -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
- timeout=request_timeout_seconds,
73
- )
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),
75
+ request_timeout=request_timeout_seconds,
83
76
  )
@@ -0,0 +1,226 @@
1
+ """Async tools for interacting with the Vuforia Cloud Recognition
2
+ Web APIs.
3
+ """
4
+
5
+ import datetime
6
+ import json
7
+ from http import HTTPMethod, HTTPStatus
8
+ from typing import Any, Self
9
+
10
+ from beartype import BeartypeConf, beartype
11
+ from urllib3.filepost import encode_multipart_formdata
12
+ from vws_auth_tools import authorization_header, rfc_1123_date
13
+
14
+ from vws._image_utils import ImageType as _ImageType
15
+ from vws._image_utils import get_image_data as _get_image_data
16
+ from vws.exceptions.cloud_reco_exceptions import (
17
+ AuthenticationFailureError,
18
+ BadImageError,
19
+ InactiveProjectError,
20
+ MaxNumResultsOutOfRangeError,
21
+ RequestTimeTooSkewedError,
22
+ )
23
+ from vws.exceptions.custom_exceptions import (
24
+ RequestEntityTooLargeError,
25
+ ServerError,
26
+ )
27
+ from vws.include_target_data import CloudRecoIncludeTargetData
28
+ from vws.reports import QueryResult, TargetData
29
+ from vws.transports import AsyncHTTPXTransport, AsyncTransport
30
+
31
+
32
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
33
+ class AsyncCloudRecoService:
34
+ """An async interface to the Vuforia Cloud Recognition Web
35
+ APIs.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ client_access_key: str,
42
+ client_secret_key: str,
43
+ base_vwq_url: str = "https://cloudreco.vuforia.com",
44
+ request_timeout_seconds: float | tuple[float, float] = 30.0,
45
+ transport: AsyncTransport | None = None,
46
+ ) -> None:
47
+ """
48
+ Args:
49
+ client_access_key: A VWS client access key.
50
+ client_secret_key: A VWS client secret key.
51
+ base_vwq_url: The base URL for the VWQ API.
52
+ request_timeout_seconds: The timeout for each
53
+ HTTP request. This can be a float to set both
54
+ the connect and read timeouts, or a
55
+ (connect, read) tuple.
56
+ transport: The async HTTP transport to use for
57
+ requests. Defaults to
58
+ ``AsyncHTTPXTransport()``.
59
+ """
60
+ self._client_access_key = client_access_key
61
+ self._client_secret_key = client_secret_key
62
+ self._base_vwq_url = base_vwq_url
63
+ self._request_timeout_seconds = request_timeout_seconds
64
+ self._transport = transport or AsyncHTTPXTransport()
65
+
66
+ async def aclose(self) -> None:
67
+ """Close the underlying transport if it supports closing."""
68
+ close = getattr(self._transport, "aclose", None)
69
+ if close is not None:
70
+ await close()
71
+
72
+ async def __aenter__(self) -> Self:
73
+ """Enter the async context manager."""
74
+ return self
75
+
76
+ async def __aexit__(self, *_args: object) -> None:
77
+ """Exit the async context manager and close the transport."""
78
+ await self.aclose()
79
+
80
+ async def query(
81
+ self,
82
+ *,
83
+ image: _ImageType,
84
+ max_num_results: int = 1,
85
+ include_target_data: CloudRecoIncludeTargetData = (
86
+ CloudRecoIncludeTargetData.TOP
87
+ ),
88
+ ) -> list[QueryResult]:
89
+ """Use the Vuforia Web Query API to make an Image
90
+ Recognition Query.
91
+
92
+ See
93
+ https://developer.vuforia.com/library/web-api/vuforia-query-web-api
94
+ for parameter details.
95
+
96
+ Args:
97
+ image: The image to make a query against.
98
+ max_num_results: The maximum number of matching
99
+ targets to be returned.
100
+ include_target_data: Indicates if target_data
101
+ records shall be returned for the matched
102
+ targets. Accepted values are top (default
103
+ value, only return target_data for top ranked
104
+ match), none (return no target_data), all
105
+ (for all matched targets).
106
+
107
+ Raises:
108
+ ~vws.exceptions.cloud_reco_exceptions.AuthenticationFailureError:
109
+ The client access key pair is not correct.
110
+ ~vws.exceptions.cloud_reco_exceptions.MaxNumResultsOutOfRangeError:
111
+ ``max_num_results`` is not within the range (1, 50).
112
+ ~vws.exceptions.cloud_reco_exceptions.InactiveProjectError: The
113
+ project is inactive.
114
+ ~vws.exceptions.cloud_reco_exceptions.RequestTimeTooSkewedError:
115
+ There is an error with the time sent to Vuforia.
116
+ ~vws.exceptions.cloud_reco_exceptions.BadImageError: There is a
117
+ problem with the given image. For example, it must be a JPEG or
118
+ PNG file in the grayscale or RGB color space.
119
+ ~vws.exceptions.custom_exceptions.RequestEntityTooLargeError: The
120
+ given image is too large.
121
+ ~vws.exceptions.custom_exceptions.ServerError: There is an
122
+ error with Vuforia's servers.
123
+
124
+ Returns:
125
+ An ordered list of target details of matching
126
+ targets.
127
+ """
128
+ image_content = _get_image_data(image=image)
129
+ body: dict[str, Any] = {
130
+ "image": (
131
+ "image.jpeg",
132
+ image_content,
133
+ "image/jpeg",
134
+ ),
135
+ "max_num_results": (
136
+ None,
137
+ int(max_num_results),
138
+ "text/plain",
139
+ ),
140
+ "include_target_data": (
141
+ None,
142
+ include_target_data.value,
143
+ "text/plain",
144
+ ),
145
+ }
146
+ date = rfc_1123_date()
147
+ request_path = "/v1/query"
148
+ content, content_type_header = encode_multipart_formdata(fields=body)
149
+ method = HTTPMethod.POST
150
+
151
+ authorization_string = authorization_header(
152
+ access_key=self._client_access_key,
153
+ secret_key=self._client_secret_key,
154
+ method=method,
155
+ content=content,
156
+ # Note that this is not the actual Content-Type
157
+ # header value sent.
158
+ content_type="multipart/form-data",
159
+ date=date,
160
+ request_path=request_path,
161
+ )
162
+
163
+ headers = {
164
+ "Authorization": authorization_string,
165
+ "Date": date,
166
+ "Content-Type": content_type_header,
167
+ }
168
+
169
+ response = await self._transport(
170
+ method=method,
171
+ url=self._base_vwq_url.rstrip("/") + request_path,
172
+ headers=headers,
173
+ data=content,
174
+ request_timeout=self._request_timeout_seconds,
175
+ )
176
+
177
+ if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
178
+ raise RequestEntityTooLargeError(response=response)
179
+
180
+ if "Integer out of range" in response.text:
181
+ raise MaxNumResultsOutOfRangeError(
182
+ response=response,
183
+ )
184
+
185
+ if (
186
+ response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
187
+ ): # pragma: no cover
188
+ raise ServerError(response=response)
189
+
190
+ result_code = json.loads(s=response.text)["result_code"]
191
+ if result_code != "Success":
192
+ exception = {
193
+ "AuthenticationFailure": (AuthenticationFailureError),
194
+ "BadImage": BadImageError,
195
+ "InactiveProject": InactiveProjectError,
196
+ "RequestTimeTooSkewed": (RequestTimeTooSkewedError),
197
+ }[result_code]
198
+ raise exception(response=response)
199
+
200
+ result: list[QueryResult] = []
201
+ result_list = list(
202
+ json.loads(s=response.text)["results"],
203
+ )
204
+ for item in result_list:
205
+ target_data: TargetData | None = None
206
+ if "target_data" in item:
207
+ target_data_dict = item["target_data"]
208
+ metadata = target_data_dict["application_metadata"]
209
+ timestamp_string = target_data_dict["target_timestamp"]
210
+ target_timestamp = datetime.datetime.fromtimestamp(
211
+ timestamp=timestamp_string,
212
+ tz=datetime.UTC,
213
+ )
214
+ target_data = TargetData(
215
+ name=target_data_dict["name"],
216
+ application_metadata=metadata,
217
+ target_timestamp=target_timestamp,
218
+ )
219
+
220
+ query_result = QueryResult(
221
+ target_id=item["target_id"],
222
+ target_data=target_data,
223
+ )
224
+
225
+ result.append(query_result)
226
+ return result