vws-python 2026.8.14__py3-none-any.whl
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/__init__.py +21 -0
- vws/_async_vws_request.py +77 -0
- vws/_image_utils.py +18 -0
- vws/_model_targets.py +323 -0
- vws/_reco_counts.py +80 -0
- vws/_vws_request.py +76 -0
- vws/async_model_target_service.py +381 -0
- vws/async_query.py +224 -0
- vws/async_vumark_service.py +155 -0
- vws/async_vws.py +752 -0
- vws/exceptions/__init__.py +1 -0
- vws/exceptions/base_exceptions.py +74 -0
- vws/exceptions/cloud_reco_exceptions.py +44 -0
- vws/exceptions/custom_exceptions.py +107 -0
- vws/exceptions/model_target_exceptions.py +210 -0
- vws/exceptions/vws_exceptions.py +256 -0
- vws/include_target_data.py +18 -0
- vws/model_target_datasets.py +152 -0
- vws/model_target_service.py +366 -0
- vws/py.typed +0 -0
- vws/query.py +194 -0
- vws/reports.py +391 -0
- vws/response.py +19 -0
- vws/transports.py +315 -0
- vws/vumark_accept.py +18 -0
- vws/vumark_service.py +138 -0
- vws/vws.py +719 -0
- vws_python-2026.8.14.dist-info/METADATA +152 -0
- vws_python-2026.8.14.dist-info/RECORD +32 -0
- vws_python-2026.8.14.dist-info/WHEEL +5 -0
- vws_python-2026.8.14.dist-info/licenses/LICENSE +21 -0
- vws_python-2026.8.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Async interface to the Vuforia Model Target Web API."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Sequence # noqa: TC003
|
|
6
|
+
from http import HTTPMethod
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
from beartype import BeartypeConf, beartype
|
|
10
|
+
|
|
11
|
+
from vws._model_targets import (
|
|
12
|
+
JSON_CONTENT_TYPE,
|
|
13
|
+
OAUTH2_TOKEN_BODY,
|
|
14
|
+
OAUTH2_TOKEN_PATH,
|
|
15
|
+
access_token_from_response,
|
|
16
|
+
dataset_collection_path,
|
|
17
|
+
dataset_download_path,
|
|
18
|
+
dataset_path,
|
|
19
|
+
dataset_request_body,
|
|
20
|
+
dataset_status_path,
|
|
21
|
+
dataset_uuid_from_response,
|
|
22
|
+
oauth2_token_headers,
|
|
23
|
+
raise_for_error,
|
|
24
|
+
status_report_from_response,
|
|
25
|
+
)
|
|
26
|
+
from vws.exceptions.model_target_exceptions import (
|
|
27
|
+
ModelTargetDatasetTimeoutError,
|
|
28
|
+
)
|
|
29
|
+
from vws.model_target_datasets import ( # noqa: TC001
|
|
30
|
+
ModelTargetDatasetType,
|
|
31
|
+
ModelTargetModel,
|
|
32
|
+
)
|
|
33
|
+
from vws.reports import (
|
|
34
|
+
ModelTargetDatasetStatuses,
|
|
35
|
+
ModelTargetDatasetStatusReport,
|
|
36
|
+
)
|
|
37
|
+
from vws.response import Response # noqa: TC001
|
|
38
|
+
from vws.transports import AsyncHTTPXTransport, AsyncTransport
|
|
39
|
+
|
|
40
|
+
_TOKEN_EXPIRY_MARGIN_SECONDS = 60.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
44
|
+
class AsyncModelTargetService:
|
|
45
|
+
"""An async interface to the Vuforia Model Target Web API."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
client_id: str,
|
|
51
|
+
client_secret: str,
|
|
52
|
+
base_vws_url: str = "https://vws.vuforia.com",
|
|
53
|
+
request_timeout_seconds: float | tuple[float, float] = 30.0,
|
|
54
|
+
transport: AsyncTransport | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Args:
|
|
58
|
+
client_id: A Model Target Web API OAuth2 client
|
|
59
|
+
ID.
|
|
60
|
+
client_secret: A Model Target Web API OAuth2
|
|
61
|
+
client secret.
|
|
62
|
+
base_vws_url: The base URL for the VWS API, which
|
|
63
|
+
also serves the Model Target Web API.
|
|
64
|
+
request_timeout_seconds: The timeout for each
|
|
65
|
+
HTTP request. This can be a float to set both
|
|
66
|
+
the connect and read timeouts, or a
|
|
67
|
+
(connect, read) tuple.
|
|
68
|
+
transport: The async HTTP transport to use for
|
|
69
|
+
requests. Defaults to
|
|
70
|
+
``AsyncHTTPXTransport()``.
|
|
71
|
+
"""
|
|
72
|
+
self._client_id = client_id
|
|
73
|
+
self._client_secret = client_secret
|
|
74
|
+
self._base_vws_url = base_vws_url
|
|
75
|
+
self._request_timeout_seconds = request_timeout_seconds
|
|
76
|
+
self._transport = (
|
|
77
|
+
transport if transport is not None else AsyncHTTPXTransport()
|
|
78
|
+
)
|
|
79
|
+
self._access_token: str | None = None
|
|
80
|
+
self._access_token_expiry_time = 0.0
|
|
81
|
+
|
|
82
|
+
async def aclose(self) -> None:
|
|
83
|
+
"""Close the underlying transport if it supports closing."""
|
|
84
|
+
await self._transport.aclose()
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self) -> Self:
|
|
87
|
+
"""Enter the async context manager."""
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(self, *_args: object) -> None:
|
|
91
|
+
"""Exit the async context manager and close the transport."""
|
|
92
|
+
await self.aclose()
|
|
93
|
+
|
|
94
|
+
async def get_access_token(self) -> str:
|
|
95
|
+
"""Get an OAuth2 access token for the Model Target Web API.
|
|
96
|
+
|
|
97
|
+
A token is requested only when the client has no token which is
|
|
98
|
+
still valid, so this can be called before each request.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A bearer token.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
105
|
+
Vuforia did not give an access token. For example, the
|
|
106
|
+
given client ID and client secret may not match a set of
|
|
107
|
+
Model Target Web API credentials.
|
|
108
|
+
"""
|
|
109
|
+
request_time = time.monotonic()
|
|
110
|
+
if (
|
|
111
|
+
self._access_token is not None
|
|
112
|
+
and request_time < self._access_token_expiry_time
|
|
113
|
+
):
|
|
114
|
+
return self._access_token
|
|
115
|
+
|
|
116
|
+
response = await self._transport(
|
|
117
|
+
method=HTTPMethod.POST,
|
|
118
|
+
url=self._base_vws_url.rstrip("/") + OAUTH2_TOKEN_PATH,
|
|
119
|
+
headers=oauth2_token_headers(
|
|
120
|
+
client_id=self._client_id,
|
|
121
|
+
client_secret=self._client_secret,
|
|
122
|
+
),
|
|
123
|
+
data=OAUTH2_TOKEN_BODY,
|
|
124
|
+
request_timeout=self._request_timeout_seconds,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
access_token, expires_in_seconds = access_token_from_response(
|
|
128
|
+
response=response,
|
|
129
|
+
)
|
|
130
|
+
self._access_token = access_token
|
|
131
|
+
self._access_token_expiry_time = (
|
|
132
|
+
request_time + expires_in_seconds - _TOKEN_EXPIRY_MARGIN_SECONDS
|
|
133
|
+
)
|
|
134
|
+
return access_token
|
|
135
|
+
|
|
136
|
+
async def make_request(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
method: str,
|
|
140
|
+
data: bytes,
|
|
141
|
+
request_path: str,
|
|
142
|
+
extra_headers: dict[str, str] | None = None,
|
|
143
|
+
) -> Response:
|
|
144
|
+
"""Make an authenticated request to the Model Target Web API.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
method: The HTTP method which will be used in
|
|
148
|
+
the request.
|
|
149
|
+
data: The request body which will be used in the
|
|
150
|
+
request.
|
|
151
|
+
request_path: The path to the endpoint which
|
|
152
|
+
will be used in the request.
|
|
153
|
+
extra_headers: Additional headers to include in
|
|
154
|
+
the request.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The response to the request.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
~vws.exceptions.model_target_exceptions.ModelTargetError:
|
|
161
|
+
Vuforia returned an error.
|
|
162
|
+
~vws.exceptions.custom_exceptions.ServerError:
|
|
163
|
+
There is an error with Vuforia's servers.
|
|
164
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError:
|
|
165
|
+
Vuforia is rate limiting access.
|
|
166
|
+
"""
|
|
167
|
+
access_token = await self.get_access_token()
|
|
168
|
+
headers = {
|
|
169
|
+
"Authorization": f"Bearer {access_token}",
|
|
170
|
+
**(extra_headers or {}),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
response = await self._transport(
|
|
174
|
+
method=method,
|
|
175
|
+
url=self._base_vws_url.rstrip("/") + request_path,
|
|
176
|
+
headers=headers,
|
|
177
|
+
data=data,
|
|
178
|
+
request_timeout=self._request_timeout_seconds,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
raise_for_error(response=response)
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
async def create_dataset(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
name: str,
|
|
188
|
+
target_sdk: str,
|
|
189
|
+
models: Sequence[ModelTargetModel],
|
|
190
|
+
dataset_type: ModelTargetDatasetType,
|
|
191
|
+
) -> str:
|
|
192
|
+
"""Start generating a Model Target dataset.
|
|
193
|
+
|
|
194
|
+
Vuforia generates the dataset in the background, so it is not
|
|
195
|
+
available to download immediately. Use
|
|
196
|
+
:meth:`wait_for_dataset_generated` to wait for it.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
name: The name of the dataset.
|
|
200
|
+
target_sdk: The Vuforia Engine version to generate the dataset
|
|
201
|
+
for.
|
|
202
|
+
models: The models to generate the dataset from. A standard
|
|
203
|
+
dataset takes exactly one model.
|
|
204
|
+
dataset_type: Whether to create a standard or an advanced
|
|
205
|
+
dataset.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The UUID of the new dataset.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
|
|
212
|
+
The request was not authenticated.
|
|
213
|
+
~vws.exceptions.model_target_exceptions.ModelTargetValidationError:
|
|
214
|
+
Vuforia rejected the request. For example, a model may
|
|
215
|
+
give neither a CAD data URL nor a CAD data blob.
|
|
216
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
217
|
+
Vuforia did not give an access token.
|
|
218
|
+
"""
|
|
219
|
+
response = await self.make_request(
|
|
220
|
+
method=HTTPMethod.POST,
|
|
221
|
+
data=dataset_request_body(
|
|
222
|
+
name=name,
|
|
223
|
+
target_sdk=target_sdk,
|
|
224
|
+
models=models,
|
|
225
|
+
),
|
|
226
|
+
request_path=dataset_collection_path(dataset_type=dataset_type),
|
|
227
|
+
extra_headers={"Content-Type": JSON_CONTENT_TYPE},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return dataset_uuid_from_response(response=response)
|
|
231
|
+
|
|
232
|
+
async def get_dataset_status(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
dataset_uuid: str,
|
|
236
|
+
dataset_type: ModelTargetDatasetType,
|
|
237
|
+
) -> ModelTargetDatasetStatusReport:
|
|
238
|
+
"""Get the status of a Model Target dataset.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
dataset_uuid: The UUID of the dataset, as given by
|
|
242
|
+
:meth:`create_dataset`.
|
|
243
|
+
dataset_type: The kind of dataset to get the status of.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The status of the dataset.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
|
|
250
|
+
The request was not authenticated.
|
|
251
|
+
~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
|
|
252
|
+
No dataset of the given type matches the given UUID.
|
|
253
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
254
|
+
Vuforia did not give an access token.
|
|
255
|
+
"""
|
|
256
|
+
response = await self.make_request(
|
|
257
|
+
method=HTTPMethod.GET,
|
|
258
|
+
data=b"",
|
|
259
|
+
request_path=dataset_status_path(
|
|
260
|
+
dataset_type=dataset_type,
|
|
261
|
+
dataset_uuid=dataset_uuid,
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return status_report_from_response(response=response)
|
|
266
|
+
|
|
267
|
+
async def wait_for_dataset_generated(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
dataset_uuid: str,
|
|
271
|
+
dataset_type: ModelTargetDatasetType,
|
|
272
|
+
seconds_between_requests: float = 0.2,
|
|
273
|
+
timeout_seconds: float = 60 * 5,
|
|
274
|
+
) -> ModelTargetDatasetStatusReport:
|
|
275
|
+
"""Wait for Vuforia to finish generating a Model Target dataset.
|
|
276
|
+
|
|
277
|
+
A dataset which failed to generate is also finished, so the
|
|
278
|
+
returned report may have a
|
|
279
|
+
:attr:`~.ModelTargetDatasetStatusReport.status` of
|
|
280
|
+
``FAILED``.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
dataset_uuid: The UUID of the dataset, as given by
|
|
284
|
+
:meth:`create_dataset`.
|
|
285
|
+
dataset_type: The kind of dataset to wait for.
|
|
286
|
+
seconds_between_requests: The number of seconds to wait between
|
|
287
|
+
requests made while polling the dataset's status.
|
|
288
|
+
timeout_seconds: The maximum number of seconds to wait for the
|
|
289
|
+
dataset to be generated.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The status of the dataset once it is no longer processing.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
~vws.exceptions.model_target_exceptions.ModelTargetDatasetTimeoutError:
|
|
296
|
+
The dataset was not generated within ``timeout_seconds``
|
|
297
|
+
seconds.
|
|
298
|
+
~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
|
|
299
|
+
No dataset of the given type matches the given UUID.
|
|
300
|
+
"""
|
|
301
|
+
start_time = time.monotonic()
|
|
302
|
+
while True:
|
|
303
|
+
report = await self.get_dataset_status(
|
|
304
|
+
dataset_uuid=dataset_uuid,
|
|
305
|
+
dataset_type=dataset_type,
|
|
306
|
+
)
|
|
307
|
+
if report.status != ModelTargetDatasetStatuses.PROCESSING:
|
|
308
|
+
return report
|
|
309
|
+
|
|
310
|
+
elapsed_time = time.monotonic() - start_time
|
|
311
|
+
if elapsed_time > timeout_seconds:
|
|
312
|
+
raise ModelTargetDatasetTimeoutError
|
|
313
|
+
|
|
314
|
+
await asyncio.sleep(delay=seconds_between_requests)
|
|
315
|
+
|
|
316
|
+
async def download_dataset(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
dataset_uuid: str,
|
|
320
|
+
dataset_type: ModelTargetDatasetType,
|
|
321
|
+
) -> bytes:
|
|
322
|
+
"""Download a generated Model Target dataset.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
dataset_uuid: The UUID of the dataset, as given by
|
|
326
|
+
:meth:`create_dataset`.
|
|
327
|
+
dataset_type: The kind of dataset to download.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The dataset, as the bytes of a zip file.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
|
|
334
|
+
The request was not authenticated.
|
|
335
|
+
~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
|
|
336
|
+
No dataset of the given type matches the given UUID.
|
|
337
|
+
~vws.exceptions.model_target_exceptions.ModelTargetDatasetNotDoneError:
|
|
338
|
+
Vuforia has not generated the dataset.
|
|
339
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
340
|
+
Vuforia did not give an access token.
|
|
341
|
+
"""
|
|
342
|
+
response = await self.make_request(
|
|
343
|
+
method=HTTPMethod.GET,
|
|
344
|
+
data=b"",
|
|
345
|
+
request_path=dataset_download_path(
|
|
346
|
+
dataset_type=dataset_type,
|
|
347
|
+
dataset_uuid=dataset_uuid,
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return response.content
|
|
352
|
+
|
|
353
|
+
async def delete_dataset(
|
|
354
|
+
self,
|
|
355
|
+
*,
|
|
356
|
+
dataset_uuid: str,
|
|
357
|
+
dataset_type: ModelTargetDatasetType,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Delete a Model Target dataset.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
dataset_uuid: The UUID of the dataset, as given by
|
|
363
|
+
:meth:`create_dataset`.
|
|
364
|
+
dataset_type: The kind of dataset to delete.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
|
|
368
|
+
The request was not authenticated.
|
|
369
|
+
~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
|
|
370
|
+
No dataset of the given type matches the given UUID.
|
|
371
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
372
|
+
Vuforia did not give an access token.
|
|
373
|
+
"""
|
|
374
|
+
await self.make_request(
|
|
375
|
+
method=HTTPMethod.DELETE,
|
|
376
|
+
data=b"",
|
|
377
|
+
request_path=dataset_path(
|
|
378
|
+
dataset_type=dataset_type,
|
|
379
|
+
dataset_uuid=dataset_uuid,
|
|
380
|
+
),
|
|
381
|
+
)
|
vws/async_query.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Async tools for interacting with the Vuforia Cloud Recognition
|
|
2
|
+
Web APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from http import HTTPMethod, HTTPStatus
|
|
7
|
+
from typing import Any, Self
|
|
8
|
+
|
|
9
|
+
from beartype import BeartypeConf, beartype
|
|
10
|
+
from urllib3.filepost import encode_multipart_formdata
|
|
11
|
+
from vws_auth_tools import authorization_header, rfc_1123_date
|
|
12
|
+
|
|
13
|
+
from vws._image_utils import ImageType as _ImageType
|
|
14
|
+
from vws._image_utils import get_image_data as _get_image_data
|
|
15
|
+
from vws.exceptions.base_exceptions import CloudRecoError
|
|
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
|
|
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 = (
|
|
65
|
+
transport if transport is not None else AsyncHTTPXTransport()
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def aclose(self) -> None:
|
|
69
|
+
"""Close the underlying transport if it supports closing."""
|
|
70
|
+
await self._transport.aclose()
|
|
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
|
+
~vws.exceptions.base_exceptions.CloudRecoError: Vuforia returned
|
|
124
|
+
a client error without a recognized JSON body.
|
|
125
|
+
json.JSONDecodeError: Vuforia returned a successful response with
|
|
126
|
+
an invalid JSON body.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
An ordered list of target details of matching
|
|
130
|
+
targets.
|
|
131
|
+
"""
|
|
132
|
+
image_content = _get_image_data(image=image)
|
|
133
|
+
body: dict[str, Any] = {
|
|
134
|
+
"image": (
|
|
135
|
+
"image.jpeg",
|
|
136
|
+
image_content,
|
|
137
|
+
"image/jpeg",
|
|
138
|
+
),
|
|
139
|
+
"max_num_results": (
|
|
140
|
+
None,
|
|
141
|
+
int(max_num_results),
|
|
142
|
+
"text/plain",
|
|
143
|
+
),
|
|
144
|
+
"include_target_data": (
|
|
145
|
+
None,
|
|
146
|
+
include_target_data.value,
|
|
147
|
+
"text/plain",
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
date = rfc_1123_date()
|
|
151
|
+
request_path = "/v1/query"
|
|
152
|
+
content, content_type_header = encode_multipart_formdata(fields=body)
|
|
153
|
+
method = HTTPMethod.POST
|
|
154
|
+
|
|
155
|
+
authorization_string = authorization_header(
|
|
156
|
+
access_key=self._client_access_key,
|
|
157
|
+
secret_key=self._client_secret_key,
|
|
158
|
+
method=method,
|
|
159
|
+
content=content,
|
|
160
|
+
# Note that this is not the actual Content-Type
|
|
161
|
+
# header value sent.
|
|
162
|
+
content_type="multipart/form-data",
|
|
163
|
+
date=date,
|
|
164
|
+
request_path=request_path,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
headers = {
|
|
168
|
+
"Authorization": authorization_string,
|
|
169
|
+
"Date": date,
|
|
170
|
+
"Content-Type": content_type_header,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
response = await self._transport(
|
|
174
|
+
method=method,
|
|
175
|
+
url=self._base_vwq_url.rstrip("/") + request_path,
|
|
176
|
+
headers=headers,
|
|
177
|
+
data=content,
|
|
178
|
+
request_timeout=self._request_timeout_seconds,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
182
|
+
raise RequestEntityTooLargeError(response=response)
|
|
183
|
+
|
|
184
|
+
if "Integer out of range" in response.text:
|
|
185
|
+
raise MaxNumResultsOutOfRangeError(
|
|
186
|
+
response=response,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
|
191
|
+
): # pragma: no cover
|
|
192
|
+
raise ServerError(response=response)
|
|
193
|
+
|
|
194
|
+
content_type = {
|
|
195
|
+
key.lower(): value for key, value in response.headers.items()
|
|
196
|
+
}.get("content-type", "")
|
|
197
|
+
if (
|
|
198
|
+
response.status_code >= HTTPStatus.BAD_REQUEST
|
|
199
|
+
and not content_type.lower().startswith("application/json")
|
|
200
|
+
):
|
|
201
|
+
raise CloudRecoError(response=response)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
response_body = json.loads(s=response.text)
|
|
205
|
+
except json.JSONDecodeError as exc:
|
|
206
|
+
if response.status_code >= HTTPStatus.BAD_REQUEST:
|
|
207
|
+
raise CloudRecoError(response=response) from exc
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
result_code = response_body["result_code"]
|
|
211
|
+
if result_code != "Success":
|
|
212
|
+
exception = {
|
|
213
|
+
"AuthenticationFailure": (AuthenticationFailureError),
|
|
214
|
+
"BadImage": BadImageError,
|
|
215
|
+
"InactiveProject": InactiveProjectError,
|
|
216
|
+
"RequestTimeTooSkewed": (RequestTimeTooSkewedError),
|
|
217
|
+
}[result_code]
|
|
218
|
+
raise exception(response=response)
|
|
219
|
+
|
|
220
|
+
result_list = list(response_body["results"])
|
|
221
|
+
return [
|
|
222
|
+
QueryResult.from_response_dict(response_dict=item)
|
|
223
|
+
for item in result_list
|
|
224
|
+
]
|