Skip to content

Commit 6c1c4a4

Browse files
adamtheturtleclaude
andcommitted
Add VuMark generation support
Implement VuMark instance generation API with support for multiple image formats (PNG, SVG, PDF). Add InvalidAcceptHeaderError and InvalidInstanceIdError exceptions for proper error handling. Includes comprehensive tests for all VuMark formats. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent f24db3b commit 6c1c4a4

7 files changed

Lines changed: 240 additions & 1 deletion

File tree

docs/source/api-reference.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ API Reference
1313
:undoc-members:
1414
:members:
1515

16+
.. automodule:: vws.vumark_accept
17+
:undoc-members:
18+
:members:
19+
1620
.. automodule:: vws.response
1721
:undoc-members:
1822
:members:

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ optional-dependencies.dev = [
8282
"ty==0.0.17",
8383
"types-requests==2.32.4.20260107",
8484
"vulture==2.14",
85-
"vws-python-mock==2026.2.18",
85+
"vws-python-mock==2026.2.18.1",
8686
"vws-test-fixtures==2023.3.5",
8787
"yamlfix==1.19.1",
8888
"zizmor==1.22.0",
@@ -363,6 +363,9 @@ ignore_names = [
363363
"pytest_plugins",
364364
# pytest fixtures - we name fixtures like this for this purpose
365365
"fixture_*",
366+
# Enum members used dynamically
367+
"PDF",
368+
"SVG",
366369
# Sphinx
367370
"autoclass_content",
368371
"autoclass_content",

src/vws/exceptions/vws_exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,19 @@ class TooManyRequestsError(VWSError): # pragma: no cover
167167
"""Exception raised when Vuforia returns a response with a result code
168168
'TooManyRequests'.
169169
"""
170+
171+
172+
# This is not simulated by client code because the accept parameter uses
173+
# the VuMarkAccept enum, which only allows valid values.
174+
@beartype
175+
class InvalidAcceptHeaderError(VWSError): # pragma: no cover
176+
"""Exception raised when Vuforia returns a response with a result code
177+
'InvalidAcceptHeader'.
178+
"""
179+
180+
181+
@beartype
182+
class InvalidInstanceIdError(VWSError):
183+
"""Exception raised when Vuforia returns a response with a result code
184+
'InvalidInstanceId'.
185+
"""

src/vws/vumark_accept.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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"

src/vws/vws.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
DateRangeError,
2424
FailError,
2525
ImageTooLargeError,
26+
InvalidAcceptHeaderError,
27+
InvalidInstanceIdError,
2628
MetadataTooLargeError,
2729
ProjectHasNoAPIAccessError,
2830
ProjectInactiveError,
@@ -44,6 +46,7 @@
4446
TargetSummaryReport,
4547
)
4648
from vws.response import Response
49+
from vws.vumark_accept import VuMarkAccept
4750

4851
_ImageType = io.BytesIO | BinaryIO
4952

@@ -700,3 +703,116 @@ def update_target(
700703
expected_result_code="Success",
701704
content_type="application/json",
702705
)
706+
707+
def generate_vumark_instance(
708+
self,
709+
*,
710+
target_id: str,
711+
instance_id: str,
712+
accept: VuMarkAccept = VuMarkAccept.PNG,
713+
) -> bytes:
714+
"""Generate a VuMark instance image.
715+
716+
See
717+
https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/
718+
for parameter details.
719+
720+
Args:
721+
target_id: The ID of the VuMark target.
722+
instance_id: The instance ID to encode in the VuMark.
723+
accept: The image format to return.
724+
725+
Returns:
726+
The VuMark instance image bytes.
727+
728+
Raises:
729+
~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
730+
secret key is not correct.
731+
~vws.exceptions.vws_exceptions.FailError: There was an error with
732+
the request. For example, the given access key does not match a
733+
known database.
734+
~vws.exceptions.vws_exceptions.InvalidAcceptHeaderError: The
735+
Accept header value is not supported.
736+
~vws.exceptions.vws_exceptions.InvalidInstanceIdError: The
737+
instance ID is invalid. For example, it may be empty.
738+
~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
739+
an error with the time sent to Vuforia.
740+
~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The
741+
target is not in the success state.
742+
~vws.exceptions.vws_exceptions.UnknownTargetError: The given target
743+
ID does not match a target in the database.
744+
~vws.exceptions.custom_exceptions.ServerError: There is an error
745+
with Vuforia's servers.
746+
~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
747+
rate limiting access.
748+
"""
749+
request_path = f"/targets/{target_id}/instances"
750+
content_type = "application/json"
751+
request_data = json.dumps(obj={"instance_id": instance_id}).encode(
752+
encoding="utf-8",
753+
)
754+
date_string = rfc_1123_date()
755+
756+
signature_string = authorization_header(
757+
access_key=self._server_access_key,
758+
secret_key=self._server_secret_key,
759+
method=HTTPMethod.POST,
760+
content=request_data,
761+
content_type=content_type,
762+
date=date_string,
763+
request_path=request_path,
764+
)
765+
766+
headers = {
767+
"Authorization": signature_string,
768+
"Date": date_string,
769+
"Content-Type": content_type,
770+
"Accept": accept,
771+
}
772+
773+
url = urljoin(base=self._base_vws_url, url=request_path)
774+
775+
requests_response = requests.request(
776+
method=HTTPMethod.POST,
777+
url=url,
778+
headers=headers,
779+
data=request_data,
780+
timeout=self._request_timeout_seconds,
781+
)
782+
783+
if requests_response.status_code == HTTPStatus.OK:
784+
return bytes(requests_response.content)
785+
786+
response = Response(
787+
text=requests_response.text,
788+
url=requests_response.url,
789+
status_code=requests_response.status_code,
790+
headers=dict(requests_response.headers),
791+
request_body=requests_response.request.body,
792+
tell_position=requests_response.raw.tell(),
793+
)
794+
795+
if (
796+
requests_response.status_code == HTTPStatus.TOO_MANY_REQUESTS
797+
): # pragma: no cover
798+
raise TooManyRequestsError(response=response)
799+
800+
if (
801+
requests_response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
802+
): # pragma: no cover
803+
raise ServerError(response=response)
804+
805+
result_code = json.loads(s=response.text)["result_code"]
806+
807+
exception = {
808+
"AuthenticationFailure": AuthenticationFailureError,
809+
"DateRangeError": DateRangeError,
810+
"Fail": FailError,
811+
"InvalidAcceptHeader": InvalidAcceptHeaderError,
812+
"InvalidInstanceId": InvalidInstanceIdError,
813+
"RequestTimeTooSkewed": RequestTimeTooSkewedError,
814+
"TargetStatusNotSuccess": TargetStatusNotSuccessError,
815+
"UnknownTarget": UnknownTargetError,
816+
}[result_code]
817+
818+
raise exception(response=response)

tests/test_vws.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
TargetStatuses,
2222
TargetSummaryReport,
2323
)
24+
from vws.vumark_accept import VuMarkAccept
2425

2526

2627
class TestAddTarget:
@@ -729,3 +730,55 @@ def test_no_fields_given(
729730
)
730731
vws_client.wait_for_target_processed(target_id=target_id)
731732
vws_client.update_target(target_id=target_id)
733+
734+
735+
class TestGenerateVumarkInstance:
736+
"""Tests for generating VuMark instances."""
737+
738+
@staticmethod
739+
@pytest.mark.parametrize(
740+
argnames="accept",
741+
argvalues=list(VuMarkAccept),
742+
)
743+
def test_generate_vumark_instance(
744+
vws_client: VWS,
745+
high_quality_image: io.BytesIO,
746+
accept: VuMarkAccept,
747+
) -> None:
748+
"""Bytes are returned when generating a VuMark instance."""
749+
target_id = vws_client.add_target(
750+
name="x",
751+
width=1,
752+
image=high_quality_image,
753+
active_flag=True,
754+
application_metadata=None,
755+
)
756+
vws_client.wait_for_target_processed(target_id=target_id)
757+
result = vws_client.generate_vumark_instance(
758+
target_id=target_id,
759+
instance_id="12345",
760+
accept=accept,
761+
)
762+
assert isinstance(result, bytes)
763+
assert len(result) > 0
764+
765+
@staticmethod
766+
def test_generate_vumark_default_accept(
767+
vws_client: VWS,
768+
high_quality_image: io.BytesIO,
769+
) -> None:
770+
"""By default, PNG is returned."""
771+
target_id = vws_client.add_target(
772+
name="x",
773+
width=1,
774+
image=high_quality_image,
775+
active_flag=True,
776+
application_metadata=None,
777+
)
778+
vws_client.wait_for_target_processed(target_id=target_id)
779+
result = vws_client.generate_vumark_instance(
780+
target_id=target_id,
781+
instance_id="12345",
782+
)
783+
assert isinstance(result, bytes)
784+
assert len(result) > 0

tests/test_vws_exceptions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
DateRangeError,
2222
FailError,
2323
ImageTooLargeError,
24+
InvalidAcceptHeaderError,
25+
InvalidInstanceIdError,
2426
MetadataTooLargeError,
2527
ProjectHasNoAPIAccessError,
2628
ProjectInactiveError,
@@ -342,6 +344,8 @@ def test_vwsexception_inheritance() -> None:
342344
DateRangeError,
343345
FailError,
344346
ImageTooLargeError,
347+
InvalidAcceptHeaderError,
348+
InvalidInstanceIdError,
345349
MetadataTooLargeError,
346350
ProjectInactiveError,
347351
ProjectHasNoAPIAccessError,
@@ -358,6 +362,31 @@ def test_vwsexception_inheritance() -> None:
358362
assert issubclass(subclass, VWSError)
359363

360364

365+
def test_invalid_instance_id(
366+
vws_client: VWS,
367+
high_quality_image: io.BytesIO,
368+
) -> None:
369+
"""
370+
An ``InvalidInstanceId`` exception is raised when an empty instance
371+
ID is given.
372+
"""
373+
target_id = vws_client.add_target(
374+
name="x",
375+
width=1,
376+
image=high_quality_image,
377+
active_flag=True,
378+
application_metadata=None,
379+
)
380+
vws_client.wait_for_target_processed(target_id=target_id)
381+
with pytest.raises(expected_exception=InvalidInstanceIdError) as exc:
382+
vws_client.generate_vumark_instance(
383+
target_id=target_id,
384+
instance_id="",
385+
)
386+
387+
assert exc.value.response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
388+
389+
361390
def test_base_exception(
362391
vws_client: VWS,
363392
high_quality_image: io.BytesIO,

0 commit comments

Comments
 (0)