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