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