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/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""A library for Vuforia Web Services."""
|
|
2
|
+
|
|
3
|
+
from .async_model_target_service import AsyncModelTargetService
|
|
4
|
+
from .async_query import AsyncCloudRecoService
|
|
5
|
+
from .async_vumark_service import AsyncVuMarkService
|
|
6
|
+
from .async_vws import AsyncVWS
|
|
7
|
+
from .model_target_service import ModelTargetService
|
|
8
|
+
from .query import CloudRecoService
|
|
9
|
+
from .vumark_service import VuMarkService
|
|
10
|
+
from .vws import VWS
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"VWS",
|
|
14
|
+
"AsyncCloudRecoService",
|
|
15
|
+
"AsyncModelTargetService",
|
|
16
|
+
"AsyncVWS",
|
|
17
|
+
"AsyncVuMarkService",
|
|
18
|
+
"CloudRecoService",
|
|
19
|
+
"ModelTargetService",
|
|
20
|
+
"VuMarkService",
|
|
21
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Internal helper for making authenticated async requests to the
|
|
2
|
+
Vuforia Target API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from beartype import BeartypeConf, beartype
|
|
6
|
+
from vws_auth_tools import authorization_header, rfc_1123_date
|
|
7
|
+
|
|
8
|
+
from vws.response import Response # noqa: TC001
|
|
9
|
+
from vws.transports import AsyncTransport # noqa: TC001
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
13
|
+
async def async_target_api_request(
|
|
14
|
+
*,
|
|
15
|
+
content_type: str,
|
|
16
|
+
server_access_key: str,
|
|
17
|
+
server_secret_key: str,
|
|
18
|
+
method: str,
|
|
19
|
+
data: bytes,
|
|
20
|
+
request_path: str,
|
|
21
|
+
base_vws_url: str,
|
|
22
|
+
request_timeout_seconds: float | tuple[float, float],
|
|
23
|
+
extra_headers: dict[str, str],
|
|
24
|
+
transport: AsyncTransport,
|
|
25
|
+
) -> Response:
|
|
26
|
+
"""Make an async request to the Vuforia Target API.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
content_type: The content type of the request.
|
|
30
|
+
server_access_key: A VWS server access key.
|
|
31
|
+
server_secret_key: A VWS server secret key.
|
|
32
|
+
method: The HTTP method which will be used in the
|
|
33
|
+
request.
|
|
34
|
+
data: The request body which will be used in the
|
|
35
|
+
request.
|
|
36
|
+
request_path: The path to the endpoint which will be
|
|
37
|
+
used in the request.
|
|
38
|
+
base_vws_url: The base URL for the VWS API.
|
|
39
|
+
request_timeout_seconds: The timeout for the request.
|
|
40
|
+
This can be a float to set both the connect and
|
|
41
|
+
read timeouts, or a (connect, read) tuple.
|
|
42
|
+
extra_headers: Additional headers to include in the
|
|
43
|
+
request.
|
|
44
|
+
transport: The async HTTP transport to use for the
|
|
45
|
+
request.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The response to the request.
|
|
49
|
+
"""
|
|
50
|
+
date_string = rfc_1123_date()
|
|
51
|
+
|
|
52
|
+
signature_string = authorization_header(
|
|
53
|
+
access_key=server_access_key,
|
|
54
|
+
secret_key=server_secret_key,
|
|
55
|
+
method=method,
|
|
56
|
+
content=data,
|
|
57
|
+
content_type=content_type,
|
|
58
|
+
date=date_string,
|
|
59
|
+
request_path=request_path,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
headers = {
|
|
63
|
+
"Authorization": signature_string,
|
|
64
|
+
"Date": date_string,
|
|
65
|
+
"Content-Type": content_type,
|
|
66
|
+
**extra_headers,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
url = base_vws_url.rstrip("/") + request_path
|
|
70
|
+
|
|
71
|
+
return await transport(
|
|
72
|
+
method=method,
|
|
73
|
+
url=url,
|
|
74
|
+
headers=headers,
|
|
75
|
+
data=data,
|
|
76
|
+
request_timeout=request_timeout_seconds,
|
|
77
|
+
)
|
vws/_image_utils.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Image utility functions shared across VWS modules."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from typing import BinaryIO
|
|
5
|
+
|
|
6
|
+
from beartype import beartype
|
|
7
|
+
|
|
8
|
+
ImageType = io.BytesIO | BinaryIO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@beartype
|
|
12
|
+
def get_image_data(image: ImageType) -> bytes:
|
|
13
|
+
"""Get the data of an image file."""
|
|
14
|
+
original_tell = image.tell()
|
|
15
|
+
image.seek(0)
|
|
16
|
+
image_data = image.read()
|
|
17
|
+
image.seek(original_tell)
|
|
18
|
+
return image_data
|
vws/_model_targets.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Internal helpers for the Vuforia Model Target Web API."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Sequence # noqa: TC003
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from beartype import BeartypeConf, beartype
|
|
10
|
+
|
|
11
|
+
from vws.exceptions.custom_exceptions import ServerError
|
|
12
|
+
from vws.exceptions.model_target_exceptions import (
|
|
13
|
+
ModelTargetAuthenticationError,
|
|
14
|
+
ModelTargetDatasetNotDoneError,
|
|
15
|
+
ModelTargetError,
|
|
16
|
+
ModelTargetOAuth2Error,
|
|
17
|
+
ModelTargetValidationError,
|
|
18
|
+
UnknownModelTargetDatasetError,
|
|
19
|
+
)
|
|
20
|
+
from vws.exceptions.vws_exceptions import TooManyRequestsError
|
|
21
|
+
from vws.model_target_datasets import ( # noqa: TC001
|
|
22
|
+
ModelTargetDatasetType,
|
|
23
|
+
ModelTargetModel,
|
|
24
|
+
ModelTargetView,
|
|
25
|
+
)
|
|
26
|
+
from vws.reports import ModelTargetDatasetStatusReport
|
|
27
|
+
from vws.response import Response # noqa: TC001
|
|
28
|
+
|
|
29
|
+
OAUTH2_TOKEN_PATH = "/oauth2/token" # noqa: S105
|
|
30
|
+
OAUTH2_TOKEN_BODY = b"grant_type=client_credentials"
|
|
31
|
+
OAUTH2_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded" # noqa: S105
|
|
32
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
33
|
+
|
|
34
|
+
_DATASET_COLLECTION_PATHS = {
|
|
35
|
+
"standard": "/modeltargets/datasets",
|
|
36
|
+
"advanced": "/modeltargets/advancedDatasets",
|
|
37
|
+
}
|
|
38
|
+
_EXCEPTIONS_BY_STATUS_CODE: dict[int, type[ModelTargetError]] = {
|
|
39
|
+
HTTPStatus.BAD_REQUEST: ModelTargetValidationError,
|
|
40
|
+
HTTPStatus.UNAUTHORIZED: ModelTargetAuthenticationError,
|
|
41
|
+
HTTPStatus.NOT_FOUND: UnknownModelTargetDatasetError,
|
|
42
|
+
HTTPStatus.UNPROCESSABLE_ENTITY: ModelTargetDatasetNotDoneError,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
47
|
+
def oauth2_token_headers(
|
|
48
|
+
*, client_id: str, client_secret: str
|
|
49
|
+
) -> dict[str, str]:
|
|
50
|
+
"""Get the headers for a request for an access token.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
client_id: A Model Target Web API client ID.
|
|
54
|
+
client_secret: A Model Target Web API client secret.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The headers to send with a token request.
|
|
58
|
+
"""
|
|
59
|
+
credentials = f"{client_id}:{client_secret}".encode()
|
|
60
|
+
encoded_credentials = base64.b64encode(s=credentials).decode(
|
|
61
|
+
encoding="ascii",
|
|
62
|
+
)
|
|
63
|
+
return {
|
|
64
|
+
"Authorization": f"Basic {encoded_credentials}",
|
|
65
|
+
"Content-Type": OAUTH2_TOKEN_CONTENT_TYPE,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
70
|
+
def access_token_from_response(*, response: Response) -> tuple[str, float]:
|
|
71
|
+
"""Get an access token and its lifetime from a token response.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
response: The response from Vuforia's token endpoint.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The access token, and the number of seconds until it expires.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
|
|
81
|
+
Vuforia did not give an access token.
|
|
82
|
+
"""
|
|
83
|
+
if response.status_code != HTTPStatus.OK:
|
|
84
|
+
raise ModelTargetOAuth2Error(response=response)
|
|
85
|
+
|
|
86
|
+
response_data = dict(json.loads(s=response.text))
|
|
87
|
+
return response_data["access_token"], float(response_data["expires_in"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
91
|
+
def dataset_collection_path(*, dataset_type: ModelTargetDatasetType) -> str:
|
|
92
|
+
"""Get the path of the endpoint for datasets of a given type.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
dataset_type: The kind of dataset to get the path for.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The path of the dataset collection endpoint.
|
|
99
|
+
"""
|
|
100
|
+
return _DATASET_COLLECTION_PATHS[dataset_type.value]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
104
|
+
def dataset_path(
|
|
105
|
+
*,
|
|
106
|
+
dataset_type: ModelTargetDatasetType,
|
|
107
|
+
dataset_uuid: str,
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Get the path of the endpoint for one dataset.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
dataset_type: The kind of dataset to get the path for.
|
|
113
|
+
dataset_uuid: The UUID of the dataset.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The path of the dataset endpoint.
|
|
117
|
+
"""
|
|
118
|
+
collection_path = dataset_collection_path(dataset_type=dataset_type)
|
|
119
|
+
return f"{collection_path}/{dataset_uuid}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
123
|
+
def dataset_status_path(
|
|
124
|
+
*,
|
|
125
|
+
dataset_type: ModelTargetDatasetType,
|
|
126
|
+
dataset_uuid: str,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Get the path of the status endpoint for one dataset.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
dataset_type: The kind of dataset to get the path for.
|
|
132
|
+
dataset_uuid: The UUID of the dataset.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The path of the dataset status endpoint.
|
|
136
|
+
"""
|
|
137
|
+
return (
|
|
138
|
+
dataset_path(dataset_type=dataset_type, dataset_uuid=dataset_uuid)
|
|
139
|
+
+ "/status"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
144
|
+
def dataset_download_path(
|
|
145
|
+
*,
|
|
146
|
+
dataset_type: ModelTargetDatasetType,
|
|
147
|
+
dataset_uuid: str,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Get the path of the download endpoint for one dataset.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
dataset_type: The kind of dataset to get the path for.
|
|
153
|
+
dataset_uuid: The UUID of the dataset.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The path of the dataset download endpoint.
|
|
157
|
+
"""
|
|
158
|
+
return (
|
|
159
|
+
dataset_path(dataset_type=dataset_type, dataset_uuid=dataset_uuid)
|
|
160
|
+
+ "/dataset"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
165
|
+
def _view_dict(*, view: ModelTargetView) -> dict[str, Any]:
|
|
166
|
+
"""Get the request representation of a guide view.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
view: The guide view to represent.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The guide view, as it is sent to Vuforia.
|
|
173
|
+
"""
|
|
174
|
+
view_dict: dict[str, Any] = {
|
|
175
|
+
"name": view.name,
|
|
176
|
+
"guideViewPosition": {
|
|
177
|
+
"rotation": list(view.guide_view_position.rotation),
|
|
178
|
+
"translation": list(view.guide_view_position.translation),
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
if view.states is not None:
|
|
182
|
+
view_dict["states"] = list(view.states)
|
|
183
|
+
|
|
184
|
+
return view_dict
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
188
|
+
def _model_dict(*, model: ModelTargetModel) -> dict[str, Any]:
|
|
189
|
+
"""Get the request representation of a model.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
model: The model to represent.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The model, as it is sent to Vuforia.
|
|
196
|
+
"""
|
|
197
|
+
model_dict: dict[str, Any] = {"name": model.name}
|
|
198
|
+
optional_values: dict[str, str | None] = {
|
|
199
|
+
"automaticColoring": model.automatic_coloring,
|
|
200
|
+
"cadDataBlob": model.cad_data_blob,
|
|
201
|
+
"cadDataFormat": model.cad_data_format,
|
|
202
|
+
"cadDataUrl": model.cad_data_url,
|
|
203
|
+
"motionHint": model.motion_hint,
|
|
204
|
+
"optimizeTrackingFor": model.optimize_tracking_for,
|
|
205
|
+
"realisticAppearance": model.realistic_appearance,
|
|
206
|
+
"simplify": model.simplify,
|
|
207
|
+
"stateBasedConfigurationJsonString": (
|
|
208
|
+
model.state_based_configuration_json_string
|
|
209
|
+
),
|
|
210
|
+
"trackingMode": model.tracking_mode,
|
|
211
|
+
}
|
|
212
|
+
for field_name, value in optional_values.items():
|
|
213
|
+
if value is not None:
|
|
214
|
+
model_dict[field_name] = str(object=value)
|
|
215
|
+
|
|
216
|
+
if model.views is not None:
|
|
217
|
+
model_dict["views"] = [_view_dict(view=view) for view in model.views]
|
|
218
|
+
|
|
219
|
+
return model_dict
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
223
|
+
def dataset_request_body(
|
|
224
|
+
*,
|
|
225
|
+
name: str,
|
|
226
|
+
target_sdk: str,
|
|
227
|
+
models: Sequence[ModelTargetModel],
|
|
228
|
+
) -> bytes:
|
|
229
|
+
"""Get the request body for creating a Model Target dataset.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
name: The name of the dataset.
|
|
233
|
+
target_sdk: The Vuforia Engine version to generate the dataset
|
|
234
|
+
for.
|
|
235
|
+
models: The models to generate the dataset from.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The body of the request.
|
|
239
|
+
"""
|
|
240
|
+
request_dict = {
|
|
241
|
+
"models": [_model_dict(model=model) for model in models],
|
|
242
|
+
"name": name,
|
|
243
|
+
"targetSdk": target_sdk,
|
|
244
|
+
}
|
|
245
|
+
return json.dumps(obj=request_dict).encode(encoding="utf-8")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
249
|
+
def raise_for_error(*, response: Response) -> None:
|
|
250
|
+
"""Raise an exception for an unsuccessful Model Target Web API
|
|
251
|
+
response.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
response: A response from the Model Target Web API.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
|
|
258
|
+
The request was not authenticated.
|
|
259
|
+
~vws.exceptions.model_target_exceptions.ModelTargetValidationError:
|
|
260
|
+
Vuforia rejected the dataset creation request.
|
|
261
|
+
~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
|
|
262
|
+
No dataset of the given type matches the given UUID.
|
|
263
|
+
~vws.exceptions.model_target_exceptions.ModelTargetDatasetNotDoneError:
|
|
264
|
+
The dataset has not been generated.
|
|
265
|
+
~vws.exceptions.model_target_exceptions.ModelTargetError: Vuforia
|
|
266
|
+
returned another error.
|
|
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
|
+
if (
|
|
273
|
+
response.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
|
274
|
+
): # pragma: no cover
|
|
275
|
+
# The Vuforia API returns a 429 response with no JSON body.
|
|
276
|
+
raise TooManyRequestsError(response=response)
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
|
280
|
+
): # pragma: no cover
|
|
281
|
+
raise ServerError(response=response)
|
|
282
|
+
|
|
283
|
+
if response.status_code < HTTPStatus.BAD_REQUEST:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
exception_type = _EXCEPTIONS_BY_STATUS_CODE.get(
|
|
287
|
+
response.status_code,
|
|
288
|
+
ModelTargetError,
|
|
289
|
+
)
|
|
290
|
+
raise exception_type(response=response)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
294
|
+
def dataset_uuid_from_response(*, response: Response) -> str:
|
|
295
|
+
"""Get the UUID of a created dataset.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
response: A response to a dataset creation request.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The UUID of the created dataset.
|
|
302
|
+
"""
|
|
303
|
+
response_data = dict(json.loads(s=response.text))
|
|
304
|
+
return str(object=response_data["uuid"])
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
308
|
+
def status_report_from_response(
|
|
309
|
+
*,
|
|
310
|
+
response: Response,
|
|
311
|
+
) -> ModelTargetDatasetStatusReport:
|
|
312
|
+
"""Get a dataset status report from a status response.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
response: A response to a dataset status request.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The status of the dataset.
|
|
319
|
+
"""
|
|
320
|
+
response_data = dict(json.loads(s=response.text))
|
|
321
|
+
return ModelTargetDatasetStatusReport.from_response_dict(
|
|
322
|
+
response_dict=response_data,
|
|
323
|
+
)
|
vws/_reco_counts.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Internal helpers for the database reco counts report endpoints."""
|
|
2
|
+
|
|
3
|
+
import calendar # noqa: TC003
|
|
4
|
+
import json
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
|
|
7
|
+
from beartype import BeartypeConf, beartype
|
|
8
|
+
|
|
9
|
+
from vws.exceptions.custom_exceptions import (
|
|
10
|
+
DatabaseIdNotSetError,
|
|
11
|
+
RecoCountsReportDownloadError,
|
|
12
|
+
RecoCountsReportNotReadyError,
|
|
13
|
+
)
|
|
14
|
+
from vws.reports import RecoCountsReport
|
|
15
|
+
from vws.response import Response # noqa: TC001
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
19
|
+
def reco_counts_report_path(*, database_id: str | None) -> str:
|
|
20
|
+
"""Get the path of the reco counts report endpoint for a database.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
database_id: The ID of the database to get the path for.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The path of the reco counts report endpoint.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
~vws.exceptions.custom_exceptions.DatabaseIdNotSetError: No
|
|
30
|
+
``database_id`` was given to the client.
|
|
31
|
+
"""
|
|
32
|
+
if database_id is None:
|
|
33
|
+
msg = (
|
|
34
|
+
"A database ID is needed to request a reco counts report. Give "
|
|
35
|
+
"``database_id`` when creating the client."
|
|
36
|
+
)
|
|
37
|
+
raise DatabaseIdNotSetError(msg)
|
|
38
|
+
|
|
39
|
+
return f"/imagetargets/databases/{database_id}/reports/recoCounts"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
43
|
+
def reco_counts_report_body(*, year: int, month: calendar.Month) -> bytes:
|
|
44
|
+
"""Get the request body for requesting a reco counts report.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
year: The year to request the report for.
|
|
48
|
+
month: The month of the year to request the report for.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The body of the request.
|
|
52
|
+
"""
|
|
53
|
+
month_string = f"{year:04d}-{month:02d}"
|
|
54
|
+
return json.dumps(obj={"month": month_string}).encode(encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
58
|
+
def report_from_download_response(*, response: Response) -> RecoCountsReport:
|
|
59
|
+
"""Get a reco counts report from a response from a report's URL.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
response: The response from a report's download URL.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The downloaded report.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportNotReadyError:
|
|
69
|
+
Vuforia has not finished generating the report.
|
|
70
|
+
~vws.exceptions.custom_exceptions.RecoCountsReportDownloadError: The
|
|
71
|
+
report could not be downloaded. For example, the report's URL may
|
|
72
|
+
have expired.
|
|
73
|
+
"""
|
|
74
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
75
|
+
raise RecoCountsReportNotReadyError(response=response)
|
|
76
|
+
|
|
77
|
+
if response.status_code != HTTPStatus.OK:
|
|
78
|
+
raise RecoCountsReportDownloadError(response=response)
|
|
79
|
+
|
|
80
|
+
return RecoCountsReport.from_csv(csv_bytes=response.content)
|
vws/_vws_request.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Internal helper for making authenticated requests to the Vuforia Target
|
|
2
|
+
API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from beartype import BeartypeConf, beartype
|
|
6
|
+
from vws_auth_tools import authorization_header, rfc_1123_date
|
|
7
|
+
|
|
8
|
+
from vws.response import Response # noqa: TC001
|
|
9
|
+
from vws.transports import Transport # noqa: TC001
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
13
|
+
def target_api_request(
|
|
14
|
+
*,
|
|
15
|
+
content_type: str,
|
|
16
|
+
server_access_key: str,
|
|
17
|
+
server_secret_key: str,
|
|
18
|
+
method: str,
|
|
19
|
+
data: bytes,
|
|
20
|
+
request_path: str,
|
|
21
|
+
base_vws_url: str,
|
|
22
|
+
request_timeout_seconds: float | tuple[float, float],
|
|
23
|
+
extra_headers: dict[str, str],
|
|
24
|
+
transport: Transport,
|
|
25
|
+
) -> Response:
|
|
26
|
+
"""Make a request to the Vuforia Target API.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
content_type: The content type of the request.
|
|
30
|
+
server_access_key: A VWS server access key.
|
|
31
|
+
server_secret_key: A VWS server secret key.
|
|
32
|
+
method: The HTTP method which will be used in the
|
|
33
|
+
request.
|
|
34
|
+
data: The request body which will be used in the
|
|
35
|
+
request.
|
|
36
|
+
request_path: The path to the endpoint which will be
|
|
37
|
+
used in the request.
|
|
38
|
+
base_vws_url: The base URL for the VWS API.
|
|
39
|
+
request_timeout_seconds: The timeout for the request.
|
|
40
|
+
This can be a float to set both the connect and
|
|
41
|
+
read timeouts, or a (connect, read) tuple.
|
|
42
|
+
extra_headers: Additional headers to include in the
|
|
43
|
+
request.
|
|
44
|
+
transport: The HTTP transport to use for the request.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The response to the request.
|
|
48
|
+
"""
|
|
49
|
+
date_string = rfc_1123_date()
|
|
50
|
+
|
|
51
|
+
signature_string = authorization_header(
|
|
52
|
+
access_key=server_access_key,
|
|
53
|
+
secret_key=server_secret_key,
|
|
54
|
+
method=method,
|
|
55
|
+
content=data,
|
|
56
|
+
content_type=content_type,
|
|
57
|
+
date=date_string,
|
|
58
|
+
request_path=request_path,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
"Authorization": signature_string,
|
|
63
|
+
"Date": date_string,
|
|
64
|
+
"Content-Type": content_type,
|
|
65
|
+
**extra_headers,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
url = base_vws_url.rstrip("/") + request_path
|
|
69
|
+
|
|
70
|
+
return transport(
|
|
71
|
+
method=method,
|
|
72
|
+
url=url,
|
|
73
|
+
headers=headers,
|
|
74
|
+
data=data,
|
|
75
|
+
request_timeout=request_timeout_seconds,
|
|
76
|
+
)
|