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/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
+ )