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.
@@ -0,0 +1,18 @@
1
+ """Tools for managing ``CloudRecoService.query``'s ``include_target_data``."""
2
+
3
+ from enum import StrEnum, auto, unique
4
+
5
+ from beartype import beartype
6
+
7
+
8
+ @beartype
9
+ @unique
10
+ class CloudRecoIncludeTargetData(StrEnum):
11
+ """
12
+ Options for the ``include_target_data`` parameter of
13
+ ``CloudRecoService.query``.
14
+ """
15
+
16
+ TOP = auto()
17
+ NONE = auto()
18
+ ALL = auto()
@@ -0,0 +1,152 @@
1
+ """Structures for describing Model Target datasets to create.
2
+
3
+ See
4
+ https://developer.vuforia.com/library/vuforia-engine/web-api/model-target-web-api/.
5
+ """
6
+
7
+ from collections.abc import Sequence # noqa: TC003
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum, unique
10
+
11
+ from beartype import BeartypeConf, beartype
12
+
13
+
14
+ @beartype
15
+ @unique
16
+ class ModelTargetDatasetType(StrEnum):
17
+ """The kinds of Model Target dataset which Vuforia generates.
18
+
19
+ Standard and advanced datasets are separate resources, so a dataset
20
+ created as one type is not visible to requests for the other type.
21
+ """
22
+
23
+ STANDARD = "standard"
24
+ ADVANCED = "advanced"
25
+
26
+
27
+ @beartype
28
+ @unique
29
+ class AutomaticColoring(StrEnum):
30
+ """Options for a model's ``automaticColoring``."""
31
+
32
+ ALWAYS = "always"
33
+ AUTO = "auto"
34
+ NEVER = "never"
35
+
36
+
37
+ @beartype
38
+ @unique
39
+ class CadDataFormat(StrEnum):
40
+ """Options for a model's ``cadDataFormat``."""
41
+
42
+ DAE = "DAE"
43
+ FBX = "FBX"
44
+ GLB = "GLB"
45
+ IGES = "IGES"
46
+ OBJ = "OBJ"
47
+ PVZ = "PVZ"
48
+ STL = "STL"
49
+ VRML = "VRML"
50
+ ZIP = "ZIP"
51
+
52
+
53
+ @beartype
54
+ @unique
55
+ class MotionHint(StrEnum):
56
+ """Options for a model's ``motionHint``."""
57
+
58
+ ADAPTIVE = "adaptive"
59
+ DYNAMIC = "dynamic"
60
+ STATIC = "static"
61
+
62
+
63
+ @beartype
64
+ @unique
65
+ class OptimizeTrackingFor(StrEnum):
66
+ """Options for a model's ``optimizeTrackingFor``."""
67
+
68
+ AR_CONTROLLER = "ar_controller"
69
+ DEFAULT = "default"
70
+ LOW_FEATURE_OBJECTS = "low_feature_objects"
71
+
72
+
73
+ @beartype
74
+ @unique
75
+ class RealisticAppearance(StrEnum):
76
+ """Options for a model's ``realisticAppearance``.
77
+
78
+ This is documented for advanced datasets only.
79
+ """
80
+
81
+ AUTO = "auto"
82
+ FALSE = "false"
83
+ TRUE = "true"
84
+
85
+
86
+ @beartype
87
+ @unique
88
+ class Simplify(StrEnum):
89
+ """Options for a model's ``simplify``."""
90
+
91
+ ALWAYS = "always"
92
+ AUTO = "auto"
93
+ NEVER = "never"
94
+
95
+
96
+ @beartype
97
+ @unique
98
+ class TrackingMode(StrEnum):
99
+ """Options for a model's ``trackingMode``."""
100
+
101
+ CAR = "car"
102
+ DEFAULT = "default"
103
+ SCAN = "scan"
104
+
105
+
106
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
107
+ @dataclass(frozen=True, kw_only=True)
108
+ class GuideViewPosition:
109
+ """The position of a guide view."""
110
+
111
+ rotation: Sequence[float]
112
+ translation: Sequence[float]
113
+
114
+
115
+ @beartype
116
+ @dataclass(frozen=True, kw_only=True)
117
+ class ModelTargetView:
118
+ """A guide view of a model."""
119
+
120
+ name: str
121
+ guide_view_position: GuideViewPosition
122
+ states: Sequence[str] | None = None
123
+ """The State-Based Model Target states which this view applies to.
124
+
125
+ Every given state must be named by the model's
126
+ ``state_based_configuration_json_string``.
127
+ """
128
+
129
+
130
+ @beartype
131
+ @dataclass(frozen=True, kw_only=True)
132
+ class ModelTargetModel:
133
+ """A model to generate a Model Target dataset from.
134
+
135
+ One and only one of ``cad_data_url`` and ``cad_data_blob`` is
136
+ required.
137
+ """
138
+
139
+ name: str
140
+ cad_data_url: str | None = None
141
+ cad_data_blob: str | None = None
142
+ automatic_coloring: AutomaticColoring | None = None
143
+ cad_data_format: CadDataFormat | None = None
144
+ motion_hint: MotionHint | None = None
145
+ optimize_tracking_for: OptimizeTrackingFor | None = None
146
+ realistic_appearance: RealisticAppearance | None = None
147
+ """This is documented for advanced datasets only."""
148
+
149
+ simplify: Simplify | None = None
150
+ tracking_mode: TrackingMode | None = None
151
+ state_based_configuration_json_string: str | None = None
152
+ views: Sequence[ModelTargetView] | None = None
@@ -0,0 +1,366 @@
1
+ """Interface to the Vuforia Model Target Web API."""
2
+
3
+ import time
4
+ from collections.abc import Sequence # noqa: TC003
5
+ from http import HTTPMethod
6
+
7
+ from beartype import BeartypeConf, beartype
8
+
9
+ from vws._model_targets import (
10
+ JSON_CONTENT_TYPE,
11
+ OAUTH2_TOKEN_BODY,
12
+ OAUTH2_TOKEN_PATH,
13
+ access_token_from_response,
14
+ dataset_collection_path,
15
+ dataset_download_path,
16
+ dataset_path,
17
+ dataset_request_body,
18
+ dataset_status_path,
19
+ dataset_uuid_from_response,
20
+ oauth2_token_headers,
21
+ raise_for_error,
22
+ status_report_from_response,
23
+ )
24
+ from vws.exceptions.model_target_exceptions import (
25
+ ModelTargetDatasetTimeoutError,
26
+ )
27
+ from vws.model_target_datasets import ( # noqa: TC001
28
+ ModelTargetDatasetType,
29
+ ModelTargetModel,
30
+ )
31
+ from vws.reports import (
32
+ ModelTargetDatasetStatuses,
33
+ ModelTargetDatasetStatusReport,
34
+ )
35
+ from vws.response import Response # noqa: TC001
36
+ from vws.transports import RequestsTransport, Transport
37
+
38
+ _TOKEN_EXPIRY_MARGIN_SECONDS = 60.0
39
+
40
+
41
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
42
+ class ModelTargetService:
43
+ """An interface to the Vuforia Model Target Web API."""
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ client_id: str,
49
+ client_secret: str,
50
+ base_vws_url: str = "https://vws.vuforia.com",
51
+ request_timeout_seconds: float | tuple[float, float] = 30.0,
52
+ transport: Transport | None = None,
53
+ ) -> None:
54
+ """
55
+ Args:
56
+ client_id: A Model Target Web API OAuth2 client
57
+ ID.
58
+ client_secret: A Model Target Web API OAuth2
59
+ client secret.
60
+ base_vws_url: The base URL for the VWS API, which
61
+ also serves the Model Target Web API.
62
+ request_timeout_seconds: The timeout for each
63
+ HTTP request. This can be a float to set both
64
+ the connect and read timeouts, or a
65
+ (connect, read) tuple.
66
+ transport: The HTTP transport to use for
67
+ requests. Defaults to
68
+ ``RequestsTransport()``.
69
+ """
70
+ self._client_id = client_id
71
+ self._client_secret = client_secret
72
+ self._base_vws_url = base_vws_url
73
+ self._request_timeout_seconds = request_timeout_seconds
74
+ self._transport = (
75
+ transport if transport is not None else RequestsTransport()
76
+ )
77
+ self._access_token: str | None = None
78
+ self._access_token_expiry_time = 0.0
79
+
80
+ def get_access_token(self) -> str:
81
+ """Get an OAuth2 access token for the Model Target Web API.
82
+
83
+ A token is requested only when the client has no token which is
84
+ still valid, so this can be called before each request.
85
+
86
+ Returns:
87
+ A bearer token.
88
+
89
+ Raises:
90
+ ~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
91
+ Vuforia did not give an access token. For example, the
92
+ given client ID and client secret may not match a set of
93
+ Model Target Web API credentials.
94
+ """
95
+ request_time = time.monotonic()
96
+ if (
97
+ self._access_token is not None
98
+ and request_time < self._access_token_expiry_time
99
+ ):
100
+ return self._access_token
101
+
102
+ response = self._transport(
103
+ method=HTTPMethod.POST,
104
+ url=self._base_vws_url.rstrip("/") + OAUTH2_TOKEN_PATH,
105
+ headers=oauth2_token_headers(
106
+ client_id=self._client_id,
107
+ client_secret=self._client_secret,
108
+ ),
109
+ data=OAUTH2_TOKEN_BODY,
110
+ request_timeout=self._request_timeout_seconds,
111
+ )
112
+
113
+ access_token, expires_in_seconds = access_token_from_response(
114
+ response=response,
115
+ )
116
+ self._access_token = access_token
117
+ self._access_token_expiry_time = (
118
+ request_time + expires_in_seconds - _TOKEN_EXPIRY_MARGIN_SECONDS
119
+ )
120
+ return access_token
121
+
122
+ def make_request(
123
+ self,
124
+ *,
125
+ method: str,
126
+ data: bytes,
127
+ request_path: str,
128
+ extra_headers: dict[str, str] | None = None,
129
+ ) -> Response:
130
+ """Make an authenticated request to the Model Target Web API.
131
+
132
+ Args:
133
+ method: The HTTP method which will be used in
134
+ the request.
135
+ data: The request body which will be used in the
136
+ request.
137
+ request_path: The path to the endpoint which
138
+ will be used in the request.
139
+ extra_headers: Additional headers to include in
140
+ the request.
141
+
142
+ Returns:
143
+ The response to the request.
144
+
145
+ Raises:
146
+ ~vws.exceptions.model_target_exceptions.ModelTargetError:
147
+ Vuforia returned an error.
148
+ ~vws.exceptions.custom_exceptions.ServerError:
149
+ There is an error with Vuforia's servers.
150
+ ~vws.exceptions.vws_exceptions.TooManyRequestsError:
151
+ Vuforia is rate limiting access.
152
+ """
153
+ headers = {
154
+ "Authorization": f"Bearer {self.get_access_token()}",
155
+ **(extra_headers or {}),
156
+ }
157
+
158
+ response = self._transport(
159
+ method=method,
160
+ url=self._base_vws_url.rstrip("/") + request_path,
161
+ headers=headers,
162
+ data=data,
163
+ request_timeout=self._request_timeout_seconds,
164
+ )
165
+
166
+ raise_for_error(response=response)
167
+ return response
168
+
169
+ def create_dataset(
170
+ self,
171
+ *,
172
+ name: str,
173
+ target_sdk: str,
174
+ models: Sequence[ModelTargetModel],
175
+ dataset_type: ModelTargetDatasetType,
176
+ ) -> str:
177
+ """Start generating a Model Target dataset.
178
+
179
+ Vuforia generates the dataset in the background, so it is not
180
+ available to download immediately. Use
181
+ :meth:`wait_for_dataset_generated` to wait for it.
182
+
183
+ Args:
184
+ name: The name of the dataset.
185
+ target_sdk: The Vuforia Engine version to generate the dataset
186
+ for.
187
+ models: The models to generate the dataset from. A standard
188
+ dataset takes exactly one model.
189
+ dataset_type: Whether to create a standard or an advanced
190
+ dataset.
191
+
192
+ Returns:
193
+ The UUID of the new dataset.
194
+
195
+ Raises:
196
+ ~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
197
+ The request was not authenticated.
198
+ ~vws.exceptions.model_target_exceptions.ModelTargetValidationError:
199
+ Vuforia rejected the request. For example, a model may
200
+ give neither a CAD data URL nor a CAD data blob.
201
+ ~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
202
+ Vuforia did not give an access token.
203
+ """
204
+ response = self.make_request(
205
+ method=HTTPMethod.POST,
206
+ data=dataset_request_body(
207
+ name=name,
208
+ target_sdk=target_sdk,
209
+ models=models,
210
+ ),
211
+ request_path=dataset_collection_path(dataset_type=dataset_type),
212
+ extra_headers={"Content-Type": JSON_CONTENT_TYPE},
213
+ )
214
+
215
+ return dataset_uuid_from_response(response=response)
216
+
217
+ def get_dataset_status(
218
+ self,
219
+ *,
220
+ dataset_uuid: str,
221
+ dataset_type: ModelTargetDatasetType,
222
+ ) -> ModelTargetDatasetStatusReport:
223
+ """Get the status of a Model Target dataset.
224
+
225
+ Args:
226
+ dataset_uuid: The UUID of the dataset, as given by
227
+ :meth:`create_dataset`.
228
+ dataset_type: The kind of dataset to get the status of.
229
+
230
+ Returns:
231
+ The status of the dataset.
232
+
233
+ Raises:
234
+ ~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
235
+ The request was not authenticated.
236
+ ~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
237
+ No dataset of the given type matches the given UUID.
238
+ ~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
239
+ Vuforia did not give an access token.
240
+ """
241
+ response = self.make_request(
242
+ method=HTTPMethod.GET,
243
+ data=b"",
244
+ request_path=dataset_status_path(
245
+ dataset_type=dataset_type,
246
+ dataset_uuid=dataset_uuid,
247
+ ),
248
+ )
249
+
250
+ return status_report_from_response(response=response)
251
+
252
+ def wait_for_dataset_generated(
253
+ self,
254
+ *,
255
+ dataset_uuid: str,
256
+ dataset_type: ModelTargetDatasetType,
257
+ seconds_between_requests: float = 0.2,
258
+ timeout_seconds: float = 60 * 5,
259
+ ) -> ModelTargetDatasetStatusReport:
260
+ """Wait for Vuforia to finish generating a Model Target dataset.
261
+
262
+ A dataset which failed to generate is also finished, so the
263
+ returned report may have a
264
+ :attr:`~.ModelTargetDatasetStatusReport.status` of
265
+ ``FAILED``.
266
+
267
+ Args:
268
+ dataset_uuid: The UUID of the dataset, as given by
269
+ :meth:`create_dataset`.
270
+ dataset_type: The kind of dataset to wait for.
271
+ seconds_between_requests: The number of seconds to wait between
272
+ requests made while polling the dataset's status.
273
+ timeout_seconds: The maximum number of seconds to wait for the
274
+ dataset to be generated.
275
+
276
+ Returns:
277
+ The status of the dataset once it is no longer processing.
278
+
279
+ Raises:
280
+ ~vws.exceptions.model_target_exceptions.ModelTargetDatasetTimeoutError:
281
+ The dataset was not generated within ``timeout_seconds``
282
+ seconds.
283
+ ~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
284
+ No dataset of the given type matches the given UUID.
285
+ """
286
+ start_time = time.monotonic()
287
+ while True:
288
+ report = self.get_dataset_status(
289
+ dataset_uuid=dataset_uuid,
290
+ dataset_type=dataset_type,
291
+ )
292
+ if report.status != ModelTargetDatasetStatuses.PROCESSING:
293
+ return report
294
+
295
+ elapsed_time = time.monotonic() - start_time
296
+ if elapsed_time > timeout_seconds:
297
+ raise ModelTargetDatasetTimeoutError
298
+
299
+ time.sleep(seconds_between_requests)
300
+
301
+ def download_dataset(
302
+ self,
303
+ *,
304
+ dataset_uuid: str,
305
+ dataset_type: ModelTargetDatasetType,
306
+ ) -> bytes:
307
+ """Download a generated Model Target dataset.
308
+
309
+ Args:
310
+ dataset_uuid: The UUID of the dataset, as given by
311
+ :meth:`create_dataset`.
312
+ dataset_type: The kind of dataset to download.
313
+
314
+ Returns:
315
+ The dataset, as the bytes of a zip file.
316
+
317
+ Raises:
318
+ ~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
319
+ The request was not authenticated.
320
+ ~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
321
+ No dataset of the given type matches the given UUID.
322
+ ~vws.exceptions.model_target_exceptions.ModelTargetDatasetNotDoneError:
323
+ Vuforia has not generated the dataset.
324
+ ~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
325
+ Vuforia did not give an access token.
326
+ """
327
+ response = self.make_request(
328
+ method=HTTPMethod.GET,
329
+ data=b"",
330
+ request_path=dataset_download_path(
331
+ dataset_type=dataset_type,
332
+ dataset_uuid=dataset_uuid,
333
+ ),
334
+ )
335
+
336
+ return response.content
337
+
338
+ def delete_dataset(
339
+ self,
340
+ *,
341
+ dataset_uuid: str,
342
+ dataset_type: ModelTargetDatasetType,
343
+ ) -> None:
344
+ """Delete a Model Target dataset.
345
+
346
+ Args:
347
+ dataset_uuid: The UUID of the dataset, as given by
348
+ :meth:`create_dataset`.
349
+ dataset_type: The kind of dataset to delete.
350
+
351
+ Raises:
352
+ ~vws.exceptions.model_target_exceptions.ModelTargetAuthenticationError:
353
+ The request was not authenticated.
354
+ ~vws.exceptions.model_target_exceptions.UnknownModelTargetDatasetError:
355
+ No dataset of the given type matches the given UUID.
356
+ ~vws.exceptions.model_target_exceptions.ModelTargetOAuth2Error:
357
+ Vuforia did not give an access token.
358
+ """
359
+ self.make_request(
360
+ method=HTTPMethod.DELETE,
361
+ data=b"",
362
+ request_path=dataset_path(
363
+ dataset_type=dataset_type,
364
+ dataset_uuid=dataset_uuid,
365
+ ),
366
+ )
vws/py.typed ADDED
File without changes