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/transports.py ADDED
@@ -0,0 +1,315 @@
1
+ """HTTP transport implementations for VWS clients."""
2
+
3
+ from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
4
+
5
+ import httpx
6
+ import requests
7
+ from beartype import BeartypeConf, beartype
8
+
9
+ from vws.response import Response
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Awaitable
13
+
14
+
15
+ @runtime_checkable
16
+ class Transport(Protocol):
17
+ """Protocol for HTTP transports used by VWS clients.
18
+
19
+ A transport is a callable that makes an HTTP request and
20
+ returns a ``Response``.
21
+ """
22
+
23
+ def close(self) -> None:
24
+ """Close the transport and release resources."""
25
+ ... # pylint: disable=unnecessary-ellipsis
26
+
27
+ def __call__(
28
+ self,
29
+ *,
30
+ method: str,
31
+ url: str,
32
+ headers: dict[str, str],
33
+ data: bytes,
34
+ request_timeout: float | tuple[float, float],
35
+ ) -> Response:
36
+ """Make an HTTP request.
37
+
38
+ Args:
39
+ method: The HTTP method (e.g. "GET", "POST").
40
+ url: The full URL to request.
41
+ headers: Headers to send with the request.
42
+ data: The request body as bytes.
43
+ request_timeout: The timeout for the request. A float
44
+ sets both the connect and read timeouts. A
45
+ (connect, read) tuple sets them individually.
46
+
47
+ Returns:
48
+ A Response populated from the HTTP response.
49
+ """
50
+ ... # pylint: disable=unnecessary-ellipsis
51
+
52
+
53
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
54
+ class RequestsTransport:
55
+ """HTTP transport using the ``requests`` library.
56
+
57
+ This is the default transport.
58
+ """
59
+
60
+ def close(self) -> None:
61
+ """Close the transport.
62
+
63
+ This is a no-op for ``RequestsTransport`` as it does not
64
+ hold persistent connections.
65
+ """
66
+
67
+ def __call__(
68
+ self,
69
+ *,
70
+ method: str,
71
+ url: str,
72
+ headers: dict[str, str],
73
+ data: bytes,
74
+ request_timeout: float | tuple[float, float],
75
+ ) -> Response:
76
+ """Make an HTTP request using ``requests``.
77
+
78
+ Args:
79
+ method: The HTTP method.
80
+ url: The full URL.
81
+ headers: Request headers.
82
+ data: The request body.
83
+ request_timeout: The request timeout.
84
+
85
+ Returns:
86
+ A Response populated from the requests response.
87
+ """
88
+ requests_response = requests.request(
89
+ method=method,
90
+ url=url,
91
+ headers=headers,
92
+ data=data,
93
+ timeout=request_timeout,
94
+ )
95
+
96
+ return Response(
97
+ text=requests_response.text,
98
+ url=requests_response.url,
99
+ status_code=requests_response.status_code,
100
+ headers=dict(requests_response.headers),
101
+ request_body=requests_response.request.body,
102
+ tell_position=requests_response.raw.tell(),
103
+ content=bytes(requests_response.content),
104
+ )
105
+
106
+
107
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
108
+ class HTTPXTransport:
109
+ """HTTP transport using the ``httpx`` library.
110
+
111
+ Use this transport for environments where ``httpx`` is
112
+ preferred over ``requests``.
113
+ A single ``httpx.Client`` is reused across requests
114
+ for connection pooling.
115
+ """
116
+
117
+ def __init__(self) -> None:
118
+ """Create an ``HTTPXTransport``."""
119
+ self._client = httpx.Client()
120
+
121
+ def close(self) -> None:
122
+ """Close the underlying ``httpx.Client``."""
123
+ self._client.close()
124
+
125
+ def __enter__(self) -> Self:
126
+ """Enter the context manager."""
127
+ return self
128
+
129
+ def __exit__(self, *_args: object) -> None:
130
+ """Exit the context manager and close the client."""
131
+ self.close()
132
+
133
+ def __call__(
134
+ self,
135
+ *,
136
+ method: str,
137
+ url: str,
138
+ headers: dict[str, str],
139
+ data: bytes,
140
+ request_timeout: float | tuple[float, float],
141
+ ) -> Response:
142
+ """Make an HTTP request using ``httpx``.
143
+
144
+ Args:
145
+ method: The HTTP method.
146
+ url: The full URL.
147
+ headers: Request headers.
148
+ data: The request body.
149
+ request_timeout: The request timeout.
150
+
151
+ Returns:
152
+ A Response populated from the httpx response.
153
+ """
154
+ match request_timeout:
155
+ case tuple() as timeout:
156
+ connect_timeout, read_timeout = timeout
157
+ httpx_timeout = httpx.Timeout(
158
+ connect=connect_timeout,
159
+ read=read_timeout,
160
+ write=None,
161
+ pool=None,
162
+ )
163
+ case timeout:
164
+ httpx_timeout = httpx.Timeout(
165
+ connect=timeout,
166
+ read=timeout,
167
+ write=None,
168
+ pool=None,
169
+ )
170
+
171
+ httpx_response = self._client.request(
172
+ method=method,
173
+ url=url,
174
+ headers=headers,
175
+ content=data,
176
+ timeout=httpx_timeout,
177
+ follow_redirects=True,
178
+ )
179
+
180
+ content = bytes(httpx_response.content)
181
+ request_content = httpx_response.request.content
182
+
183
+ return Response(
184
+ text=httpx_response.text,
185
+ url=str(object=httpx_response.url),
186
+ status_code=httpx_response.status_code,
187
+ headers=dict(httpx_response.headers),
188
+ request_body=bytes(request_content) or None,
189
+ tell_position=len(content),
190
+ content=content,
191
+ )
192
+
193
+
194
+ @runtime_checkable
195
+ class AsyncTransport(Protocol):
196
+ """Protocol for async HTTP transports used by VWS clients.
197
+
198
+ An async transport is a callable that makes an HTTP request
199
+ and returns a ``Response``.
200
+ """
201
+
202
+ async def aclose(self) -> None:
203
+ """Close the transport and release resources."""
204
+ ... # pylint: disable=unnecessary-ellipsis
205
+
206
+ def __call__(
207
+ self,
208
+ *,
209
+ method: str,
210
+ url: str,
211
+ headers: dict[str, str],
212
+ data: bytes,
213
+ request_timeout: float | tuple[float, float],
214
+ ) -> Awaitable[Response]:
215
+ """Make an async HTTP request.
216
+
217
+ Args:
218
+ method: The HTTP method (e.g. "GET", "POST").
219
+ url: The full URL to request.
220
+ headers: Headers to send with the request.
221
+ data: The request body as bytes.
222
+ request_timeout: The timeout for the request. A float
223
+ sets both the connect and read timeouts. A
224
+ (connect, read) tuple sets them individually.
225
+
226
+ Returns:
227
+ A Response populated from the HTTP response.
228
+ """
229
+ ... # pylint: disable=unnecessary-ellipsis
230
+
231
+
232
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
233
+ class AsyncHTTPXTransport:
234
+ """Async HTTP transport using the ``httpx`` library.
235
+
236
+ This is the default transport for async VWS clients.
237
+ A single ``httpx.AsyncClient`` is reused across requests
238
+ for connection pooling.
239
+ """
240
+
241
+ def __init__(self) -> None:
242
+ """Create an ``AsyncHTTPXTransport``."""
243
+ self._client = httpx.AsyncClient()
244
+
245
+ async def aclose(self) -> None:
246
+ """Close the underlying ``httpx.AsyncClient``."""
247
+ await self._client.aclose()
248
+
249
+ async def __aenter__(self) -> Self:
250
+ """Enter the async context manager."""
251
+ return self
252
+
253
+ async def __aexit__(self, *_args: object) -> None:
254
+ """Exit the async context manager and close the client."""
255
+ await self.aclose()
256
+
257
+ async def __call__(
258
+ self,
259
+ *,
260
+ method: str,
261
+ url: str,
262
+ headers: dict[str, str],
263
+ data: bytes,
264
+ request_timeout: float | tuple[float, float],
265
+ ) -> Response:
266
+ """Make an async HTTP request using ``httpx``.
267
+
268
+ Args:
269
+ method: The HTTP method.
270
+ url: The full URL.
271
+ headers: Request headers.
272
+ data: The request body.
273
+ request_timeout: The request timeout.
274
+
275
+ Returns:
276
+ A Response populated from the httpx response.
277
+ """
278
+ match request_timeout:
279
+ case tuple() as timeout:
280
+ connect_timeout, read_timeout = timeout
281
+ httpx_timeout = httpx.Timeout(
282
+ connect=connect_timeout,
283
+ read=read_timeout,
284
+ write=None,
285
+ pool=None,
286
+ )
287
+ case timeout:
288
+ httpx_timeout = httpx.Timeout(
289
+ connect=timeout,
290
+ read=timeout,
291
+ write=None,
292
+ pool=None,
293
+ )
294
+
295
+ httpx_response = await self._client.request(
296
+ method=method,
297
+ url=url,
298
+ headers=headers,
299
+ content=data,
300
+ timeout=httpx_timeout,
301
+ follow_redirects=True,
302
+ )
303
+
304
+ content = bytes(httpx_response.content)
305
+ request_content = httpx_response.request.content
306
+
307
+ return Response(
308
+ text=httpx_response.text,
309
+ url=str(object=httpx_response.url),
310
+ status_code=httpx_response.status_code,
311
+ headers=dict(httpx_response.headers),
312
+ request_body=bytes(request_content) or None,
313
+ tell_position=len(content),
314
+ content=content,
315
+ )
vws/vumark_accept.py ADDED
@@ -0,0 +1,18 @@
1
+ """Tools for managing ``VWS.generate_vumark_instance``'s ``accept``."""
2
+
3
+ from enum import StrEnum, unique
4
+
5
+ from beartype import beartype
6
+
7
+
8
+ @beartype
9
+ @unique
10
+ class VuMarkAccept(StrEnum):
11
+ """
12
+ Options for the ``accept`` parameter of
13
+ ``VWS.generate_vumark_instance``.
14
+ """
15
+
16
+ PNG = "image/png"
17
+ SVG = "image/svg+xml"
18
+ PDF = "application/pdf"
vws/vumark_service.py ADDED
@@ -0,0 +1,138 @@
1
+ """Interface to the Vuforia VuMark Generation Web API."""
2
+
3
+ import json
4
+ from http import HTTPMethod, HTTPStatus
5
+
6
+ from beartype import BeartypeConf, beartype
7
+
8
+ from vws._vws_request import target_api_request
9
+ from vws.exceptions.base_exceptions import VWSError
10
+ from vws.exceptions.custom_exceptions import ServerError
11
+ from vws.exceptions.vws_exceptions import TooManyRequestsError
12
+ from vws.transports import RequestsTransport, Transport
13
+ from vws.vumark_accept import VuMarkAccept # noqa: TC001
14
+
15
+
16
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
17
+ class VuMarkService:
18
+ """An interface to the Vuforia VuMark Generation Web API."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ server_access_key: str,
24
+ server_secret_key: str,
25
+ base_vws_url: str = "https://vws.vuforia.com",
26
+ request_timeout_seconds: float | tuple[float, float] = 30.0,
27
+ transport: Transport | None = None,
28
+ ) -> None:
29
+ """
30
+ Args:
31
+ server_access_key: A VWS server access key.
32
+ server_secret_key: A VWS server secret key.
33
+ base_vws_url: The base URL for the VWS API.
34
+ request_timeout_seconds: The timeout for each
35
+ HTTP request. This can be a float to set both
36
+ the connect and read timeouts, or a
37
+ (connect, read) tuple.
38
+ transport: The HTTP transport to use for
39
+ requests. Defaults to
40
+ ``RequestsTransport()``.
41
+ """
42
+ self._server_access_key = server_access_key
43
+ self._server_secret_key = server_secret_key
44
+ self._base_vws_url = base_vws_url
45
+ self._request_timeout_seconds = request_timeout_seconds
46
+ self._transport = (
47
+ transport if transport is not None else RequestsTransport()
48
+ )
49
+
50
+ def generate_vumark_instance(
51
+ self,
52
+ *,
53
+ target_id: str,
54
+ instance_id: str,
55
+ accept: VuMarkAccept,
56
+ ) -> bytes:
57
+ """Generate a VuMark instance image.
58
+
59
+ See
60
+ https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/
61
+ for parameter details.
62
+
63
+ Args:
64
+ target_id: The ID of the VuMark target.
65
+ instance_id: The instance ID to encode in the VuMark.
66
+ accept: The image format to return.
67
+
68
+ Returns:
69
+ The VuMark instance image bytes.
70
+
71
+ Raises:
72
+ ~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
73
+ secret key is not correct.
74
+ ~vws.exceptions.vws_exceptions.AuthorizationFailedError: There was
75
+ a general authentication problem.
76
+ ~vws.exceptions.vws_exceptions.FailError: There was an error with
77
+ the request. For example, the given access key does not match a
78
+ known database.
79
+ ~vws.exceptions.vws_exceptions.InvalidAcceptHeaderError: The
80
+ Accept header value is not supported.
81
+ ~vws.exceptions.vws_exceptions.InvalidInstanceIdError: The
82
+ instance ID is invalid. For example, it may be empty.
83
+ ~vws.exceptions.vws_exceptions.InvalidTargetTypeError: The target
84
+ is not a VuMark template target.
85
+ ~vws.exceptions.vws_exceptions.LicenseCheckFailedError: The
86
+ license state and/or type does not allow this request.
87
+ ~vws.exceptions.vws_exceptions.QuotaExceededError: No more
88
+ instances can be created for the associated license.
89
+ ~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
90
+ an error with the time sent to Vuforia.
91
+ ~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The
92
+ target is not in the success state.
93
+ ~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
94
+ ID does not match a target in the database.
95
+ ~vws.exceptions.custom_exceptions.ServerError: There is an error
96
+ with Vuforia's servers.
97
+ ~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
98
+ rate limiting access.
99
+ """
100
+ request_path = f"/targets/{target_id}/instances"
101
+ content_type = "application/json"
102
+ request_data = json.dumps(obj={"instance_id": instance_id}).encode(
103
+ encoding="utf-8",
104
+ )
105
+
106
+ response = target_api_request(
107
+ content_type=content_type,
108
+ server_access_key=self._server_access_key,
109
+ server_secret_key=self._server_secret_key,
110
+ method=HTTPMethod.POST,
111
+ data=request_data,
112
+ request_path=request_path,
113
+ base_vws_url=self._base_vws_url,
114
+ request_timeout_seconds=self._request_timeout_seconds,
115
+ extra_headers={"Accept": accept},
116
+ transport=self._transport,
117
+ )
118
+
119
+ if (
120
+ response.status_code == HTTPStatus.TOO_MANY_REQUESTS
121
+ ): # pragma: no cover
122
+ # The Vuforia API returns a 429 response with no JSON body.
123
+ raise TooManyRequestsError(response=response)
124
+
125
+ if (
126
+ response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
127
+ ): # pragma: no cover
128
+ raise ServerError(response=response)
129
+
130
+ if response.status_code == HTTPStatus.OK:
131
+ return response.content
132
+
133
+ result_code = json.loads(s=response.text)["result_code"]
134
+
135
+ raise VWSError.from_result_code(
136
+ result_code=result_code,
137
+ response=response,
138
+ )