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/query.py ADDED
@@ -0,0 +1,194 @@
1
+ """Tools for interacting with the Vuforia Cloud Recognition Web APIs."""
2
+
3
+ import json
4
+ from http import HTTPMethod, HTTPStatus
5
+ from typing import Any
6
+
7
+ from beartype import BeartypeConf, beartype
8
+ from urllib3.filepost import encode_multipart_formdata
9
+ from vws_auth_tools import authorization_header, rfc_1123_date
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.exceptions.base_exceptions import CloudRecoError
14
+ from vws.exceptions.cloud_reco_exceptions import (
15
+ AuthenticationFailureError,
16
+ BadImageError,
17
+ InactiveProjectError,
18
+ MaxNumResultsOutOfRangeError,
19
+ RequestTimeTooSkewedError,
20
+ )
21
+ from vws.exceptions.custom_exceptions import (
22
+ RequestEntityTooLargeError,
23
+ ServerError,
24
+ )
25
+ from vws.include_target_data import CloudRecoIncludeTargetData
26
+ from vws.reports import QueryResult
27
+ from vws.transports import RequestsTransport, Transport
28
+
29
+
30
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
31
+ class CloudRecoService:
32
+ """An interface to the Vuforia Cloud Recognition Web APIs."""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ client_access_key: str,
38
+ client_secret_key: str,
39
+ base_vwq_url: str = "https://cloudreco.vuforia.com",
40
+ request_timeout_seconds: float | tuple[float, float] = 30.0,
41
+ transport: Transport | None = None,
42
+ ) -> None:
43
+ """
44
+ Args:
45
+ client_access_key: A VWS client access key.
46
+ client_secret_key: A VWS client secret key.
47
+ base_vwq_url: The base URL for the VWQ API.
48
+ request_timeout_seconds: The timeout for each
49
+ HTTP request. This can be a float to set both
50
+ the connect and read timeouts, or a
51
+ (connect, read) tuple.
52
+ transport: The HTTP transport to use for
53
+ requests. Defaults to
54
+ ``RequestsTransport()``.
55
+ """
56
+ self._client_access_key = client_access_key
57
+ self._client_secret_key = client_secret_key
58
+ self._base_vwq_url = base_vwq_url
59
+ self._request_timeout_seconds = request_timeout_seconds
60
+ self._transport = (
61
+ transport if transport is not None else RequestsTransport()
62
+ )
63
+
64
+ def query(
65
+ self,
66
+ *,
67
+ image: _ImageType,
68
+ max_num_results: int = 1,
69
+ include_target_data: CloudRecoIncludeTargetData = (
70
+ CloudRecoIncludeTargetData.TOP
71
+ ),
72
+ ) -> list[QueryResult]:
73
+ """Use the Vuforia Web Query API to make an Image Recognition
74
+ Query.
75
+
76
+ See
77
+ https://developer.vuforia.com/library/web-api/vuforia-query-web-api
78
+ for parameter details.
79
+
80
+ Args:
81
+ image: The image to make a query against.
82
+ max_num_results: The maximum number of matching targets to be
83
+ returned.
84
+ include_target_data: Indicates if target_data records shall be
85
+ returned for the matched targets. Accepted values are top
86
+ (default value, only return target_data for top ranked match),
87
+ none (return no target_data), all (for all matched targets).
88
+
89
+ Raises:
90
+ ~vws.exceptions.cloud_reco_exceptions.AuthenticationFailureError:
91
+ The client access key pair is not correct.
92
+ ~vws.exceptions.cloud_reco_exceptions.MaxNumResultsOutOfRangeError:
93
+ ``max_num_results`` is not within the range (1, 50).
94
+ ~vws.exceptions.cloud_reco_exceptions.InactiveProjectError: The
95
+ project is inactive.
96
+ ~vws.exceptions.cloud_reco_exceptions.RequestTimeTooSkewedError:
97
+ There is an error with the time sent to Vuforia.
98
+ ~vws.exceptions.cloud_reco_exceptions.BadImageError: There is a
99
+ problem with the given image. For example, it must be a JPEG or
100
+ PNG file in the grayscale or RGB color space.
101
+ ~vws.exceptions.custom_exceptions.RequestEntityTooLargeError: The
102
+ given image is too large.
103
+ ~vws.exceptions.custom_exceptions.ServerError: There is an
104
+ error with Vuforia's servers.
105
+ ~vws.exceptions.base_exceptions.CloudRecoError: Vuforia returned
106
+ a client error without a recognized JSON body.
107
+ json.JSONDecodeError: Vuforia returned a successful response with
108
+ an invalid JSON body.
109
+
110
+ Returns:
111
+ An ordered list of target details of matching targets.
112
+ """
113
+ image_content = _get_image_data(image=image)
114
+ body: dict[str, Any] = {
115
+ "image": ("image.jpeg", image_content, "image/jpeg"),
116
+ "max_num_results": (None, int(max_num_results), "text/plain"),
117
+ "include_target_data": (
118
+ None,
119
+ include_target_data.value,
120
+ "text/plain",
121
+ ),
122
+ }
123
+ date = rfc_1123_date()
124
+ request_path = "/v1/query"
125
+ content, content_type_header = encode_multipart_formdata(fields=body)
126
+ method = HTTPMethod.POST
127
+
128
+ authorization_string = authorization_header(
129
+ access_key=self._client_access_key,
130
+ secret_key=self._client_secret_key,
131
+ method=method,
132
+ content=content,
133
+ # Note that this is not the actual Content-Type header value sent.
134
+ content_type="multipart/form-data",
135
+ date=date,
136
+ request_path=request_path,
137
+ )
138
+
139
+ headers = {
140
+ "Authorization": authorization_string,
141
+ "Date": date,
142
+ "Content-Type": content_type_header,
143
+ }
144
+
145
+ response = self._transport(
146
+ method=method,
147
+ url=self._base_vwq_url.rstrip("/") + request_path,
148
+ headers=headers,
149
+ data=content,
150
+ request_timeout=self._request_timeout_seconds,
151
+ )
152
+
153
+ if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
154
+ raise RequestEntityTooLargeError(response=response)
155
+
156
+ if "Integer out of range" in response.text:
157
+ raise MaxNumResultsOutOfRangeError(response=response)
158
+
159
+ if (
160
+ response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
161
+ ): # pragma: no cover
162
+ raise ServerError(response=response)
163
+
164
+ content_type = {
165
+ key.lower(): value for key, value in response.headers.items()
166
+ }.get("content-type", "")
167
+ if (
168
+ response.status_code >= HTTPStatus.BAD_REQUEST
169
+ and not content_type.lower().startswith("application/json")
170
+ ):
171
+ raise CloudRecoError(response=response)
172
+
173
+ try:
174
+ response_body = json.loads(s=response.text)
175
+ except json.JSONDecodeError as exc:
176
+ if response.status_code >= HTTPStatus.BAD_REQUEST:
177
+ raise CloudRecoError(response=response) from exc
178
+ raise
179
+
180
+ result_code = response_body["result_code"]
181
+ if result_code != "Success":
182
+ exception = {
183
+ "AuthenticationFailure": AuthenticationFailureError,
184
+ "BadImage": BadImageError,
185
+ "InactiveProject": InactiveProjectError,
186
+ "RequestTimeTooSkewed": RequestTimeTooSkewedError,
187
+ }[result_code]
188
+ raise exception(response=response)
189
+
190
+ result_list = list(response_body["results"])
191
+ return [
192
+ QueryResult.from_response_dict(response_dict=item)
193
+ for item in result_list
194
+ ]
vws/reports.py ADDED
@@ -0,0 +1,391 @@
1
+ """Classes for representing Vuforia reports."""
2
+
3
+ import csv
4
+ import datetime
5
+ import io
6
+ from collections.abc import Sequence # noqa: TC003
7
+ from dataclasses import dataclass
8
+ from enum import Enum, unique
9
+ from typing import Any, Self
10
+
11
+ from beartype import BeartypeConf, beartype
12
+
13
+
14
+ @beartype
15
+ @dataclass(frozen=True, kw_only=True)
16
+ class DatabaseSummaryReport:
17
+ """A database summary report.
18
+
19
+ See
20
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report.
21
+ """
22
+
23
+ active_images: int
24
+ current_month_recos: int
25
+ failed_images: int
26
+ inactive_images: int
27
+ name: str
28
+ previous_month_recos: int
29
+ processing_images: int
30
+ reco_threshold: int
31
+ request_quota: int
32
+ request_usage: int
33
+ target_quota: int
34
+ total_recos: int
35
+
36
+ @classmethod
37
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
38
+ """Construct from a VWS API response dict."""
39
+ return cls(
40
+ active_images=int(response_dict["active_images"]),
41
+ current_month_recos=int(response_dict["current_month_recos"]),
42
+ failed_images=int(response_dict["failed_images"]),
43
+ inactive_images=int(response_dict["inactive_images"]),
44
+ name=response_dict["name"],
45
+ previous_month_recos=int(response_dict["previous_month_recos"]),
46
+ processing_images=int(response_dict["processing_images"]),
47
+ reco_threshold=int(response_dict["reco_threshold"]),
48
+ request_quota=int(response_dict["request_quota"]),
49
+ request_usage=int(response_dict["request_usage"]),
50
+ target_quota=int(response_dict["target_quota"]),
51
+ total_recos=int(response_dict["total_recos"]),
52
+ )
53
+
54
+
55
+ @beartype
56
+ @unique
57
+ class TargetStatuses(Enum):
58
+ """Constants representing VWS target statuses.
59
+
60
+ See the 'status' field in
61
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record
62
+ """
63
+
64
+ PROCESSING = "processing"
65
+ SUCCESS = "success"
66
+ FAILED = "failed"
67
+
68
+
69
+ @beartype
70
+ @dataclass(frozen=True, kw_only=True)
71
+ class TargetSummaryReport:
72
+ """A target summary report.
73
+
74
+ See
75
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report.
76
+ """
77
+
78
+ status: TargetStatuses
79
+ database_name: str
80
+ target_name: str
81
+ upload_date: datetime.date
82
+ active_flag: bool
83
+ tracking_rating: int
84
+ total_recos: int
85
+ current_month_recos: int
86
+ previous_month_recos: int
87
+
88
+ @classmethod
89
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
90
+ """Construct from a VWS API response dict."""
91
+ return cls(
92
+ status=TargetStatuses(value=response_dict["status"]),
93
+ database_name=response_dict["database_name"],
94
+ target_name=response_dict["target_name"],
95
+ upload_date=datetime.date.fromisoformat(
96
+ response_dict["upload_date"]
97
+ ),
98
+ active_flag=bool(response_dict["active_flag"]),
99
+ tracking_rating=int(response_dict["tracking_rating"]),
100
+ total_recos=int(response_dict["total_recos"]),
101
+ current_month_recos=int(response_dict["current_month_recos"]),
102
+ previous_month_recos=int(response_dict["previous_month_recos"]),
103
+ )
104
+
105
+
106
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
107
+ @dataclass(frozen=True, kw_only=True)
108
+ class TargetRecord:
109
+ """A target record.
110
+
111
+ See
112
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record.
113
+ """
114
+
115
+ target_id: str
116
+ active_flag: bool
117
+ name: str
118
+ width: float
119
+ tracking_rating: int
120
+ reco_rating: str
121
+
122
+
123
+ @beartype
124
+ @dataclass(frozen=True, kw_only=True)
125
+ class TargetData:
126
+ """The target data optionally included with a query match."""
127
+
128
+ name: str
129
+ application_metadata: str | None
130
+ target_timestamp: datetime.datetime
131
+
132
+
133
+ @beartype
134
+ @dataclass(frozen=True, kw_only=True)
135
+ class QueryResult:
136
+ """One query match result.
137
+
138
+ See
139
+ https://developer.vuforia.com/library/web-api/vuforia-query-web-api.
140
+ """
141
+
142
+ target_id: str
143
+ target_data: TargetData | None
144
+
145
+ @classmethod
146
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
147
+ """Construct from a VWS API query result item dict."""
148
+ target_data: TargetData | None = None
149
+ if "target_data" in response_dict:
150
+ target_data_dict = response_dict["target_data"]
151
+ target_timestamp = datetime.datetime.fromtimestamp(
152
+ timestamp=target_data_dict["target_timestamp"],
153
+ tz=datetime.UTC,
154
+ )
155
+ target_data = TargetData(
156
+ name=target_data_dict["name"],
157
+ application_metadata=target_data_dict["application_metadata"],
158
+ target_timestamp=target_timestamp,
159
+ )
160
+ return cls(
161
+ target_id=response_dict["target_id"],
162
+ target_data=target_data,
163
+ )
164
+
165
+
166
+ @beartype
167
+ @dataclass(frozen=True, kw_only=True)
168
+ class TargetStatusAndRecord:
169
+ """The target status and a target record.
170
+
171
+ See
172
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record.
173
+ """
174
+
175
+ status: TargetStatuses
176
+ target_record: TargetRecord
177
+
178
+ @classmethod
179
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
180
+ """Construct from a VWS API response dict."""
181
+ status = TargetStatuses(value=response_dict["status"])
182
+ target_record_dict = dict(response_dict["target_record"])
183
+ target_record = TargetRecord(
184
+ target_id=target_record_dict["target_id"],
185
+ active_flag=bool(target_record_dict["active_flag"]),
186
+ name=target_record_dict["name"],
187
+ width=float(target_record_dict["width"]),
188
+ tracking_rating=int(target_record_dict["tracking_rating"]),
189
+ reco_rating=target_record_dict["reco_rating"],
190
+ )
191
+ return cls(status=status, target_record=target_record)
192
+
193
+
194
+ @beartype
195
+ @dataclass(frozen=True, kw_only=True)
196
+ class RecoCountsReportRequest:
197
+ """A requested database reco counts report.
198
+
199
+ See
200
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api.
201
+ """
202
+
203
+ transaction_id: str
204
+ presigned_url: str
205
+ """The URL to download the report from.
206
+
207
+ Real Vuforia's URLs expire just under seven days after the report is
208
+ requested.
209
+ """
210
+
211
+ @classmethod
212
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
213
+ """Construct from a VWS API response dict."""
214
+ return cls(
215
+ transaction_id=response_dict["transaction_id"],
216
+ presigned_url=response_dict["presigned_url"],
217
+ )
218
+
219
+
220
+ @beartype
221
+ @unique
222
+ class ModelTargetDatasetStatuses(Enum):
223
+ """Constants representing Model Target dataset generation statuses.
224
+
225
+ See the 'status' field of the dataset status response at
226
+ https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/.
227
+ """
228
+
229
+ PROCESSING = "processing"
230
+ DONE = "done"
231
+ FAILED = "failed"
232
+
233
+
234
+ @beartype
235
+ @dataclass(frozen=True, kw_only=True)
236
+ class ModelTargetGenerationDetail:
237
+ """One detail of a Model Target dataset generation warning."""
238
+
239
+ code: str
240
+ message: str
241
+
242
+
243
+ @beartype
244
+ @dataclass(frozen=True, kw_only=True)
245
+ class ModelTargetGenerationError:
246
+ """The reason a Model Target dataset failed to generate."""
247
+
248
+ code: str
249
+ message: str
250
+
251
+
252
+ @beartype
253
+ @dataclass(frozen=True, kw_only=True)
254
+ class ModelTargetGenerationWarning:
255
+ """A warning about a generated Model Target dataset.
256
+
257
+ A dataset with a warning is generated, and can be downloaded.
258
+ """
259
+
260
+ code: str
261
+ message: str
262
+ target: str
263
+ details: Sequence[ModelTargetGenerationDetail]
264
+
265
+
266
+ @beartype
267
+ @dataclass(frozen=True, kw_only=True)
268
+ class ModelTargetDatasetStatusReport:
269
+ """The status of a Model Target dataset.
270
+
271
+ See
272
+ https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/.
273
+ """
274
+
275
+ status: ModelTargetDatasetStatuses
276
+ dataset_uuid: str
277
+ created_at: datetime.datetime
278
+ eta: datetime.datetime | None
279
+ """When Vuforia expects to finish generating the dataset.
280
+
281
+ This is given only while the dataset is processing.
282
+ """
283
+
284
+ completed_at: datetime.datetime | None
285
+ """When Vuforia finished generating the dataset.
286
+
287
+ This is given only once the dataset is no longer processing.
288
+ """
289
+
290
+ error: ModelTargetGenerationError | None
291
+ """Why the dataset failed to generate.
292
+
293
+ This is given only for a failed dataset.
294
+ """
295
+
296
+ warning: ModelTargetGenerationWarning | None
297
+ """A warning about the generated dataset.
298
+
299
+ This is given only for a generated dataset which has a warning.
300
+ """
301
+
302
+ @classmethod
303
+ def from_response_dict(cls, response_dict: dict[str, Any]) -> Self:
304
+ """Construct from a Model Target Web API response dict."""
305
+ error: ModelTargetGenerationError | None = None
306
+ if "error" in response_dict:
307
+ error_dict = dict(response_dict["error"])
308
+ error = ModelTargetGenerationError(
309
+ code=error_dict["code"],
310
+ message=error_dict["message"],
311
+ )
312
+
313
+ warning: ModelTargetGenerationWarning | None = None
314
+ if "warning" in response_dict:
315
+ warning_dict = dict(response_dict["warning"])
316
+ warning = ModelTargetGenerationWarning(
317
+ code=warning_dict["code"],
318
+ message=warning_dict["message"],
319
+ target=warning_dict["target"],
320
+ details=[
321
+ ModelTargetGenerationDetail(
322
+ code=detail["code"],
323
+ message=detail["message"],
324
+ )
325
+ for detail in warning_dict["details"]
326
+ ],
327
+ )
328
+
329
+ eta: datetime.datetime | None = None
330
+ if "eta" in response_dict:
331
+ eta = datetime.datetime.fromisoformat(response_dict["eta"])
332
+
333
+ completed_at: datetime.datetime | None = None
334
+ if "completedAt" in response_dict:
335
+ completed_at = datetime.datetime.fromisoformat(
336
+ response_dict["completedAt"],
337
+ )
338
+
339
+ return cls(
340
+ status=ModelTargetDatasetStatuses(value=response_dict["status"]),
341
+ dataset_uuid=response_dict["uuid"],
342
+ created_at=datetime.datetime.fromisoformat(
343
+ response_dict["createdAt"],
344
+ ),
345
+ eta=eta,
346
+ completed_at=completed_at,
347
+ error=error,
348
+ warning=warning,
349
+ )
350
+
351
+
352
+ @beartype
353
+ @dataclass(frozen=True, kw_only=True)
354
+ class RecoCount:
355
+ """The number of recognitions of one target in a reco counts
356
+ report.
357
+ """
358
+
359
+ target_id: str
360
+ reco_count: int
361
+
362
+
363
+ @beartype
364
+ @dataclass(frozen=True, kw_only=True)
365
+ class RecoCountsReport:
366
+ """A downloaded database reco counts report.
367
+
368
+ A report for a month with no recognitions has no ``reco_counts``.
369
+ """
370
+
371
+ reco_counts: Sequence[RecoCount]
372
+ raw_csv: bytes
373
+ """The downloaded CSV, before it was parsed.
374
+
375
+ Vuforia does not document the format of the report, so it may include
376
+ columns which ``reco_counts`` does not expose.
377
+ """
378
+
379
+ @classmethod
380
+ def from_csv(cls, csv_bytes: bytes) -> Self:
381
+ """Construct from the CSV content of a downloaded report."""
382
+ text = csv_bytes.decode(encoding="utf-8")
383
+ reader = csv.DictReader(f=io.StringIO(initial_value=text, newline=""))
384
+ reco_counts = [
385
+ RecoCount(
386
+ target_id=row["target_id"],
387
+ reco_count=int(row["reco_count"]),
388
+ )
389
+ for row in reader
390
+ ]
391
+ return cls(reco_counts=reco_counts, raw_csv=csv_bytes)
vws/response.py ADDED
@@ -0,0 +1,19 @@
1
+ """Responses for requests to VWS and VWQ."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from beartype import beartype
6
+
7
+
8
+ @dataclass(frozen=True, kw_only=True)
9
+ @beartype
10
+ class Response:
11
+ """A response from a request."""
12
+
13
+ text: str
14
+ url: str
15
+ status_code: int
16
+ headers: dict[str, str]
17
+ request_body: bytes | str | None
18
+ tell_position: int
19
+ content: bytes