vws-python 2026.2.24__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.
- {vws_python-2026.2.24 → vws_python-2026.2.25}/CHANGELOG.rst +4 -0
- {vws_python-2026.2.24/src/vws_python.egg-info → vws_python-2026.2.25}/PKG-INFO +2 -1
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/api-reference.rst +12 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/pyproject.toml +2 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/spelling_private_dict.txt +2 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/__init__.py +6 -0
- vws_python-2026.2.25/src/vws/_async_vws_request.py +77 -0
- vws_python-2026.2.25/src/vws/_image_utils.py +18 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/_vws_request.py +1 -1
- vws_python-2026.2.25/src/vws/async_query.py +226 -0
- vws_python-2026.2.25/src/vws/async_vumark_service.py +170 -0
- vws_python-2026.2.25/src/vws/async_vws.py +692 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/query.py +4 -15
- vws_python-2026.2.25/src/vws/transports.py +278 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/vws.py +2 -14
- {vws_python-2026.2.24 → vws_python-2026.2.25/src/vws_python.egg-info}/PKG-INFO +2 -1
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws_python.egg-info/SOURCES.txt +9 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws_python.egg-info/requires.txt +1 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/conftest.py +53 -2
- vws_python-2026.2.25/tests/test_async_cloud_reco_exceptions.py +120 -0
- vws_python-2026.2.25/tests/test_async_query.py +225 -0
- vws_python-2026.2.25/tests/test_async_vws.py +500 -0
- vws_python-2026.2.25/tests/test_async_vws_exceptions.py +304 -0
- vws_python-2026.2.25/tests/test_transports.py +206 -0
- vws_python-2026.2.24/src/vws/transports.py +0 -158
- vws_python-2026.2.24/tests/test_transports.py +0 -61
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.git_archival.txt +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.gitattributes +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/dependabot.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/workflows/ci.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/workflows/dependabot-merge.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/workflows/lint.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/workflows/publish-site.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.github/workflows/release.yml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.gitignore +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.pre-commit-config.yaml +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.prettierrc +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.vscode/extensions.json +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/.vscode/settings.json +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/CODE_OF_CONDUCT.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/LICENSE +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/README.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/conftest.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/__init__.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/changelog.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/conf.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/contributing.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/exceptions.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/index.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/docs/source/release-process.rst +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/setup.cfg +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/exceptions/__init__.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/exceptions/base_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/exceptions/custom_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/exceptions/vws_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/include_target_data.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/py.typed +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/reports.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/response.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/vumark_accept.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws/vumark_service.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws_python.egg-info/dependency_links.txt +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws_python.egg-info/not-zip-safe +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/src/vws_python.egg-info/top_level.txt +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/__init__.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/test_cloud_reco_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/test_query.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/test_vws.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/tests/test_vws_exceptions.py +0 -0
- {vws_python-2026.2.24 → vws_python-2026.2.25}/zizmor.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vws-python
|
|
3
|
-
Version: 2026.2.
|
|
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
|
|
@@ -44,6 +44,7 @@ Requires-Dist: pyrefly==0.54.0; extra == "dev"
|
|
|
44
44
|
Requires-Dist: pyright==1.1.408; extra == "dev"
|
|
45
45
|
Requires-Dist: pyroma==5.0.1; extra == "dev"
|
|
46
46
|
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
47
48
|
Requires-Dist: pytest-cov==7.0.0; extra == "dev"
|
|
48
49
|
Requires-Dist: pyyaml==6.0.3; extra == "dev"
|
|
49
50
|
Requires-Dist: ruff==0.15.2; extra == "dev"
|
|
@@ -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:
|
|
@@ -60,6 +60,7 @@ optional-dependencies.dev = [
|
|
|
60
60
|
"pyright==1.1.408",
|
|
61
61
|
"pyroma==5.0.1",
|
|
62
62
|
"pytest==9.0.2",
|
|
63
|
+
"pytest-asyncio==1.3.0",
|
|
63
64
|
"pytest-cov==7.0.0",
|
|
64
65
|
"pyyaml==6.0.3",
|
|
65
66
|
"ruff==0.15.2",
|
|
@@ -359,6 +360,7 @@ ignore_path = [
|
|
|
359
360
|
# but Vulture does not enable this.
|
|
360
361
|
ignore_names = [
|
|
361
362
|
# Public API classes imported by users from vws.transports
|
|
363
|
+
"AsyncHTTPXTransport",
|
|
362
364
|
"HTTPXTransport",
|
|
363
365
|
# pytest configuration
|
|
364
366
|
"pytest_collect_file",
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Async interface to the Vuforia VuMark Generation Web API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from http import HTTPMethod, HTTPStatus
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
from beartype import BeartypeConf, beartype
|
|
8
|
+
|
|
9
|
+
from vws._async_vws_request import async_target_api_request
|
|
10
|
+
from vws.exceptions.custom_exceptions import ServerError
|
|
11
|
+
from vws.exceptions.vws_exceptions import (
|
|
12
|
+
AuthenticationFailureError,
|
|
13
|
+
BadRequestError,
|
|
14
|
+
DateRangeError,
|
|
15
|
+
FailError,
|
|
16
|
+
InvalidAcceptHeaderError,
|
|
17
|
+
InvalidInstanceIdError,
|
|
18
|
+
InvalidTargetTypeError,
|
|
19
|
+
RequestTimeTooSkewedError,
|
|
20
|
+
TargetStatusNotSuccessError,
|
|
21
|
+
TooManyRequestsError,
|
|
22
|
+
UnknownTargetError,
|
|
23
|
+
)
|
|
24
|
+
from vws.transports import AsyncHTTPXTransport, AsyncTransport
|
|
25
|
+
from vws.vumark_accept import VuMarkAccept
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
29
|
+
class AsyncVuMarkService:
|
|
30
|
+
"""An async interface to the Vuforia VuMark Generation Web
|
|
31
|
+
API.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
server_access_key: str,
|
|
38
|
+
server_secret_key: str,
|
|
39
|
+
base_vws_url: str = "https://vws.vuforia.com",
|
|
40
|
+
request_timeout_seconds: float | tuple[float, float] = 30.0,
|
|
41
|
+
transport: AsyncTransport | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Args:
|
|
45
|
+
server_access_key: A VWS server access key.
|
|
46
|
+
server_secret_key: A VWS server secret key.
|
|
47
|
+
base_vws_url: The base URL for the VWS API.
|
|
48
|
+
request_timeout_seconds: The timeout for each
|
|
49
|
+
HTTP request. This can be a float to set both
|
|
50
|
+
the connect and read timeouts, or a
|
|
51
|
+
(connect, read) tuple.
|
|
52
|
+
transport: The async HTTP transport to use for
|
|
53
|
+
requests. Defaults to
|
|
54
|
+
``AsyncHTTPXTransport()``.
|
|
55
|
+
"""
|
|
56
|
+
self._server_access_key = server_access_key
|
|
57
|
+
self._server_secret_key = server_secret_key
|
|
58
|
+
self._base_vws_url = base_vws_url
|
|
59
|
+
self._request_timeout_seconds = request_timeout_seconds
|
|
60
|
+
self._transport = transport or AsyncHTTPXTransport()
|
|
61
|
+
|
|
62
|
+
async def aclose(self) -> None:
|
|
63
|
+
"""Close the underlying transport if it supports closing."""
|
|
64
|
+
close = getattr(self._transport, "aclose", None)
|
|
65
|
+
if close is not None:
|
|
66
|
+
await close()
|
|
67
|
+
|
|
68
|
+
async def __aenter__(self) -> Self:
|
|
69
|
+
"""Enter the async context manager."""
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def __aexit__(self, *_args: object) -> None:
|
|
73
|
+
"""Exit the async context manager and close the transport."""
|
|
74
|
+
await self.aclose()
|
|
75
|
+
|
|
76
|
+
async def generate_vumark_instance(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
target_id: str,
|
|
80
|
+
instance_id: str,
|
|
81
|
+
accept: VuMarkAccept,
|
|
82
|
+
) -> bytes:
|
|
83
|
+
"""Generate a VuMark instance image.
|
|
84
|
+
|
|
85
|
+
See
|
|
86
|
+
https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/
|
|
87
|
+
for parameter details.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
target_id: The ID of the VuMark target.
|
|
91
|
+
instance_id: The instance ID to encode in the
|
|
92
|
+
VuMark.
|
|
93
|
+
accept: The image format to return.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The VuMark instance image bytes.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
100
|
+
secret key is not correct.
|
|
101
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
102
|
+
the request. For example, the given access key does not match a
|
|
103
|
+
known database.
|
|
104
|
+
~vws.exceptions.vws_exceptions.InvalidAcceptHeaderError: The
|
|
105
|
+
Accept header value is not supported.
|
|
106
|
+
~vws.exceptions.vws_exceptions.InvalidInstanceIdError: The
|
|
107
|
+
instance ID is invalid. For example, it may be empty.
|
|
108
|
+
~vws.exceptions.vws_exceptions.InvalidTargetTypeError: The target
|
|
109
|
+
is not a VuMark template target.
|
|
110
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
111
|
+
an error with the time sent to Vuforia.
|
|
112
|
+
~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The
|
|
113
|
+
target is not in the success state.
|
|
114
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
115
|
+
ID does not match a target in the database.
|
|
116
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
117
|
+
with Vuforia's servers.
|
|
118
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
119
|
+
rate limiting access.
|
|
120
|
+
"""
|
|
121
|
+
request_path = f"/targets/{target_id}/instances"
|
|
122
|
+
content_type = "application/json"
|
|
123
|
+
request_data = json.dumps(
|
|
124
|
+
obj={"instance_id": instance_id},
|
|
125
|
+
).encode(encoding="utf-8")
|
|
126
|
+
|
|
127
|
+
response = await async_target_api_request(
|
|
128
|
+
content_type=content_type,
|
|
129
|
+
server_access_key=self._server_access_key,
|
|
130
|
+
server_secret_key=self._server_secret_key,
|
|
131
|
+
method=HTTPMethod.POST,
|
|
132
|
+
data=request_data,
|
|
133
|
+
request_path=request_path,
|
|
134
|
+
base_vws_url=self._base_vws_url,
|
|
135
|
+
request_timeout_seconds=(self._request_timeout_seconds),
|
|
136
|
+
extra_headers={"Accept": accept},
|
|
137
|
+
transport=self._transport,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
response.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
|
142
|
+
): # pragma: no cover
|
|
143
|
+
# The Vuforia API returns a 429 response with no
|
|
144
|
+
# JSON body.
|
|
145
|
+
raise TooManyRequestsError(response=response)
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
|
149
|
+
): # pragma: no cover
|
|
150
|
+
raise ServerError(response=response)
|
|
151
|
+
|
|
152
|
+
if response.status_code == HTTPStatus.OK:
|
|
153
|
+
return response.content
|
|
154
|
+
|
|
155
|
+
result_code = json.loads(s=response.text)["result_code"]
|
|
156
|
+
|
|
157
|
+
exception = {
|
|
158
|
+
"AuthenticationFailure": (AuthenticationFailureError),
|
|
159
|
+
"BadRequest": BadRequestError,
|
|
160
|
+
"DateRangeError": DateRangeError,
|
|
161
|
+
"Fail": FailError,
|
|
162
|
+
"InvalidAcceptHeader": InvalidAcceptHeaderError,
|
|
163
|
+
"InvalidInstanceId": InvalidInstanceIdError,
|
|
164
|
+
"InvalidTargetType": InvalidTargetTypeError,
|
|
165
|
+
"RequestTimeTooSkewed": RequestTimeTooSkewedError,
|
|
166
|
+
"TargetStatusNotSuccess": (TargetStatusNotSuccessError),
|
|
167
|
+
"UnknownTarget": UnknownTargetError,
|
|
168
|
+
}[result_code]
|
|
169
|
+
|
|
170
|
+
raise exception(response=response)
|