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.
- {vws_python-2026.2.15 → vws_python-2026.2.21}/CHANGELOG.rst +4 -0
- {vws_python-2026.2.15/src/vws_python.egg-info → vws_python-2026.2.21}/PKG-INFO +10 -8
- {vws_python-2026.2.15 → vws_python-2026.2.21}/conftest.py +3 -3
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/api-reference.rst +4 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/index.rst +3 -3
- {vws_python-2026.2.15 → vws_python-2026.2.21}/pyproject.toml +25 -7
- {vws_python-2026.2.15 → vws_python-2026.2.21}/spelling_private_dict.txt +2 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/__init__.py +2 -0
- vws_python-2026.2.21/src/vws/_vws_request.py +85 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/vws_exceptions.py +44 -9
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/query.py +1 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/response.py +1 -0
- vws_python-2026.2.21/src/vws/vumark_accept.py +18 -0
- vws_python-2026.2.21/src/vws/vumark_service.py +143 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/vws.py +7 -74
- {vws_python-2026.2.15 → vws_python-2026.2.21/src/vws_python.egg-info}/PKG-INFO +10 -8
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/SOURCES.txt +3 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/requires.txt +9 -7
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/conftest.py +37 -8
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_cloud_reco_exceptions.py +5 -5
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_query.py +7 -7
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_vws.py +42 -14
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/test_vws_exceptions.py +70 -6
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.git_archival.txt +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.gitattributes +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/dependabot.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/ci.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/dependabot-merge.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/lint.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/publish-site.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.github/workflows/release.yml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.gitignore +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.pre-commit-config.yaml +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.prettierrc +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.vscode/extensions.json +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/.vscode/settings.json +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/CODE_OF_CONDUCT.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/LICENSE +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/README.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/__init__.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/changelog.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/conf.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/contributing.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/exceptions.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/docs/source/release-process.rst +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/setup.cfg +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/__init__.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/base_exceptions.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/exceptions/custom_exceptions.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/include_target_data.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/py.typed +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws/reports.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/dependency_links.txt +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/not-zip-safe +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/src/vws_python.egg-info/top_level.txt +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/tests/__init__.py +0 -0
- {vws_python-2026.2.15 → vws_python-2026.2.21}/zizmor.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vws-python
|
|
3
|
-
Version: 2026.2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
-
Requires-Dist: pyrefly==0.
|
|
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:
|
|
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.
|
|
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
|
|
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 =
|
|
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.
|
|
69
|
+
mock.add_cloud_database(cloud_database=database)
|
|
70
70
|
yield
|
|
71
71
|
|
|
72
72
|
|
|
@@ -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
|
|
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 =
|
|
87
|
-
mock.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
-
"pyrefly==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
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
|
28
|
-
# `/something/{target_id}
|
|
29
|
-
|
|
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
|
|
72
|
-
# `/something/{target_id}
|
|
73
|
-
|
|
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
|
|
161
|
-
# `/something/{target_id}
|
|
162
|
-
|
|
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:
|
|
@@ -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)
|