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