Skip to content

Commit 0fc38aa

Browse files
adamtheturtleclaude
andcommitted
Move generate_vumark_instance to a new VuMarkService class
VuMark generation targets a different database type (VuMark vs Cloud Reco), so it belongs in its own class rather than VWS, mirroring how CloudRecoService is separate from VWS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a2b6332 commit 0fc38aa

File tree

6 files changed

+181
-92
lines changed

6 files changed

+181
-92
lines changed

src/vws/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""A library for Vuforia Web Services."""
22

33
from .query import CloudRecoService
4+
from .vumark_service import VuMarkService
45
from .vws import VWS
56

67
__all__ = [
78
"VWS",
89
"CloudRecoService",
10+
"VuMarkService",
911
]

src/vws/vumark_service.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.exceptions.custom_exceptions import ServerError
9+
from vws.exceptions.vws_exceptions import (
10+
AuthenticationFailureError,
11+
BadRequestError,
12+
DateRangeError,
13+
FailError,
14+
InvalidAcceptHeaderError,
15+
InvalidInstanceIdError,
16+
InvalidTargetTypeError,
17+
RequestTimeTooSkewedError,
18+
TargetStatusNotSuccessError,
19+
TooManyRequestsError,
20+
UnknownTargetError,
21+
)
22+
from vws.vumark_accept import VuMarkAccept
23+
from vws.vws import _target_api_request
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)

src/vws/vws.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@
2424
DateRangeError,
2525
FailError,
2626
ImageTooLargeError,
27-
InvalidAcceptHeaderError,
28-
InvalidInstanceIdError,
29-
InvalidTargetTypeError,
3027
MetadataTooLargeError,
3128
ProjectHasNoAPIAccessError,
3229
ProjectInactiveError,
@@ -48,7 +45,6 @@
4845
TargetSummaryReport,
4946
)
5047
from vws.response import Response
51-
from vws.vumark_accept import VuMarkAccept
5248

5349
_ImageType = io.BytesIO | BinaryIO
5450

@@ -243,9 +239,6 @@ def make_request(
243239
"DateRangeError": DateRangeError,
244240
"Fail": FailError,
245241
"ImageTooLarge": ImageTooLargeError,
246-
"InvalidAcceptHeader": InvalidAcceptHeaderError,
247-
"InvalidInstanceId": InvalidInstanceIdError,
248-
"InvalidTargetType": InvalidTargetTypeError,
249242
"MetadataTooLarge": MetadataTooLargeError,
250243
"ProjectHasNoAPIAccess": ProjectHasNoAPIAccessError,
251244
"ProjectInactive": ProjectInactiveError,
@@ -725,64 +718,3 @@ def update_target(
725718
expected_result_code="Success",
726719
content_type="application/json",
727720
)
728-
729-
def generate_vumark_instance(
730-
self,
731-
*,
732-
target_id: str,
733-
instance_id: str,
734-
accept: VuMarkAccept,
735-
) -> bytes:
736-
"""Generate a VuMark instance image.
737-
738-
See
739-
https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/
740-
for parameter details.
741-
742-
Args:
743-
target_id: The ID of the VuMark target.
744-
instance_id: The instance ID to encode in the VuMark.
745-
accept: The image format to return.
746-
747-
Returns:
748-
The VuMark instance image bytes.
749-
750-
Raises:
751-
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
752-
secret key is not correct.
753-
~vws.exceptions.vws_exceptions.FailError: There was an error with
754-
the request. For example, the given access key does not match a
755-
known database.
756-
~vws.exceptions.vws_exceptions.InvalidAcceptHeaderError: The
757-
Accept header value is not supported.
758-
~vws.exceptions.vws_exceptions.InvalidInstanceIdError: The
759-
instance ID is invalid. For example, it may be empty.
760-
~vws.exceptions.vws_exceptions.InvalidTargetTypeError: The target
761-
is not a VuMark template target.
762-
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
763-
an error with the time sent to Vuforia.
764-
~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The
765-
target is not in the success state.
766-
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
767-
ID does not match a target in the database.
768-
~vws.exceptions.custom_exceptions.ServerError: There is an error
769-
with Vuforia's servers.
770-
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
771-
rate limiting access.
772-
"""
773-
request_path = f"/targets/{target_id}/instances"
774-
content_type = "application/json"
775-
request_data = json.dumps(obj={"instance_id": instance_id}).encode(
776-
encoding="utf-8",
777-
)
778-
779-
response = self.make_request(
780-
method=HTTPMethod.POST,
781-
data=request_data,
782-
request_path=request_path,
783-
expected_result_code=None,
784-
content_type=content_type,
785-
extra_headers={"Accept": accept},
786-
)
787-
788-
return response.content

tests/conftest.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mock_vws import MockVWS
1010
from mock_vws.database import CloudDatabase, VuMarkDatabase, VuMarkTarget
1111

12-
from vws import VWS, CloudRecoService
12+
from vws import VWS, CloudRecoService, VuMarkService
1313

1414

1515
@pytest.fixture(name="_mock_database")
@@ -33,9 +33,11 @@ def fixture_mock_vumark_database() -> Generator[VuMarkDatabase]:
3333

3434

3535
@pytest.fixture
36-
def vumark_vws_client(_mock_vumark_database: VuMarkDatabase) -> VWS:
37-
"""A VWS client which connects to a mock VuMark database."""
38-
return VWS(
36+
def vumark_service_client(
37+
_mock_vumark_database: VuMarkDatabase,
38+
) -> VuMarkService:
39+
"""A ``VuMarkService`` client which connects to a mock VuMark database."""
40+
return VuMarkService(
3941
server_access_key=_mock_vumark_database.server_access_key,
4042
server_secret_key=_mock_vumark_database.server_secret_key,
4143
)

tests/test_vws.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from mock_vws import MockVWS
1414
from mock_vws.database import CloudDatabase
1515

16-
from vws import VWS, CloudRecoService
16+
from vws import VWS, CloudRecoService, VuMarkService
1717
from vws.exceptions.custom_exceptions import TargetProcessingTimeoutError
1818
from vws.reports import (
1919
DatabaseSummaryReport,
@@ -745,13 +745,13 @@ class TestGenerateVumarkInstance:
745745
],
746746
)
747747
def test_generate_vumark_instance(
748-
vumark_vws_client: VWS,
748+
vumark_service_client: VuMarkService,
749749
vumark_target_id: str,
750750
accept: VuMarkAccept,
751751
expected_prefix: bytes,
752752
) -> None:
753753
"""The returned bytes match the requested format."""
754-
result = vumark_vws_client.generate_vumark_instance(
754+
result = vumark_service_client.generate_vumark_instance(
755755
target_id=vumark_target_id,
756756
instance_id="12345",
757757
accept=accept,

tests/test_vws_exceptions.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from mock_vws.database import CloudDatabase
1111
from mock_vws.states import States
1212

13-
from vws import VWS
13+
from vws import VWS, VuMarkService
1414
from vws.exceptions.base_exceptions import VWSError
1515
from vws.exceptions.custom_exceptions import (
1616
ServerError,
@@ -368,15 +368,15 @@ def test_vwsexception_inheritance() -> None:
368368

369369

370370
def test_invalid_instance_id(
371-
vumark_vws_client: VWS,
371+
vumark_service_client: VuMarkService,
372372
vumark_target_id: str,
373373
) -> None:
374374
"""
375375
An ``InvalidInstanceId`` exception is raised when an empty instance
376376
ID is given.
377377
"""
378378
with pytest.raises(expected_exception=InvalidInstanceIdError) as exc:
379-
vumark_vws_client.generate_vumark_instance(
379+
vumark_service_client.generate_vumark_instance(
380380
target_id=vumark_target_id,
381381
instance_id="",
382382
accept=VuMarkAccept.PNG,
@@ -386,27 +386,37 @@ def test_invalid_instance_id(
386386

387387

388388
def test_invalid_target_type(
389-
vws_client: VWS,
390389
high_quality_image: io.BytesIO,
391390
) -> None:
392391
"""
393392
An ``InvalidTargetType`` exception is raised when trying to generate
394393
a VuMark instance from a non-VuMark target.
395394
"""
396-
target_id = vws_client.add_target(
397-
name="x",
398-
width=1,
399-
image=high_quality_image,
400-
active_flag=True,
401-
application_metadata=None,
402-
)
403-
vws_client.wait_for_target_processed(target_id=target_id)
404-
with pytest.raises(expected_exception=InvalidTargetTypeError) as exc:
405-
vws_client.generate_vumark_instance(
406-
target_id=target_id,
407-
instance_id="12345",
408-
accept=VuMarkAccept.PNG,
395+
database = VuforiaDatabase()
396+
with MockVWS(processing_time_seconds=0.2) as mock:
397+
mock.add_database(database=database)
398+
vws_client = VWS(
399+
server_access_key=database.server_access_key,
400+
server_secret_key=database.server_secret_key,
409401
)
402+
target_id = vws_client.add_target(
403+
name="x",
404+
width=1,
405+
image=high_quality_image,
406+
active_flag=True,
407+
application_metadata=None,
408+
)
409+
vws_client.wait_for_target_processed(target_id=target_id)
410+
vumark_client = VuMarkService(
411+
server_access_key=database.server_access_key,
412+
server_secret_key=database.server_secret_key,
413+
)
414+
with pytest.raises(expected_exception=InvalidTargetTypeError) as exc:
415+
vumark_client.generate_vumark_instance(
416+
target_id=target_id,
417+
instance_id="12345",
418+
accept=VuMarkAccept.PNG,
419+
)
410420

411421
assert exc.value.response.status_code == HTTPStatus.FORBIDDEN
412422

0 commit comments

Comments
 (0)