vws-python 2026.2.23__tar.gz → 2026.2.25__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.23 → vws_python-2026.2.25}/CHANGELOG.rst +8 -0
- {vws_python-2026.2.23/src/vws_python.egg-info → vws_python-2026.2.25}/PKG-INFO +9 -7
- {vws_python-2026.2.23 → vws_python-2026.2.25}/conftest.py +2 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/api-reference.rst +16 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/pyproject.toml +11 -6
- {vws_python-2026.2.23 → vws_python-2026.2.25}/spelling_private_dict.txt +3 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/__init__.py +6 -0
- vws_python-2026.2.25/src/vws/_async_vws_request.py +77 -0
- vws_python-2026.2.25/src/vws/_image_utils.py +18 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/_vws_request.py +16 -23
- vws_python-2026.2.25/src/vws/async_query.py +226 -0
- vws_python-2026.2.25/src/vws/async_vumark_service.py +170 -0
- vws_python-2026.2.25/src/vws/async_vws.py +692 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/query.py +15 -31
- vws_python-2026.2.25/src/vws/transports.py +278 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vumark_service.py +12 -4
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vws.py +30 -33
- {vws_python-2026.2.23 → vws_python-2026.2.25/src/vws_python.egg-info}/PKG-INFO +9 -7
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/SOURCES.txt +11 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/requires.txt +8 -6
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/conftest.py +59 -5
- vws_python-2026.2.25/tests/test_async_cloud_reco_exceptions.py +120 -0
- vws_python-2026.2.25/tests/test_async_query.py +225 -0
- vws_python-2026.2.25/tests/test_async_vws.py +500 -0
- vws_python-2026.2.25/tests/test_async_vws_exceptions.py +304 -0
- vws_python-2026.2.25/tests/test_transports.py +206 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.git_archival.txt +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.gitattributes +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/dependabot.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/ci.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/dependabot-merge.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/lint.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/publish-site.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.github/workflows/release.yml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.gitignore +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.pre-commit-config.yaml +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.prettierrc +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.vscode/extensions.json +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/.vscode/settings.json +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/CODE_OF_CONDUCT.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/LICENSE +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/README.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/__init__.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/changelog.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/conf.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/contributing.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/exceptions.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/index.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/docs/source/release-process.rst +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/setup.cfg +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/__init__.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/base_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/cloud_reco_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/custom_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/exceptions/vws_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/include_target_data.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/py.typed +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/reports.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/response.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws/vumark_accept.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/dependency_links.txt +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/not-zip-safe +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/src/vws_python.egg-info/top_level.txt +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/__init__.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_cloud_reco_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_query.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_vws.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/tests/test_vws_exceptions.py +0 -0
- {vws_python-2026.2.23 → vws_python-2026.2.25}/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.25
|
|
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
|
|
@@ -18,6 +18,7 @@ Requires-Python: >=3.13
|
|
|
18
18
|
Description-Content-Type: text/x-rst
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
Requires-Dist: beartype>=0.22.9
|
|
21
|
+
Requires-Dist: httpx>=0.28.0
|
|
21
22
|
Requires-Dist: requests>=2.32.3
|
|
22
23
|
Requires-Dist: urllib3>=2.2.3
|
|
23
24
|
Requires-Dist: vws-auth-tools>=2024.7.12
|
|
@@ -36,16 +37,17 @@ Requires-Dist: prek==0.3.3; extra == "dev"
|
|
|
36
37
|
Requires-Dist: pydocstringformatter==0.7.5; extra == "dev"
|
|
37
38
|
Requires-Dist: pydocstyle==6.3; extra == "dev"
|
|
38
39
|
Requires-Dist: pygments==2.19.2; extra == "dev"
|
|
39
|
-
Requires-Dist: pylint[spelling]==4.0.
|
|
40
|
+
Requires-Dist: pylint[spelling]==4.0.5; extra == "dev"
|
|
40
41
|
Requires-Dist: pylint-per-file-ignores==3.2.0; extra == "dev"
|
|
41
|
-
Requires-Dist: pyproject-fmt==2.16.
|
|
42
|
-
Requires-Dist: pyrefly==0.
|
|
42
|
+
Requires-Dist: pyproject-fmt==2.16.2; extra == "dev"
|
|
43
|
+
Requires-Dist: pyrefly==0.54.0; extra == "dev"
|
|
43
44
|
Requires-Dist: pyright==1.1.408; extra == "dev"
|
|
44
45
|
Requires-Dist: pyroma==5.0.1; extra == "dev"
|
|
45
46
|
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
46
48
|
Requires-Dist: pytest-cov==7.0.0; extra == "dev"
|
|
47
49
|
Requires-Dist: pyyaml==6.0.3; extra == "dev"
|
|
48
|
-
Requires-Dist: ruff==0.15.
|
|
50
|
+
Requires-Dist: ruff==0.15.2; extra == "dev"
|
|
49
51
|
Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
|
|
50
52
|
Requires-Dist: shfmt-py==3.12.0.2; extra == "dev"
|
|
51
53
|
Requires-Dist: sphinx==9.1.0; extra == "dev"
|
|
@@ -57,10 +59,10 @@ Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
|
|
|
57
59
|
Requires-Dist: sybil==9.3.0; extra == "dev"
|
|
58
60
|
Requires-Dist: torch>=2.5.1; extra == "dev"
|
|
59
61
|
Requires-Dist: torchvision>=0.20.1; extra == "dev"
|
|
60
|
-
Requires-Dist: ty==0.0.
|
|
62
|
+
Requires-Dist: ty==0.0.18; extra == "dev"
|
|
61
63
|
Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
|
|
62
64
|
Requires-Dist: vulture==2.14; extra == "dev"
|
|
63
|
-
Requires-Dist: vws-python-mock==2026.2.22.
|
|
65
|
+
Requires-Dist: vws-python-mock==2026.2.22.3; extra == "dev"
|
|
64
66
|
Requires-Dist: vws-test-fixtures==2023.3.5; extra == "dev"
|
|
65
67
|
Requires-Dist: yamlfix==1.19.1; extra == "dev"
|
|
66
68
|
Requires-Dist: zizmor==1.22.0; extra == "dev"
|
|
@@ -28,6 +28,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
|
|
28
28
|
|
|
29
29
|
@pytest.fixture(name="make_image_file")
|
|
30
30
|
def fixture_make_image_file(
|
|
31
|
+
*,
|
|
31
32
|
high_quality_image: io.BytesIO,
|
|
32
33
|
) -> Generator[None]:
|
|
33
34
|
"""Make an image file available in the test directory.
|
|
@@ -43,6 +44,7 @@ def fixture_make_image_file(
|
|
|
43
44
|
|
|
44
45
|
@pytest.fixture(name="mock_vws")
|
|
45
46
|
def fixture_mock_vws(
|
|
47
|
+
*,
|
|
46
48
|
monkeypatch: pytest.MonkeyPatch,
|
|
47
49
|
) -> Generator[None]:
|
|
48
50
|
"""Yield a mock VWS.
|
|
@@ -5,6 +5,18 @@ API Reference
|
|
|
5
5
|
:undoc-members:
|
|
6
6
|
:members:
|
|
7
7
|
|
|
8
|
+
.. automodule:: vws.async_vws
|
|
9
|
+
:undoc-members:
|
|
10
|
+
:members:
|
|
11
|
+
|
|
12
|
+
.. automodule:: vws.async_query
|
|
13
|
+
:undoc-members:
|
|
14
|
+
:members:
|
|
15
|
+
|
|
16
|
+
.. automodule:: vws.async_vumark_service
|
|
17
|
+
:undoc-members:
|
|
18
|
+
:members:
|
|
19
|
+
|
|
8
20
|
.. automodule:: vws.reports
|
|
9
21
|
:undoc-members:
|
|
10
22
|
:members:
|
|
@@ -20,3 +32,7 @@ API Reference
|
|
|
20
32
|
.. automodule:: vws.response
|
|
21
33
|
:undoc-members:
|
|
22
34
|
:members:
|
|
35
|
+
|
|
36
|
+
.. automodule:: vws.transports
|
|
37
|
+
:undoc-members:
|
|
38
|
+
:members:
|
|
@@ -33,6 +33,7 @@ dynamic = [
|
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
35
|
"beartype>=0.22.9",
|
|
36
|
+
"httpx>=0.28.0",
|
|
36
37
|
"requests>=2.32.3",
|
|
37
38
|
"urllib3>=2.2.3",
|
|
38
39
|
"vws-auth-tools>=2024.7.12",
|
|
@@ -52,16 +53,17 @@ optional-dependencies.dev = [
|
|
|
52
53
|
"pydocstringformatter==0.7.5",
|
|
53
54
|
"pydocstyle==6.3",
|
|
54
55
|
"pygments==2.19.2",
|
|
55
|
-
"pylint[spelling]==4.0.
|
|
56
|
+
"pylint[spelling]==4.0.5",
|
|
56
57
|
"pylint-per-file-ignores==3.2.0",
|
|
57
|
-
"pyproject-fmt==2.16.
|
|
58
|
-
"pyrefly==0.
|
|
58
|
+
"pyproject-fmt==2.16.2",
|
|
59
|
+
"pyrefly==0.54.0",
|
|
59
60
|
"pyright==1.1.408",
|
|
60
61
|
"pyroma==5.0.1",
|
|
61
62
|
"pytest==9.0.2",
|
|
63
|
+
"pytest-asyncio==1.3.0",
|
|
62
64
|
"pytest-cov==7.0.0",
|
|
63
65
|
"pyyaml==6.0.3",
|
|
64
|
-
"ruff==0.15.
|
|
66
|
+
"ruff==0.15.2",
|
|
65
67
|
# We add shellcheck-py not only for shell scripts and shell code blocks,
|
|
66
68
|
# but also because having it installed means that ``actionlint-py`` will
|
|
67
69
|
# use it to lint shell commands in GitHub workflow files.
|
|
@@ -79,10 +81,10 @@ optional-dependencies.dev = [
|
|
|
79
81
|
# See: https://vws-python.github.io/vws-python-mock/installation.html#faster-installation
|
|
80
82
|
"torch>=2.5.1",
|
|
81
83
|
"torchvision>=0.20.1",
|
|
82
|
-
"ty==0.0.
|
|
84
|
+
"ty==0.0.18",
|
|
83
85
|
"types-requests==2.32.4.20260107",
|
|
84
86
|
"vulture==2.14",
|
|
85
|
-
"vws-python-mock==2026.2.22.
|
|
87
|
+
"vws-python-mock==2026.2.22.3",
|
|
86
88
|
"vws-test-fixtures==2023.3.5",
|
|
87
89
|
"yamlfix==1.19.1",
|
|
88
90
|
"zizmor==1.22.0",
|
|
@@ -357,6 +359,9 @@ ignore_path = [
|
|
|
357
359
|
# Ideally we would limit the paths to the source code where we want to ignore names,
|
|
358
360
|
# but Vulture does not enable this.
|
|
359
361
|
ignore_names = [
|
|
362
|
+
# Public API classes imported by users from vws.transports
|
|
363
|
+
"AsyncHTTPXTransport",
|
|
364
|
+
"HTTPXTransport",
|
|
360
365
|
# pytest configuration
|
|
361
366
|
"pytest_collect_file",
|
|
362
367
|
"pytest_collection_modifyitems",
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""A library for Vuforia Web Services."""
|
|
2
2
|
|
|
3
|
+
from .async_query import AsyncCloudRecoService
|
|
4
|
+
from .async_vumark_service import AsyncVuMarkService
|
|
5
|
+
from .async_vws import AsyncVWS
|
|
3
6
|
from .query import CloudRecoService
|
|
4
7
|
from .vumark_service import VuMarkService
|
|
5
8
|
from .vws import VWS
|
|
6
9
|
|
|
7
10
|
__all__ = [
|
|
8
11
|
"VWS",
|
|
12
|
+
"AsyncCloudRecoService",
|
|
13
|
+
"AsyncVWS",
|
|
14
|
+
"AsyncVuMarkService",
|
|
9
15
|
"CloudRecoService",
|
|
10
16
|
"VuMarkService",
|
|
11
17
|
]
|
|
@@ -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
|
|
9
|
+
from vws.transports import AsyncTransport
|
|
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
|
+
)
|
|
@@ -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
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
API.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import requests
|
|
6
5
|
from beartype import BeartypeConf, beartype
|
|
7
6
|
from vws_auth_tools import authorization_header, rfc_1123_date
|
|
8
7
|
|
|
9
8
|
from vws.response import Response
|
|
9
|
+
from vws.transports import Transport
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
@@ -21,27 +21,30 @@ def target_api_request(
|
|
|
21
21
|
base_vws_url: str,
|
|
22
22
|
request_timeout_seconds: float | tuple[float, float],
|
|
23
23
|
extra_headers: dict[str, str],
|
|
24
|
+
transport: Transport,
|
|
24
25
|
) -> Response:
|
|
25
26
|
"""Make a request to the Vuforia Target API.
|
|
26
27
|
|
|
27
|
-
This uses `requests` to make a request against https://vws.vuforia.com.
|
|
28
|
-
|
|
29
28
|
Args:
|
|
30
29
|
content_type: The content type of the request.
|
|
31
30
|
server_access_key: A VWS server access key.
|
|
32
31
|
server_secret_key: A VWS server secret key.
|
|
33
|
-
method: The HTTP method which will be used in the
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
method: The HTTP method which will be used in the
|
|
33
|
+
request.
|
|
34
|
+
data: The request body which will be used in the
|
|
36
35
|
request.
|
|
36
|
+
request_path: The path to the endpoint which will be
|
|
37
|
+
used in the request.
|
|
37
38
|
base_vws_url: The base URL for the VWS API.
|
|
38
|
-
request_timeout_seconds: The timeout for the request
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
extra_headers: Additional headers to include in the
|
|
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.
|
|
42
45
|
|
|
43
46
|
Returns:
|
|
44
|
-
The response to the request
|
|
47
|
+
The response to the request.
|
|
45
48
|
"""
|
|
46
49
|
date_string = rfc_1123_date()
|
|
47
50
|
|
|
@@ -64,20 +67,10 @@ def target_api_request(
|
|
|
64
67
|
|
|
65
68
|
url = base_vws_url.rstrip("/") + request_path
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
return transport(
|
|
68
71
|
method=method,
|
|
69
72
|
url=url,
|
|
70
73
|
headers=headers,
|
|
71
74
|
data=data,
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
return Response(
|
|
76
|
-
text=requests_response.text,
|
|
77
|
-
url=requests_response.url,
|
|
78
|
-
status_code=requests_response.status_code,
|
|
79
|
-
headers=dict(requests_response.headers),
|
|
80
|
-
request_body=requests_response.request.body,
|
|
81
|
-
tell_position=requests_response.raw.tell(),
|
|
82
|
-
content=bytes(requests_response.content),
|
|
75
|
+
request_timeout=request_timeout_seconds,
|
|
83
76
|
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Async tools for interacting with the Vuforia Cloud Recognition
|
|
2
|
+
Web APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
from http import HTTPMethod, HTTPStatus
|
|
8
|
+
from typing import Any, Self
|
|
9
|
+
|
|
10
|
+
from beartype import BeartypeConf, beartype
|
|
11
|
+
from urllib3.filepost import encode_multipart_formdata
|
|
12
|
+
from vws_auth_tools import authorization_header, rfc_1123_date
|
|
13
|
+
|
|
14
|
+
from vws._image_utils import ImageType as _ImageType
|
|
15
|
+
from vws._image_utils import get_image_data as _get_image_data
|
|
16
|
+
from vws.exceptions.cloud_reco_exceptions import (
|
|
17
|
+
AuthenticationFailureError,
|
|
18
|
+
BadImageError,
|
|
19
|
+
InactiveProjectError,
|
|
20
|
+
MaxNumResultsOutOfRangeError,
|
|
21
|
+
RequestTimeTooSkewedError,
|
|
22
|
+
)
|
|
23
|
+
from vws.exceptions.custom_exceptions import (
|
|
24
|
+
RequestEntityTooLargeError,
|
|
25
|
+
ServerError,
|
|
26
|
+
)
|
|
27
|
+
from vws.include_target_data import CloudRecoIncludeTargetData
|
|
28
|
+
from vws.reports import QueryResult, TargetData
|
|
29
|
+
from vws.transports import AsyncHTTPXTransport, AsyncTransport
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
|
|
33
|
+
class AsyncCloudRecoService:
|
|
34
|
+
"""An async interface to the Vuforia Cloud Recognition Web
|
|
35
|
+
APIs.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
client_access_key: str,
|
|
42
|
+
client_secret_key: str,
|
|
43
|
+
base_vwq_url: str = "https://cloudreco.vuforia.com",
|
|
44
|
+
request_timeout_seconds: float | tuple[float, float] = 30.0,
|
|
45
|
+
transport: AsyncTransport | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Args:
|
|
49
|
+
client_access_key: A VWS client access key.
|
|
50
|
+
client_secret_key: A VWS client secret key.
|
|
51
|
+
base_vwq_url: The base URL for the VWQ API.
|
|
52
|
+
request_timeout_seconds: The timeout for each
|
|
53
|
+
HTTP request. This can be a float to set both
|
|
54
|
+
the connect and read timeouts, or a
|
|
55
|
+
(connect, read) tuple.
|
|
56
|
+
transport: The async HTTP transport to use for
|
|
57
|
+
requests. Defaults to
|
|
58
|
+
``AsyncHTTPXTransport()``.
|
|
59
|
+
"""
|
|
60
|
+
self._client_access_key = client_access_key
|
|
61
|
+
self._client_secret_key = client_secret_key
|
|
62
|
+
self._base_vwq_url = base_vwq_url
|
|
63
|
+
self._request_timeout_seconds = request_timeout_seconds
|
|
64
|
+
self._transport = transport or AsyncHTTPXTransport()
|
|
65
|
+
|
|
66
|
+
async def aclose(self) -> None:
|
|
67
|
+
"""Close the underlying transport if it supports closing."""
|
|
68
|
+
close = getattr(self._transport, "aclose", None)
|
|
69
|
+
if close is not None:
|
|
70
|
+
await close()
|
|
71
|
+
|
|
72
|
+
async def __aenter__(self) -> Self:
|
|
73
|
+
"""Enter the async context manager."""
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(self, *_args: object) -> None:
|
|
77
|
+
"""Exit the async context manager and close the transport."""
|
|
78
|
+
await self.aclose()
|
|
79
|
+
|
|
80
|
+
async def query(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
image: _ImageType,
|
|
84
|
+
max_num_results: int = 1,
|
|
85
|
+
include_target_data: CloudRecoIncludeTargetData = (
|
|
86
|
+
CloudRecoIncludeTargetData.TOP
|
|
87
|
+
),
|
|
88
|
+
) -> list[QueryResult]:
|
|
89
|
+
"""Use the Vuforia Web Query API to make an Image
|
|
90
|
+
Recognition Query.
|
|
91
|
+
|
|
92
|
+
See
|
|
93
|
+
https://developer.vuforia.com/library/web-api/vuforia-query-web-api
|
|
94
|
+
for parameter details.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
image: The image to make a query against.
|
|
98
|
+
max_num_results: The maximum number of matching
|
|
99
|
+
targets to be returned.
|
|
100
|
+
include_target_data: Indicates if target_data
|
|
101
|
+
records shall be returned for the matched
|
|
102
|
+
targets. Accepted values are top (default
|
|
103
|
+
value, only return target_data for top ranked
|
|
104
|
+
match), none (return no target_data), all
|
|
105
|
+
(for all matched targets).
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
~vws.exceptions.cloud_reco_exceptions.AuthenticationFailureError:
|
|
109
|
+
The client access key pair is not correct.
|
|
110
|
+
~vws.exceptions.cloud_reco_exceptions.MaxNumResultsOutOfRangeError:
|
|
111
|
+
``max_num_results`` is not within the range (1, 50).
|
|
112
|
+
~vws.exceptions.cloud_reco_exceptions.InactiveProjectError: The
|
|
113
|
+
project is inactive.
|
|
114
|
+
~vws.exceptions.cloud_reco_exceptions.RequestTimeTooSkewedError:
|
|
115
|
+
There is an error with the time sent to Vuforia.
|
|
116
|
+
~vws.exceptions.cloud_reco_exceptions.BadImageError: There is a
|
|
117
|
+
problem with the given image. For example, it must be a JPEG or
|
|
118
|
+
PNG file in the grayscale or RGB color space.
|
|
119
|
+
~vws.exceptions.custom_exceptions.RequestEntityTooLargeError: The
|
|
120
|
+
given image is too large.
|
|
121
|
+
~vws.exceptions.custom_exceptions.ServerError: There is an
|
|
122
|
+
error with Vuforia's servers.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
An ordered list of target details of matching
|
|
126
|
+
targets.
|
|
127
|
+
"""
|
|
128
|
+
image_content = _get_image_data(image=image)
|
|
129
|
+
body: dict[str, Any] = {
|
|
130
|
+
"image": (
|
|
131
|
+
"image.jpeg",
|
|
132
|
+
image_content,
|
|
133
|
+
"image/jpeg",
|
|
134
|
+
),
|
|
135
|
+
"max_num_results": (
|
|
136
|
+
None,
|
|
137
|
+
int(max_num_results),
|
|
138
|
+
"text/plain",
|
|
139
|
+
),
|
|
140
|
+
"include_target_data": (
|
|
141
|
+
None,
|
|
142
|
+
include_target_data.value,
|
|
143
|
+
"text/plain",
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
date = rfc_1123_date()
|
|
147
|
+
request_path = "/v1/query"
|
|
148
|
+
content, content_type_header = encode_multipart_formdata(fields=body)
|
|
149
|
+
method = HTTPMethod.POST
|
|
150
|
+
|
|
151
|
+
authorization_string = authorization_header(
|
|
152
|
+
access_key=self._client_access_key,
|
|
153
|
+
secret_key=self._client_secret_key,
|
|
154
|
+
method=method,
|
|
155
|
+
content=content,
|
|
156
|
+
# Note that this is not the actual Content-Type
|
|
157
|
+
# header value sent.
|
|
158
|
+
content_type="multipart/form-data",
|
|
159
|
+
date=date,
|
|
160
|
+
request_path=request_path,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
headers = {
|
|
164
|
+
"Authorization": authorization_string,
|
|
165
|
+
"Date": date,
|
|
166
|
+
"Content-Type": content_type_header,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
response = await self._transport(
|
|
170
|
+
method=method,
|
|
171
|
+
url=self._base_vwq_url.rstrip("/") + request_path,
|
|
172
|
+
headers=headers,
|
|
173
|
+
data=content,
|
|
174
|
+
request_timeout=self._request_timeout_seconds,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
178
|
+
raise RequestEntityTooLargeError(response=response)
|
|
179
|
+
|
|
180
|
+
if "Integer out of range" in response.text:
|
|
181
|
+
raise MaxNumResultsOutOfRangeError(
|
|
182
|
+
response=response,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
|
187
|
+
): # pragma: no cover
|
|
188
|
+
raise ServerError(response=response)
|
|
189
|
+
|
|
190
|
+
result_code = json.loads(s=response.text)["result_code"]
|
|
191
|
+
if result_code != "Success":
|
|
192
|
+
exception = {
|
|
193
|
+
"AuthenticationFailure": (AuthenticationFailureError),
|
|
194
|
+
"BadImage": BadImageError,
|
|
195
|
+
"InactiveProject": InactiveProjectError,
|
|
196
|
+
"RequestTimeTooSkewed": (RequestTimeTooSkewedError),
|
|
197
|
+
}[result_code]
|
|
198
|
+
raise exception(response=response)
|
|
199
|
+
|
|
200
|
+
result: list[QueryResult] = []
|
|
201
|
+
result_list = list(
|
|
202
|
+
json.loads(s=response.text)["results"],
|
|
203
|
+
)
|
|
204
|
+
for item in result_list:
|
|
205
|
+
target_data: TargetData | None = None
|
|
206
|
+
if "target_data" in item:
|
|
207
|
+
target_data_dict = item["target_data"]
|
|
208
|
+
metadata = target_data_dict["application_metadata"]
|
|
209
|
+
timestamp_string = target_data_dict["target_timestamp"]
|
|
210
|
+
target_timestamp = datetime.datetime.fromtimestamp(
|
|
211
|
+
timestamp=timestamp_string,
|
|
212
|
+
tz=datetime.UTC,
|
|
213
|
+
)
|
|
214
|
+
target_data = TargetData(
|
|
215
|
+
name=target_data_dict["name"],
|
|
216
|
+
application_metadata=metadata,
|
|
217
|
+
target_timestamp=target_timestamp,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
query_result = QueryResult(
|
|
221
|
+
target_id=item["target_id"],
|
|
222
|
+
target_data=target_data,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result.append(query_result)
|
|
226
|
+
return result
|