vws-python 2026.2.15__tar.gz → 2026.2.21__tar.gz

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.
Files changed (58) hide show
  1. {vws_python-2026.2.15 → vws_python-2026.2.21}/CHANGELOG.rst +4 -0
  2. {vws_python-2026.2.15/src/vws_python.egg-info → vws_python-2026.2.21}/PKG-INFO +10 -8
  3. {vws_python-2026.2.15 → vws_python-2026.2.21}/conftest.py +3 -3
  4. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/api-reference.rst +4 -0
  5. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/index.rst +3 -3
  6. {vws_python-2026.2.15 → vws_python-2026.2.21}/pyproject.toml +25 -7
  7. {vws_python-2026.2.15 → vws_python-2026.2.21}/spelling_private_dict.txt +2 -0
  8. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/__init__.py +2 -0
  9. vws_python-2026.2.21/src/vws/_vws_request.py +85 -0
  10. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/vws_exceptions.py +44 -9
  11. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/query.py +1 -0
  12. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/response.py +1 -0
  13. vws_python-2026.2.21/src/vws/vumark_accept.py +18 -0
  14. vws_python-2026.2.21/src/vws/vumark_service.py +143 -0
  15. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/vws.py +7 -74
  16. {vws_python-2026.2.15 → vws_python-2026.2.21/src/vws_python.egg-info}/PKG-INFO +10 -8
  17. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/SOURCES.txt +3 -0
  18. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/requires.txt +9 -7
  19. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/conftest.py +37 -8
  20. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_cloud_reco_exceptions.py +5 -5
  21. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_query.py +7 -7
  22. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_vws.py +42 -14
  23. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_vws_exceptions.py +70 -6
  24. {vws_python-2026.2.15 → vws_python-2026.2.21}/.git_archival.txt +0 -0
  25. {vws_python-2026.2.15 → vws_python-2026.2.21}/.gitattributes +0 -0
  26. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/dependabot.yml +0 -0
  27. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/ci.yml +0 -0
  28. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/dependabot-merge.yml +0 -0
  29. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/lint.yml +0 -0
  30. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/publish-site.yml +0 -0
  31. {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/release.yml +0 -0
  32. {vws_python-2026.2.15 → vws_python-2026.2.21}/.gitignore +0 -0
  33. {vws_python-2026.2.15 → vws_python-2026.2.21}/.pre-commit-config.yaml +0 -0
  34. {vws_python-2026.2.15 → vws_python-2026.2.21}/.prettierrc +0 -0
  35. {vws_python-2026.2.15 → vws_python-2026.2.21}/.vscode/extensions.json +0 -0
  36. {vws_python-2026.2.15 → vws_python-2026.2.21}/.vscode/settings.json +0 -0
  37. {vws_python-2026.2.15 → vws_python-2026.2.21}/CODE_OF_CONDUCT.rst +0 -0
  38. {vws_python-2026.2.15 → vws_python-2026.2.21}/LICENSE +0 -0
  39. {vws_python-2026.2.15 → vws_python-2026.2.21}/README.rst +0 -0
  40. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/__init__.py +0 -0
  41. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/changelog.rst +0 -0
  42. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/conf.py +0 -0
  43. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/contributing.rst +0 -0
  44. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/exceptions.rst +0 -0
  45. {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/release-process.rst +0 -0
  46. {vws_python-2026.2.15 → vws_python-2026.2.21}/setup.cfg +0 -0
  47. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/__init__.py +0 -0
  48. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/base_exceptions.py +0 -0
  49. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
  50. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/custom_exceptions.py +0 -0
  51. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/include_target_data.py +0 -0
  52. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/py.typed +0 -0
  53. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/reports.py +0 -0
  54. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/dependency_links.txt +0 -0
  55. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/not-zip-safe +0 -0
  56. {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/top_level.txt +0 -0
  57. {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/__init__.py +0 -0
  58. {vws_python-2026.2.15 → vws_python-2026.2.21}/zizmor.yml +0 -0
@@ -4,6 +4,10 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.21
8
+ ----------
9
+
10
+
7
11
  2026.02.15
8
12
  ----------
9
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python
3
- Version: 2026.2.15
3
+ Version: 2026.2.21
4
4
  Summary: Interact with the Vuforia Web Services (VWS) API.
5
5
  Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
6
  License-Expression: MIT
@@ -22,24 +22,24 @@ Requires-Dist: requests>=2.32.3
22
22
  Requires-Dist: urllib3>=2.2.3
23
23
  Requires-Dist: vws-auth-tools>=2024.7.12
24
24
  Provides-Extra: dev
25
- Requires-Dist: actionlint-py==1.7.10.24; extra == "dev"
25
+ Requires-Dist: actionlint-py==1.7.11.24; extra == "dev"
26
26
  Requires-Dist: check-manifest==0.51; extra == "dev"
27
27
  Requires-Dist: deptry==0.24.0; extra == "dev"
28
28
  Requires-Dist: doc8==2.0.0; extra == "dev"
29
- Requires-Dist: doccmd==2026.1.31.3; extra == "dev"
29
+ Requires-Dist: doccmd==2026.2.15; extra == "dev"
30
30
  Requires-Dist: freezegun==1.5.5; extra == "dev"
31
31
  Requires-Dist: furo==2025.12.19; extra == "dev"
32
32
  Requires-Dist: interrogate==1.7.0; extra == "dev"
33
33
  Requires-Dist: mypy[faster-cache]==1.19.1; extra == "dev"
34
34
  Requires-Dist: mypy-strict-kwargs==2026.1.12; extra == "dev"
35
- Requires-Dist: prek==0.3.2; extra == "dev"
35
+ Requires-Dist: prek==0.3.3; extra == "dev"
36
36
  Requires-Dist: pydocstringformatter==0.7.5; extra == "dev"
37
37
  Requires-Dist: pydocstyle==6.3; extra == "dev"
38
38
  Requires-Dist: pygments==2.19.2; extra == "dev"
39
39
  Requires-Dist: pylint[spelling]==4.0.4; extra == "dev"
40
40
  Requires-Dist: pylint-per-file-ignores==3.2.0; extra == "dev"
41
- Requires-Dist: pyproject-fmt==2.16.0; extra == "dev"
42
- Requires-Dist: pyrefly==0.52.0; extra == "dev"
41
+ Requires-Dist: pyproject-fmt==2.16.1; extra == "dev"
42
+ Requires-Dist: pyrefly==0.53.0; extra == "dev"
43
43
  Requires-Dist: pyright==1.1.408; extra == "dev"
44
44
  Requires-Dist: pyroma==5.0.1; extra == "dev"
45
45
  Requires-Dist: pytest==9.0.2; extra == "dev"
@@ -55,10 +55,12 @@ Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
55
55
  Requires-Dist: sphinx-substitution-extensions==2026.1.12; extra == "dev"
56
56
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
57
57
  Requires-Dist: sybil==9.3.0; extra == "dev"
58
- Requires-Dist: ty==0.0.16; extra == "dev"
58
+ Requires-Dist: torch>=2.5.1; extra == "dev"
59
+ Requires-Dist: torchvision>=0.20.1; extra == "dev"
60
+ Requires-Dist: ty==0.0.17; extra == "dev"
59
61
  Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
60
62
  Requires-Dist: vulture==2.14; extra == "dev"
61
- Requires-Dist: vws-python-mock==2026.2.15.4; extra == "dev"
63
+ Requires-Dist: vws-python-mock==2026.2.21; extra == "dev"
62
64
  Requires-Dist: vws-test-fixtures==2023.3.5; extra == "dev"
63
65
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
64
66
  Requires-Dist: zizmor==1.22.0; extra == "dev"
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import pytest
10
10
  from beartype import beartype
11
11
  from mock_vws import MockVWS
12
- from mock_vws.database import VuforiaDatabase
12
+ from mock_vws.database import CloudDatabase
13
13
  from sybil import Sybil
14
14
  from sybil.parsers.rest import (
15
15
  ClearNamespaceParser,
@@ -53,7 +53,7 @@ def fixture_mock_vws(
53
53
  client_access_key = uuid.uuid4().hex
54
54
  client_secret_key = uuid.uuid4().hex
55
55
 
56
- database = VuforiaDatabase(
56
+ database = CloudDatabase(
57
57
  server_access_key=server_access_key,
58
58
  server_secret_key=server_secret_key,
59
59
  client_access_key=client_access_key,
@@ -66,7 +66,7 @@ def fixture_mock_vws(
66
66
  monkeypatch.setenv(name="VWS_CLIENT_SECRET_KEY", value=client_secret_key)
67
67
  # We use a low processing time so that tests run quickly.
68
68
  with MockVWS(processing_time_seconds=0.2) as mock:
69
- mock.add_database(database=database)
69
+ mock.add_cloud_database(cloud_database=database)
70
70
  yield
71
71
 
72
72
 
@@ -13,6 +13,10 @@ API Reference
13
13
  :undoc-members:
14
14
  :members:
15
15
 
16
+ .. automodule:: vws.vumark_accept
17
+ :undoc-members:
18
+ :members:
19
+
16
20
  .. automodule:: vws.response
17
21
  :undoc-members:
18
22
  :members:
@@ -78,13 +78,13 @@ To write unit tests for code which uses this library, without using your Vuforia
78
78
  import pathlib
79
79
 
80
80
  from mock_vws import MockVWS
81
- from mock_vws.database import VuforiaDatabase
81
+ from mock_vws.database import CloudDatabase
82
82
 
83
83
  from vws import VWS, CloudRecoService
84
84
 
85
85
  with MockVWS() as mock:
86
- database = VuforiaDatabase()
87
- mock.add_database(database=database)
86
+ database = CloudDatabase()
87
+ mock.add_cloud_database(cloud_database=database)
88
88
  vws_client = VWS(
89
89
  server_access_key=database.server_access_key,
90
90
  server_secret_key=database.server_secret_key,
@@ -38,24 +38,24 @@ dependencies = [
38
38
  "vws-auth-tools>=2024.7.12",
39
39
  ]
40
40
  optional-dependencies.dev = [
41
- "actionlint-py==1.7.10.24",
41
+ "actionlint-py==1.7.11.24",
42
42
  "check-manifest==0.51",
43
43
  "deptry==0.24.0",
44
44
  "doc8==2.0.0",
45
- "doccmd==2026.1.31.3",
45
+ "doccmd==2026.2.15",
46
46
  "freezegun==1.5.5",
47
47
  "furo==2025.12.19",
48
48
  "interrogate==1.7.0",
49
49
  "mypy[faster-cache]==1.19.1",
50
50
  "mypy-strict-kwargs==2026.1.12",
51
- "prek==0.3.2",
51
+ "prek==0.3.3",
52
52
  "pydocstringformatter==0.7.5",
53
53
  "pydocstyle==6.3",
54
54
  "pygments==2.19.2",
55
55
  "pylint[spelling]==4.0.4",
56
56
  "pylint-per-file-ignores==3.2.0",
57
- "pyproject-fmt==2.16.0",
58
- "pyrefly==0.52.0",
57
+ "pyproject-fmt==2.16.1",
58
+ "pyrefly==0.53.0",
59
59
  "pyright==1.1.408",
60
60
  "pyroma==5.0.1",
61
61
  "pytest==9.0.2",
@@ -74,10 +74,15 @@ optional-dependencies.dev = [
74
74
  "sphinx-substitution-extensions==2026.1.12",
75
75
  "sphinxcontrib-spelling==8.0.2",
76
76
  "sybil==9.3.0",
77
- "ty==0.0.16",
77
+ # Listed explicitly (despite being transitive via vws-python-mock) so that
78
+ # [tool.uv.sources] can redirect to the CPU-only PyTorch index.
79
+ # See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
80
+ "torch>=2.5.1",
81
+ "torchvision>=0.20.1",
82
+ "ty==0.0.17",
78
83
  "types-requests==2.32.4.20260107",
79
84
  "vulture==2.14",
80
- "vws-python-mock==2026.2.15.4",
85
+ "vws-python-mock==2026.2.21",
81
86
  "vws-test-fixtures==2023.3.5",
82
87
  "yamlfix==1.19.1",
83
88
  "zizmor==1.22.0",
@@ -106,6 +111,11 @@ bdist_wheel.universal = true
106
111
  # Code to match this is in ``conf.py``.
107
112
  version_scheme = "post-release"
108
113
 
114
+ [tool.uv]
115
+ sources.torch = { index = "pytorch-cpu" }
116
+ sources.torchvision = { index = "pytorch-cpu" }
117
+ index = [ { name = "pytorch-cpu", url = "https://download.pytorch.org/whl/cpu", explicit = true } ]
118
+
109
119
  [tool.ruff]
110
120
  line-length = 79
111
121
  lint.select = [
@@ -277,6 +287,13 @@ pep621_dev_dependency_groups = [
277
287
  "dev",
278
288
  "release",
279
289
  ]
290
+ # torch and torchvision are listed explicitly in dev deps to allow
291
+ # [tool.uv.sources] to redirect them to the CPU-only PyTorch index,
292
+ # but they are not directly imported in the vws-python source code.
293
+ per_rule_ignores.DEP002 = [
294
+ "torch",
295
+ "torchvision",
296
+ ]
280
297
 
281
298
  [tool.pyproject-fmt]
282
299
  indent = 4
@@ -292,6 +309,7 @@ run.branch = true
292
309
  report.exclude_also = [
293
310
  "if TYPE_CHECKING:",
294
311
  ]
312
+ report.show_missing = true
295
313
 
296
314
  [tool.mypy]
297
315
  strict = true
@@ -42,6 +42,7 @@ decodable
42
42
  dev
43
43
  dict
44
44
  docstring
45
+ enum
45
46
  filename
46
47
  foo
47
48
  formdata
@@ -100,6 +101,7 @@ usefixtures
100
101
  validators
101
102
  vuforia
102
103
  vuforia's
104
+ vumark
103
105
  vwq
104
106
  vws
105
107
  xxx
@@ -1,9 +1,11 @@
1
1
  """A library for Vuforia Web Services."""
2
2
 
3
3
  from .query import CloudRecoService
4
+ from .vumark_service import VuMarkService
4
5
  from .vws import VWS
5
6
 
6
7
  __all__ = [
7
8
  "VWS",
8
9
  "CloudRecoService",
10
+ "VuMarkService",
9
11
  ]
@@ -0,0 +1,85 @@
1
+ """Internal helper for making authenticated requests to the Vuforia Target
2
+ API.
3
+ """
4
+
5
+ from urllib.parse import urljoin
6
+
7
+ import requests
8
+ from beartype import BeartypeConf, beartype
9
+ from vws_auth_tools import authorization_header, rfc_1123_date
10
+
11
+ from vws.response import Response
12
+
13
+
14
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
15
+ def target_api_request(
16
+ *,
17
+ content_type: str,
18
+ server_access_key: str,
19
+ server_secret_key: str,
20
+ method: str,
21
+ data: bytes,
22
+ request_path: str,
23
+ base_vws_url: str,
24
+ request_timeout_seconds: float | tuple[float, float],
25
+ extra_headers: dict[str, str],
26
+ ) -> Response:
27
+ """Make a request to the Vuforia Target API.
28
+
29
+ This uses `requests` to make a request against https://vws.vuforia.com.
30
+
31
+ Args:
32
+ content_type: The content type of the request.
33
+ server_access_key: A VWS server access key.
34
+ server_secret_key: A VWS server secret key.
35
+ method: The HTTP method which will be used in the request.
36
+ data: The request body which will be used in the request.
37
+ request_path: The path to the endpoint which will be used in the
38
+ request.
39
+ base_vws_url: The base URL for the VWS API.
40
+ request_timeout_seconds: The timeout for the request, as used by
41
+ ``requests.request``. This can be a float to set both the
42
+ connect and read timeouts, or a (connect, read) tuple.
43
+ extra_headers: Additional headers to include in the request.
44
+
45
+ Returns:
46
+ The response to the request made by `requests`.
47
+ """
48
+ date_string = rfc_1123_date()
49
+
50
+ signature_string = authorization_header(
51
+ access_key=server_access_key,
52
+ secret_key=server_secret_key,
53
+ method=method,
54
+ content=data,
55
+ content_type=content_type,
56
+ date=date_string,
57
+ request_path=request_path,
58
+ )
59
+
60
+ headers = {
61
+ "Authorization": signature_string,
62
+ "Date": date_string,
63
+ "Content-Type": content_type,
64
+ **extra_headers,
65
+ }
66
+
67
+ url = urljoin(base=base_vws_url, url=request_path)
68
+
69
+ requests_response = requests.request(
70
+ method=method,
71
+ url=url,
72
+ headers=headers,
73
+ data=data,
74
+ timeout=request_timeout_seconds,
75
+ )
76
+
77
+ return Response(
78
+ text=requests_response.text,
79
+ url=requests_response.url,
80
+ status_code=requests_response.status_code,
81
+ headers=dict(requests_response.headers),
82
+ request_body=requests_response.request.body,
83
+ tell_position=requests_response.raw.tell(),
84
+ content=bytes(requests_response.content),
85
+ )
@@ -24,9 +24,10 @@ class UnknownTargetError(VWSError):
24
24
  def target_id(self) -> str:
25
25
  """The unknown target ID."""
26
26
  path = urlparse(url=self.response.url).path
27
- # Every HTTP path which can raise this error is in the format
28
- # `/something/{target_id}`.
29
- return path.split(sep="/", maxsplit=2)[-1]
27
+ # Every HTTP path which can raise this error has the target ID as the
28
+ # second path segment, e.g. `/something/{target_id}` or
29
+ # `/something/{target_id}/more`.
30
+ return path.split(sep="/")[2]
30
31
 
31
32
 
32
33
  @beartype
@@ -68,9 +69,10 @@ class TargetStatusProcessingError(VWSError):
68
69
  def target_id(self) -> str:
69
70
  """The processing target ID."""
70
71
  path = urlparse(url=self.response.url).path
71
- # Every HTTP path which can raise this error is in the format
72
- # `/something/{target_id}`.
73
- return path.split(sep="/", maxsplit=2)[-1]
72
+ # Every HTTP path which can raise this error has the target ID as the
73
+ # second path segment, e.g. `/something/{target_id}` or
74
+ # `/something/{target_id}/more`.
75
+ return path.split(sep="/")[2]
74
76
 
75
77
 
76
78
  # This is not simulated by the mock.
@@ -157,9 +159,10 @@ class TargetStatusNotSuccessError(VWSError):
157
159
  def target_id(self) -> str:
158
160
  """The unknown target ID."""
159
161
  path = urlparse(url=self.response.url).path
160
- # Every HTTP path which can raise this error is in the format
161
- # `/something/{target_id}`.
162
- return path.split(sep="/", maxsplit=2)[-1]
162
+ # Every HTTP path which can raise this error has the target ID as the
163
+ # second path segment, e.g. `/something/{target_id}` or
164
+ # `/something/{target_id}/more`.
165
+ return path.split(sep="/")[2]
163
166
 
164
167
 
165
168
  @beartype
@@ -167,3 +170,35 @@ class TooManyRequestsError(VWSError): # pragma: no cover
167
170
  """Exception raised when Vuforia returns a response with a result code
168
171
  'TooManyRequests'.
169
172
  """
173
+
174
+
175
+ # This is not simulated by client code because the accept parameter uses
176
+ # the VuMarkAccept enum, which only allows valid values.
177
+ @beartype
178
+ class InvalidAcceptHeaderError(VWSError): # pragma: no cover
179
+ """Exception raised when Vuforia returns a response with a result code
180
+ ``InvalidAcceptHeader``.
181
+ """
182
+
183
+
184
+ @beartype
185
+ class InvalidInstanceIdError(VWSError):
186
+ """Exception raised when Vuforia returns a response with a result code
187
+ ``InvalidInstanceId``.
188
+ """
189
+
190
+
191
+ # This is not simulated by client code because the request body
192
+ # is always valid JSON when using this client.
193
+ @beartype
194
+ class BadRequestError(VWSError): # pragma: no cover
195
+ """Exception raised when Vuforia returns a response with a result code
196
+ ``BadRequest``.
197
+ """
198
+
199
+
200
+ @beartype
201
+ class InvalidTargetTypeError(VWSError):
202
+ """Exception raised when Vuforia returns a response with a result code
203
+ ``InvalidTargetType``.
204
+ """
@@ -156,6 +156,7 @@ class CloudRecoService:
156
156
  headers=dict(requests_response.headers),
157
157
  request_body=requests_response.request.body,
158
158
  tell_position=requests_response.raw.tell(),
159
+ content=bytes(requests_response.content),
159
160
  )
160
161
 
161
162
  if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
@@ -16,3 +16,4 @@ class Response:
16
16
  headers: dict[str, str]
17
17
  request_body: bytes | str | None
18
18
  tell_position: int
19
+ content: bytes
@@ -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"
@@ -0,0 +1,143 @@
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.custom_exceptions import ServerError
10
+ from vws.exceptions.vws_exceptions import (
11
+ AuthenticationFailureError,
12
+ BadRequestError,
13
+ DateRangeError,
14
+ FailError,
15
+ InvalidAcceptHeaderError,
16
+ InvalidInstanceIdError,
17
+ InvalidTargetTypeError,
18
+ RequestTimeTooSkewedError,
19
+ TargetStatusNotSuccessError,
20
+ TooManyRequestsError,
21
+ UnknownTargetError,
22
+ )
23
+ from vws.vumark_accept import VuMarkAccept
24
+
25
+
26
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
27
+ class VuMarkService:
28
+ """An interface to the Vuforia VuMark Generation Web API."""
29
+
30
+ def __init__(
31
+ self,
32
+ server_access_key: str,
33
+ server_secret_key: str,
34
+ base_vws_url: str = "https://vws.vuforia.com",
35
+ request_timeout_seconds: float | tuple[float, float] = 30.0,
36
+ ) -> None:
37
+ """
38
+ Args:
39
+ server_access_key: A VWS server access key.
40
+ server_secret_key: A VWS server secret key.
41
+ base_vws_url: The base URL for the VWS API.
42
+ request_timeout_seconds: The timeout for each HTTP request, as
43
+ used by ``requests.request``. This can be a float to set
44
+ both the connect and read timeouts, or a (connect, read)
45
+ tuple.
46
+ """
47
+ self._server_access_key = server_access_key
48
+ self._server_secret_key = server_secret_key
49
+ self._base_vws_url = base_vws_url
50
+ self._request_timeout_seconds = request_timeout_seconds
51
+
52
+ def generate_vumark_instance(
53
+ self,
54
+ *,
55
+ target_id: str,
56
+ instance_id: str,
57
+ accept: VuMarkAccept,
58
+ ) -> bytes:
59
+ """Generate a VuMark instance image.
60
+
61
+ See
62
+ https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/
63
+ for parameter details.
64
+
65
+ Args:
66
+ target_id: The ID of the VuMark target.
67
+ instance_id: The instance ID to encode in the VuMark.
68
+ accept: The image format to return.
69
+
70
+ Returns:
71
+ The VuMark instance image bytes.
72
+
73
+ Raises:
74
+ ~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
75
+ secret key is not correct.
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.RequestTimeTooSkewedError: There is
86
+ an error with the time sent to Vuforia.
87
+ ~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The
88
+ target is not in the success state.
89
+ ~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
90
+ ID does not match a target in the database.
91
+ ~vws.exceptions.custom_exceptions.ServerError: There is an error
92
+ with Vuforia's servers.
93
+ ~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
94
+ rate limiting access.
95
+ """
96
+ request_path = f"/targets/{target_id}/instances"
97
+ content_type = "application/json"
98
+ request_data = json.dumps(obj={"instance_id": instance_id}).encode(
99
+ encoding="utf-8",
100
+ )
101
+
102
+ response = target_api_request(
103
+ content_type=content_type,
104
+ server_access_key=self._server_access_key,
105
+ server_secret_key=self._server_secret_key,
106
+ method=HTTPMethod.POST,
107
+ data=request_data,
108
+ request_path=request_path,
109
+ base_vws_url=self._base_vws_url,
110
+ request_timeout_seconds=self._request_timeout_seconds,
111
+ extra_headers={"Accept": accept},
112
+ )
113
+
114
+ if (
115
+ response.status_code == HTTPStatus.TOO_MANY_REQUESTS
116
+ ): # pragma: no cover
117
+ # The Vuforia API returns a 429 response with no JSON body.
118
+ raise TooManyRequestsError(response=response)
119
+
120
+ if (
121
+ response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
122
+ ): # pragma: no cover
123
+ raise ServerError(response=response)
124
+
125
+ if response.status_code == HTTPStatus.OK:
126
+ return response.content
127
+
128
+ result_code = json.loads(s=response.text)["result_code"]
129
+
130
+ exception = {
131
+ "AuthenticationFailure": AuthenticationFailureError,
132
+ "BadRequest": BadRequestError,
133
+ "DateRangeError": DateRangeError,
134
+ "Fail": FailError,
135
+ "InvalidAcceptHeader": InvalidAcceptHeaderError,
136
+ "InvalidInstanceId": InvalidInstanceIdError,
137
+ "InvalidTargetType": InvalidTargetTypeError,
138
+ "RequestTimeTooSkewed": RequestTimeTooSkewedError,
139
+ "TargetStatusNotSuccess": TargetStatusNotSuccessError,
140
+ "UnknownTarget": UnknownTargetError,
141
+ }[result_code]
142
+
143
+ raise exception(response=response)