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
vws/async_vws.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
"""Async tools for interacting with Vuforia APIs."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import calendar # noqa: TC003
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from http import HTTPMethod, HTTPStatus
|
|
9
|
+
from typing import Self
|
|
10
|
+
|
|
11
|
+
from beartype import BeartypeConf, beartype
|
|
12
|
+
|
|
13
|
+
from vws._async_vws_request import async_target_api_request
|
|
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._reco_counts import (
|
|
17
|
+
reco_counts_report_body,
|
|
18
|
+
reco_counts_report_path,
|
|
19
|
+
report_from_download_response,
|
|
20
|
+
)
|
|
21
|
+
from vws.exceptions.base_exceptions import VWSError
|
|
22
|
+
from vws.exceptions.custom_exceptions import (
|
|
23
|
+
RecoCountsReportNotReadyError,
|
|
24
|
+
RecoCountsReportTimeoutError,
|
|
25
|
+
ServerError,
|
|
26
|
+
TargetProcessingTimeoutError,
|
|
27
|
+
)
|
|
28
|
+
from vws.exceptions.vws_exceptions import TooManyRequestsError
|
|
29
|
+
from vws.reports import (
|
|
30
|
+
DatabaseSummaryReport,
|
|
31
|
+
RecoCountsReport,
|
|
32
|
+
RecoCountsReportRequest,
|
|
33
|
+
TargetStatusAndRecord,
|
|
34
|
+
TargetStatuses,
|
|
35
|
+
TargetSummaryReport,
|
|
36
|
+
)
|
|
37
|
+
from vws.response import Response # noqa: TC001
|
|
38
|
+
from vws.transports import AsyncHTTPXTransport, AsyncTransport
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
42
|
+
class AsyncVWS:
|
|
43
|
+
"""An async interface to Vuforia Web Services APIs."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
server_access_key: str,
|
|
49
|
+
server_secret_key: str,
|
|
50
|
+
base_vws_url: str = "https://vws.vuforia.com",
|
|
51
|
+
database_id: str | None = None,
|
|
52
|
+
request_timeout_seconds: float | tuple[float, float] = 30.0,
|
|
53
|
+
transport: AsyncTransport | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
server_access_key: A VWS server access key.
|
|
58
|
+
server_secret_key: A VWS server secret key.
|
|
59
|
+
base_vws_url: The base URL for the VWS API.
|
|
60
|
+
database_id: The ID of the database which the
|
|
61
|
+
given keys belong to. This is shown in the
|
|
62
|
+
target manager. It is needed only by
|
|
63
|
+
:meth:`request_database_reco_counts_report`.
|
|
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._server_access_key = server_access_key
|
|
73
|
+
self._server_secret_key = server_secret_key
|
|
74
|
+
self._base_vws_url = base_vws_url
|
|
75
|
+
self._database_id = database_id
|
|
76
|
+
self._request_timeout_seconds = request_timeout_seconds
|
|
77
|
+
self._transport = (
|
|
78
|
+
transport if transport is not None else AsyncHTTPXTransport()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def aclose(self) -> None:
|
|
82
|
+
"""Close the underlying transport if it supports closing."""
|
|
83
|
+
await self._transport.aclose()
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self) -> Self:
|
|
86
|
+
"""Enter the async context manager."""
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
async def __aexit__(self, *_args: object) -> None:
|
|
90
|
+
"""Exit the async context manager and close the transport."""
|
|
91
|
+
await self.aclose()
|
|
92
|
+
|
|
93
|
+
async def make_request(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
method: str,
|
|
97
|
+
data: bytes,
|
|
98
|
+
request_path: str,
|
|
99
|
+
expected_result_code: str,
|
|
100
|
+
content_type: str,
|
|
101
|
+
extra_headers: dict[str, str] | None = None,
|
|
102
|
+
) -> Response:
|
|
103
|
+
"""Make an async request to the Vuforia Target API.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
method: The HTTP method which will be used in
|
|
107
|
+
the request.
|
|
108
|
+
data: The request body which will be used in the
|
|
109
|
+
request.
|
|
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
|
|
114
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api.
|
|
115
|
+
content_type: The content type of the request.
|
|
116
|
+
extra_headers: Additional headers to include in
|
|
117
|
+
the request.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The response to the request.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
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.
|
|
130
|
+
"""
|
|
131
|
+
response = await async_target_api_request(
|
|
132
|
+
content_type=content_type,
|
|
133
|
+
server_access_key=self._server_access_key,
|
|
134
|
+
server_secret_key=self._server_secret_key,
|
|
135
|
+
method=method,
|
|
136
|
+
data=data,
|
|
137
|
+
request_path=request_path,
|
|
138
|
+
base_vws_url=self._base_vws_url,
|
|
139
|
+
request_timeout_seconds=self._request_timeout_seconds,
|
|
140
|
+
extra_headers=extra_headers or {},
|
|
141
|
+
transport=self._transport,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
response.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
|
146
|
+
): # pragma: no cover
|
|
147
|
+
# The Vuforia API returns a 429 response with no JSON body.
|
|
148
|
+
raise TooManyRequestsError(response=response)
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
|
152
|
+
): # pragma: no cover
|
|
153
|
+
raise ServerError(response=response)
|
|
154
|
+
|
|
155
|
+
result_code = json.loads(s=response.text)["result_code"]
|
|
156
|
+
|
|
157
|
+
if result_code == expected_result_code:
|
|
158
|
+
return response
|
|
159
|
+
|
|
160
|
+
raise VWSError.from_result_code(
|
|
161
|
+
result_code=result_code,
|
|
162
|
+
response=response,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def add_target(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
name: str,
|
|
169
|
+
width: float,
|
|
170
|
+
image: _ImageType,
|
|
171
|
+
application_metadata: str | None,
|
|
172
|
+
active_flag: bool,
|
|
173
|
+
) -> str:
|
|
174
|
+
"""Add a target to a Vuforia Web Services database.
|
|
175
|
+
|
|
176
|
+
See
|
|
177
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#add
|
|
178
|
+
for parameter details.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
name: The name of the target.
|
|
182
|
+
width: The width of the target.
|
|
183
|
+
image: The image of the target.
|
|
184
|
+
active_flag: Whether or not the target is active for query.
|
|
185
|
+
application_metadata: The application metadata of the target.
|
|
186
|
+
This must be base64 encoded, for example by using::
|
|
187
|
+
|
|
188
|
+
base64.b64encode('input_string').decode('ascii')
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The target ID of the new target.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
195
|
+
secret key is not correct.
|
|
196
|
+
~vws.exceptions.vws_exceptions.BadImageError: There is a problem
|
|
197
|
+
with the given image. For example, it must be a JPEG or PNG
|
|
198
|
+
file in the grayscale or RGB color space.
|
|
199
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
200
|
+
the request. For example, the given access key does not match a
|
|
201
|
+
known database.
|
|
202
|
+
~vws.exceptions.vws_exceptions.MetadataTooLargeError: The given
|
|
203
|
+
metadata is too large. The maximum size is 1 MB of data when
|
|
204
|
+
Base64 encoded.
|
|
205
|
+
~vws.exceptions.vws_exceptions.ImageTooLargeError: The given image
|
|
206
|
+
is too large.
|
|
207
|
+
~vws.exceptions.vws_exceptions.TargetNameExistError: A target with
|
|
208
|
+
the given ``name`` already exists.
|
|
209
|
+
~vws.exceptions.vws_exceptions.ProjectInactiveError: The project is
|
|
210
|
+
inactive.
|
|
211
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
212
|
+
an error with the time sent to Vuforia.
|
|
213
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
214
|
+
with Vuforia's servers. This has been seen to happen when the
|
|
215
|
+
given name includes a bad character.
|
|
216
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
217
|
+
rate limiting access.
|
|
218
|
+
"""
|
|
219
|
+
image_data = _get_image_data(image=image)
|
|
220
|
+
image_data_encoded = base64.b64encode(s=image_data).decode(
|
|
221
|
+
encoding="ascii",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
data = {
|
|
225
|
+
"name": name,
|
|
226
|
+
"width": width,
|
|
227
|
+
"image": image_data_encoded,
|
|
228
|
+
"active_flag": active_flag,
|
|
229
|
+
"application_metadata": application_metadata,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
content = json.dumps(obj=data).encode(encoding="utf-8")
|
|
233
|
+
|
|
234
|
+
response = await self.make_request(
|
|
235
|
+
method=HTTPMethod.POST,
|
|
236
|
+
data=content,
|
|
237
|
+
request_path="/targets",
|
|
238
|
+
expected_result_code="TargetCreated",
|
|
239
|
+
content_type="application/json",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return str(object=json.loads(s=response.text)["target_id"])
|
|
243
|
+
|
|
244
|
+
async def get_target_record(self, target_id: str) -> TargetStatusAndRecord:
|
|
245
|
+
"""Get a given target's target record from the Target
|
|
246
|
+
Management System.
|
|
247
|
+
|
|
248
|
+
See
|
|
249
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
target_id: The ID of the target to get details of.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Response details of a target from Vuforia.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
259
|
+
secret key is not correct.
|
|
260
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
261
|
+
the request. For example, the given access key does not match a
|
|
262
|
+
known database.
|
|
263
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
264
|
+
ID does not match a target in the database.
|
|
265
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
266
|
+
an error with the time sent to Vuforia.
|
|
267
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
268
|
+
with Vuforia's servers.
|
|
269
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
270
|
+
rate limiting access.
|
|
271
|
+
"""
|
|
272
|
+
response = await self.make_request(
|
|
273
|
+
method=HTTPMethod.GET,
|
|
274
|
+
data=b"",
|
|
275
|
+
request_path=f"/targets/{target_id}",
|
|
276
|
+
expected_result_code="Success",
|
|
277
|
+
content_type="application/json",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
result_data = json.loads(s=response.text)
|
|
281
|
+
return TargetStatusAndRecord.from_response_dict(
|
|
282
|
+
response_dict=result_data,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
async def wait_for_target_processed(
|
|
286
|
+
self,
|
|
287
|
+
*,
|
|
288
|
+
target_id: str,
|
|
289
|
+
seconds_between_requests: float = 0.2,
|
|
290
|
+
timeout_seconds: float = 60 * 5,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Wait up to five minutes (arbitrary) for a target to
|
|
293
|
+
get past the processing stage.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
target_id: The ID of the target to wait for.
|
|
297
|
+
seconds_between_requests: The number of seconds to
|
|
298
|
+
wait between requests made while polling the
|
|
299
|
+
target status.
|
|
300
|
+
We wait 0.2 seconds by default, rather than
|
|
301
|
+
less, than that to decrease the number of calls
|
|
302
|
+
made to the API, to decrease the likelihood of
|
|
303
|
+
hitting the request quota.
|
|
304
|
+
timeout_seconds: The maximum number of seconds to
|
|
305
|
+
wait for the target to be processed.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
309
|
+
secret key is not correct.
|
|
310
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
311
|
+
the request. For example, the given access key does not match a
|
|
312
|
+
known database.
|
|
313
|
+
~vws.exceptions.custom_exceptions.TargetProcessingTimeoutError: The
|
|
314
|
+
target remained in the processing stage for more than
|
|
315
|
+
``timeout_seconds`` seconds.
|
|
316
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
317
|
+
ID does not match a target in the database.
|
|
318
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
319
|
+
an error with the time sent to Vuforia.
|
|
320
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
321
|
+
with Vuforia's servers.
|
|
322
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
323
|
+
rate limiting access.
|
|
324
|
+
"""
|
|
325
|
+
start_time = asyncio.get_event_loop().time()
|
|
326
|
+
while True:
|
|
327
|
+
report = await self.get_target_summary_report(
|
|
328
|
+
target_id=target_id,
|
|
329
|
+
)
|
|
330
|
+
if report.status != TargetStatuses.PROCESSING:
|
|
331
|
+
# Guard against the target still being seen as
|
|
332
|
+
# processing by other endpoints due to eventual
|
|
333
|
+
# consistency.
|
|
334
|
+
await asyncio.sleep(
|
|
335
|
+
delay=seconds_between_requests,
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
elapsed_time = asyncio.get_event_loop().time() - start_time
|
|
340
|
+
if elapsed_time > timeout_seconds: # pragma: no cover
|
|
341
|
+
raise TargetProcessingTimeoutError
|
|
342
|
+
|
|
343
|
+
await asyncio.sleep(
|
|
344
|
+
delay=seconds_between_requests,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def list_targets(self) -> list[str]:
|
|
348
|
+
"""List target IDs.
|
|
349
|
+
|
|
350
|
+
See
|
|
351
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#details-list.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
The IDs of all targets in the database.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
358
|
+
secret key is not correct.
|
|
359
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
360
|
+
the request. For example, the given access key does not match a
|
|
361
|
+
known database.
|
|
362
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
363
|
+
an error with the time sent to Vuforia.
|
|
364
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
365
|
+
with Vuforia's servers.
|
|
366
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
367
|
+
rate limiting access.
|
|
368
|
+
"""
|
|
369
|
+
response = await self.make_request(
|
|
370
|
+
method=HTTPMethod.GET,
|
|
371
|
+
data=b"",
|
|
372
|
+
request_path="/targets",
|
|
373
|
+
expected_result_code="Success",
|
|
374
|
+
content_type="application/json",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
return list(json.loads(s=response.text)["results"])
|
|
378
|
+
|
|
379
|
+
async def get_target_summary_report(
|
|
380
|
+
self, target_id: str
|
|
381
|
+
) -> TargetSummaryReport:
|
|
382
|
+
"""Get a summary report for a target.
|
|
383
|
+
|
|
384
|
+
See
|
|
385
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
target_id: The ID of the target to get a summary
|
|
389
|
+
report for.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Details of the target.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
396
|
+
secret key is not correct.
|
|
397
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
398
|
+
the request. For example, the given access key does not match a
|
|
399
|
+
known database.
|
|
400
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
401
|
+
ID does not match a target in the database.
|
|
402
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
403
|
+
an error with the time sent to Vuforia.
|
|
404
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
405
|
+
with Vuforia's servers.
|
|
406
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
407
|
+
rate limiting access.
|
|
408
|
+
"""
|
|
409
|
+
response = await self.make_request(
|
|
410
|
+
method=HTTPMethod.GET,
|
|
411
|
+
data=b"",
|
|
412
|
+
request_path=f"/summary/{target_id}",
|
|
413
|
+
expected_result_code="Success",
|
|
414
|
+
content_type="application/json",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
result_data = dict(json.loads(s=response.text))
|
|
418
|
+
return TargetSummaryReport.from_response_dict(
|
|
419
|
+
response_dict=result_data,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
async def get_database_summary_report(
|
|
423
|
+
self,
|
|
424
|
+
) -> DatabaseSummaryReport:
|
|
425
|
+
"""Get a summary report for the database.
|
|
426
|
+
|
|
427
|
+
See
|
|
428
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Details of the database.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
435
|
+
secret key is not correct.
|
|
436
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
437
|
+
the request. For example, the given access key does not match a
|
|
438
|
+
known database.
|
|
439
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
440
|
+
an error with the time sent to Vuforia.
|
|
441
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
442
|
+
with Vuforia's servers.
|
|
443
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
444
|
+
rate limiting access.
|
|
445
|
+
"""
|
|
446
|
+
response = await self.make_request(
|
|
447
|
+
method=HTTPMethod.GET,
|
|
448
|
+
data=b"",
|
|
449
|
+
request_path="/summary",
|
|
450
|
+
expected_result_code="Success",
|
|
451
|
+
content_type="application/json",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
response_data = dict(json.loads(s=response.text))
|
|
455
|
+
return DatabaseSummaryReport.from_response_dict(
|
|
456
|
+
response_dict=response_data,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def request_database_reco_counts_report(
|
|
460
|
+
self,
|
|
461
|
+
*,
|
|
462
|
+
year: int,
|
|
463
|
+
month: calendar.Month,
|
|
464
|
+
) -> RecoCountsReportRequest:
|
|
465
|
+
"""Request a per-target recognition count report for the database.
|
|
466
|
+
|
|
467
|
+
Vuforia generates the report in the background, so the report is not
|
|
468
|
+
available to download immediately. Use
|
|
469
|
+
:meth:`wait_for_reco_counts_report` to wait for it.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
year: The year to get recognition counts for.
|
|
473
|
+
month: The month of the year to get recognition counts for.
|
|
474
|
+
Vuforia accepts only the current month and the previous
|
|
475
|
+
month. A month taken from a :class:`datetime.datetime` needs
|
|
476
|
+
wrapping, as in ``calendar.Month(value=now.month)``.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
The URL to download the report from, and the transaction ID of
|
|
480
|
+
the request.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
~vws.exceptions.custom_exceptions.DatabaseIdNotSetError: No
|
|
484
|
+
``database_id`` was given to the client.
|
|
485
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
486
|
+
secret key is not correct, or the client's ``database_id`` is
|
|
487
|
+
not the ID of the database which the client's keys belong to.
|
|
488
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
489
|
+
the request. For example, the given year and month are not
|
|
490
|
+
the current month or the previous month.
|
|
491
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
492
|
+
an error with the time sent to Vuforia.
|
|
493
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
494
|
+
with Vuforia's servers.
|
|
495
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
496
|
+
rate limiting access.
|
|
497
|
+
"""
|
|
498
|
+
response = await self.make_request(
|
|
499
|
+
method=HTTPMethod.POST,
|
|
500
|
+
data=reco_counts_report_body(year=year, month=month),
|
|
501
|
+
request_path=reco_counts_report_path(
|
|
502
|
+
database_id=self._database_id,
|
|
503
|
+
),
|
|
504
|
+
expected_result_code="Success",
|
|
505
|
+
content_type="application/json",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
response_data = dict(json.loads(s=response.text))
|
|
509
|
+
return RecoCountsReportRequest.from_response_dict(
|
|
510
|
+
response_dict=response_data,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
async def download_reco_counts_report(
|
|
514
|
+
self,
|
|
515
|
+
*,
|
|
516
|
+
presigned_url: str,
|
|
517
|
+
) -> RecoCountsReport:
|
|
518
|
+
"""Download a requested reco counts report.
|
|
519
|
+
|
|
520
|
+
The report's URL is not part of the VWS API, so this request is not
|
|
521
|
+
authorized with the client's keys.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
presigned_url: The URL of the report, as given by
|
|
525
|
+
:meth:`request_database_reco_counts_report`.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
The downloaded report.
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportNotReadyError:
|
|
532
|
+
Vuforia has not finished generating the report.
|
|
533
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportDownloadError:
|
|
534
|
+
The report could not be downloaded. For example, the report's
|
|
535
|
+
URL may have expired.
|
|
536
|
+
"""
|
|
537
|
+
response = await self._transport(
|
|
538
|
+
method=HTTPMethod.GET,
|
|
539
|
+
url=presigned_url,
|
|
540
|
+
headers={},
|
|
541
|
+
data=b"",
|
|
542
|
+
request_timeout=self._request_timeout_seconds,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return report_from_download_response(response=response)
|
|
546
|
+
|
|
547
|
+
async def wait_for_reco_counts_report(
|
|
548
|
+
self,
|
|
549
|
+
*,
|
|
550
|
+
presigned_url: str,
|
|
551
|
+
seconds_between_requests: float = 0.2,
|
|
552
|
+
timeout_seconds: float = 60 * 5,
|
|
553
|
+
) -> RecoCountsReport:
|
|
554
|
+
"""Wait for a requested reco counts report to be generated, then
|
|
555
|
+
download it.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
presigned_url: The URL of the report, as given by
|
|
559
|
+
:meth:`request_database_reco_counts_report`.
|
|
560
|
+
seconds_between_requests: The number of seconds to wait between
|
|
561
|
+
requests made while polling the report's URL.
|
|
562
|
+
timeout_seconds: The maximum number of seconds to wait for the
|
|
563
|
+
report to be generated.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The downloaded report.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportTimeoutError:
|
|
570
|
+
The report was not generated within ``timeout_seconds``
|
|
571
|
+
seconds.
|
|
572
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportDownloadError:
|
|
573
|
+
The report could not be downloaded. For example, the report's
|
|
574
|
+
URL may have expired.
|
|
575
|
+
"""
|
|
576
|
+
start_time = time.monotonic()
|
|
577
|
+
while True:
|
|
578
|
+
try:
|
|
579
|
+
return await self.download_reco_counts_report(
|
|
580
|
+
presigned_url=presigned_url,
|
|
581
|
+
)
|
|
582
|
+
except RecoCountsReportNotReadyError:
|
|
583
|
+
elapsed_time = time.monotonic() - start_time
|
|
584
|
+
if elapsed_time > timeout_seconds:
|
|
585
|
+
raise RecoCountsReportTimeoutError from None
|
|
586
|
+
|
|
587
|
+
await asyncio.sleep(delay=seconds_between_requests)
|
|
588
|
+
|
|
589
|
+
async def delete_target(self, target_id: str) -> None:
|
|
590
|
+
"""Delete a given target.
|
|
591
|
+
|
|
592
|
+
See
|
|
593
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#delete.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
target_id: The ID of the target to delete.
|
|
597
|
+
|
|
598
|
+
Raises:
|
|
599
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
600
|
+
secret key is not correct.
|
|
601
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
602
|
+
the request. For example, the given access key does not match a
|
|
603
|
+
known database.
|
|
604
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
605
|
+
ID does not match a target in the database.
|
|
606
|
+
~vws.exceptions.vws_exceptions.TargetStatusProcessingError: The
|
|
607
|
+
given target is in the processing state.
|
|
608
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
609
|
+
an error with the time sent to Vuforia.
|
|
610
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
611
|
+
with Vuforia's servers.
|
|
612
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
613
|
+
rate limiting access.
|
|
614
|
+
"""
|
|
615
|
+
await self.make_request(
|
|
616
|
+
method=HTTPMethod.DELETE,
|
|
617
|
+
data=b"",
|
|
618
|
+
request_path=f"/targets/{target_id}",
|
|
619
|
+
expected_result_code="Success",
|
|
620
|
+
content_type="application/json",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
async def get_duplicate_targets(self, target_id: str) -> list[str]:
|
|
624
|
+
"""Get targets which may be considered duplicates of a
|
|
625
|
+
given target.
|
|
626
|
+
|
|
627
|
+
See
|
|
628
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#check.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
target_id: The ID of the target to delete.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
The target IDs of duplicate targets.
|
|
635
|
+
|
|
636
|
+
Raises:
|
|
637
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
638
|
+
secret key is not correct.
|
|
639
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
640
|
+
the request. For example, the given access key does not match a
|
|
641
|
+
known database.
|
|
642
|
+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
|
|
643
|
+
ID does not match a target in the database.
|
|
644
|
+
~vws.exceptions.vws_exceptions.ProjectInactiveError: The project is
|
|
645
|
+
inactive.
|
|
646
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
647
|
+
an error with the time sent to Vuforia.
|
|
648
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
649
|
+
with Vuforia's servers.
|
|
650
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
651
|
+
rate limiting access.
|
|
652
|
+
"""
|
|
653
|
+
response = await self.make_request(
|
|
654
|
+
method=HTTPMethod.GET,
|
|
655
|
+
data=b"",
|
|
656
|
+
request_path=f"/duplicates/{target_id}",
|
|
657
|
+
expected_result_code="Success",
|
|
658
|
+
content_type="application/json",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return list(
|
|
662
|
+
json.loads(s=response.text)["similar_targets"],
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
async def update_target(
|
|
666
|
+
self,
|
|
667
|
+
*,
|
|
668
|
+
target_id: str,
|
|
669
|
+
name: str | None = None,
|
|
670
|
+
width: float | None = None,
|
|
671
|
+
image: _ImageType | None = None,
|
|
672
|
+
active_flag: bool | None = None,
|
|
673
|
+
application_metadata: str | None = None,
|
|
674
|
+
) -> None:
|
|
675
|
+
"""Update a target in a Vuforia Web Services database.
|
|
676
|
+
|
|
677
|
+
See
|
|
678
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#update
|
|
679
|
+
for parameter details.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
target_id: The ID of the target to update.
|
|
683
|
+
name: The name of the target.
|
|
684
|
+
width: The width of the target.
|
|
685
|
+
image: The image of the target.
|
|
686
|
+
active_flag: Whether or not the target is active
|
|
687
|
+
for query.
|
|
688
|
+
application_metadata: The application metadata of
|
|
689
|
+
the target.
|
|
690
|
+
This must be base64 encoded, for example by
|
|
691
|
+
using::
|
|
692
|
+
|
|
693
|
+
base64.b64encode('input_string').decode('ascii')
|
|
694
|
+
|
|
695
|
+
Giving ``None`` will not change the application
|
|
696
|
+
metadata.
|
|
697
|
+
|
|
698
|
+
Raises:
|
|
699
|
+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
|
|
700
|
+
secret key is not correct.
|
|
701
|
+
~vws.exceptions.vws_exceptions.BadImageError: There is a problem
|
|
702
|
+
with the given image. For example, it must be a JPEG or PNG
|
|
703
|
+
file in the grayscale or RGB color space.
|
|
704
|
+
~vws.exceptions.vws_exceptions.FailError: There was an error with
|
|
705
|
+
the request. For example, the given access key does not match a
|
|
706
|
+
known database.
|
|
707
|
+
~vws.exceptions.vws_exceptions.MetadataTooLargeError: The given
|
|
708
|
+
metadata is too large. The maximum size is 1 MB of data when
|
|
709
|
+
Base64 encoded.
|
|
710
|
+
~vws.exceptions.vws_exceptions.ImageTooLargeError: The given image
|
|
711
|
+
is too large.
|
|
712
|
+
~vws.exceptions.vws_exceptions.TargetNameExistError: A target with
|
|
713
|
+
the given ``name`` already exists.
|
|
714
|
+
~vws.exceptions.vws_exceptions.ProjectInactiveError: The project is
|
|
715
|
+
inactive.
|
|
716
|
+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
|
|
717
|
+
an error with the time sent to Vuforia.
|
|
718
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an error
|
|
719
|
+
with Vuforia's servers.
|
|
720
|
+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
|
|
721
|
+
rate limiting access.
|
|
722
|
+
"""
|
|
723
|
+
data: dict[str, str | bool | float | int] = {}
|
|
724
|
+
|
|
725
|
+
if name is not None:
|
|
726
|
+
data["name"] = name
|
|
727
|
+
|
|
728
|
+
if width is not None:
|
|
729
|
+
data["width"] = width
|
|
730
|
+
|
|
731
|
+
if image is not None:
|
|
732
|
+
image_data = _get_image_data(image=image)
|
|
733
|
+
image_data_encoded = base64.b64encode(
|
|
734
|
+
s=image_data,
|
|
735
|
+
).decode(encoding="ascii")
|
|
736
|
+
data["image"] = image_data_encoded
|
|
737
|
+
|
|
738
|
+
if active_flag is not None:
|
|
739
|
+
data["active_flag"] = active_flag
|
|
740
|
+
|
|
741
|
+
if application_metadata is not None:
|
|
742
|
+
data["application_metadata"] = application_metadata
|
|
743
|
+
|
|
744
|
+
content = json.dumps(obj=data).encode(encoding="utf-8")
|
|
745
|
+
|
|
746
|
+
await self.make_request(
|
|
747
|
+
method=HTTPMethod.PUT,
|
|
748
|
+
data=content,
|
|
749
|
+
request_path=f"/targets/{target_id}",
|
|
750
|
+
expected_result_code="Success",
|
|
751
|
+
content_type="application/json",
|
|
752
|
+
)
|