From 0434a1b975eb0e3ea35c01527efa994eab5a15da Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 17 Jul 2026 07:57:15 +0100 Subject: [PATCH] Add exceptions for documented VuMark error result codes. Map QuotaExceeded, LicenseCheckFailed, and AuthorizationFailed to public exceptions so VuMark clients no longer raise KeyError. Closes #3090. Co-authored-by: Cursor --- newsfragments/3090.change.rst | 1 + spelling_private_dict.txt | 3 ++ src/vws/async_vumark_service.py | 6 +++ src/vws/exceptions/vws_exceptions.py | 27 ++++++++++ src/vws/vumark_service.py | 6 +++ tests/test_async_vws_exceptions.py | 79 +++++++++++++++++++++++++++ tests/test_vws_exceptions.py | 80 ++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+) create mode 100644 newsfragments/3090.change.rst diff --git a/newsfragments/3090.change.rst b/newsfragments/3090.change.rst new file mode 100644 index 000000000..226d9665f --- /dev/null +++ b/newsfragments/3090.change.rst @@ -0,0 +1 @@ +Add ``QuotaExceededError``, ``LicenseCheckFailedError``, and ``AuthorizationFailedError`` for documented VuMark Generation API result codes. diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 73d358a00..2a0e5d963 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -1,10 +1,12 @@ AuthenticationFailure +AuthorizationFailed BadImage ConnectionErrorPossiblyImageTooLarge DateRangeError ImageTooLarge InactiveProject JSONDecodeError +LicenseCheckFailed MatchProcessing MaxNumResultsOutOfRange MetadataTooLarge @@ -13,6 +15,7 @@ OopsAnErrorOccurredPossiblyBadNameError ProjectHasNoAPIAccess ProjectInactive ProjectSuspended +QuotaExceeded RequestQuotaReached RequestTimeTooSkewed TargetNameExist diff --git a/src/vws/async_vumark_service.py b/src/vws/async_vumark_service.py index 51c2ef349..ac5c85019 100644 --- a/src/vws/async_vumark_service.py +++ b/src/vws/async_vumark_service.py @@ -85,6 +85,8 @@ async def generate_vumark_instance( Raises: ~vws.exceptions.vws_exceptions.AuthenticationFailureError: The secret key is not correct. + ~vws.exceptions.vws_exceptions.AuthorizationFailedError: There was + a general authentication problem. ~vws.exceptions.vws_exceptions.FailError: There was an error with the request. For example, the given access key does not match a known database. @@ -94,6 +96,10 @@ async def generate_vumark_instance( instance ID is invalid. For example, it may be empty. ~vws.exceptions.vws_exceptions.InvalidTargetTypeError: The target is not a VuMark template target. + ~vws.exceptions.vws_exceptions.LicenseCheckFailedError: The + license state and/or type does not allow this request. + ~vws.exceptions.vws_exceptions.QuotaExceededError: No more + instances can be created for the associated license. ~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is an error with the time sent to Vuforia. ~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The diff --git a/src/vws/exceptions/vws_exceptions.py b/src/vws/exceptions/vws_exceptions.py index de09cdeaa..cb652399e 100644 --- a/src/vws/exceptions/vws_exceptions.py +++ b/src/vws/exceptions/vws_exceptions.py @@ -204,9 +204,34 @@ class InvalidTargetTypeError(VWSError): """ +# This is not simulated by the mock. +@beartype +class QuotaExceededError(VWSError): + """Exception raised when Vuforia returns a response with a result code + ``QuotaExceeded``. + """ + + +# This is not simulated by the mock. +@beartype +class LicenseCheckFailedError(VWSError): + """Exception raised when Vuforia returns a response with a result code + ``LicenseCheckFailed``. + """ + + +# This is not simulated by the mock. +@beartype +class AuthorizationFailedError(VWSError): + """Exception raised when Vuforia returns a response with a result code + ``AuthorizationFailed``. + """ + + VWSError.register_exceptions_by_result_code( exceptions_by_result_code={ "AuthenticationFailure": AuthenticationFailureError, + "AuthorizationFailed": AuthorizationFailedError, "BadImage": BadImageError, "BadRequest": BadRequestError, "DateRangeError": DateRangeError, @@ -215,10 +240,12 @@ class InvalidTargetTypeError(VWSError): "InvalidAcceptHeader": InvalidAcceptHeaderError, "InvalidInstanceId": InvalidInstanceIdError, "InvalidTargetType": InvalidTargetTypeError, + "LicenseCheckFailed": LicenseCheckFailedError, "MetadataTooLarge": MetadataTooLargeError, "ProjectHasNoAPIAccess": ProjectHasNoAPIAccessError, "ProjectInactive": ProjectInactiveError, "ProjectSuspended": ProjectSuspendedError, + "QuotaExceeded": QuotaExceededError, "RequestQuotaReached": RequestQuotaReachedError, "RequestTimeTooSkewed": RequestTimeTooSkewedError, "TargetNameExist": TargetNameExistError, diff --git a/src/vws/vumark_service.py b/src/vws/vumark_service.py index f4b04152f..5ce5df3cb 100644 --- a/src/vws/vumark_service.py +++ b/src/vws/vumark_service.py @@ -69,6 +69,8 @@ def generate_vumark_instance( Raises: ~vws.exceptions.vws_exceptions.AuthenticationFailureError: The secret key is not correct. + ~vws.exceptions.vws_exceptions.AuthorizationFailedError: There was + a general authentication problem. ~vws.exceptions.vws_exceptions.FailError: There was an error with the request. For example, the given access key does not match a known database. @@ -78,6 +80,10 @@ def generate_vumark_instance( instance ID is invalid. For example, it may be empty. ~vws.exceptions.vws_exceptions.InvalidTargetTypeError: The target is not a VuMark template target. + ~vws.exceptions.vws_exceptions.LicenseCheckFailedError: The + license state and/or type does not allow this request. + ~vws.exceptions.vws_exceptions.QuotaExceededError: No more + instances can be created for the associated license. ~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is an error with the time sent to Vuforia. ~vws.exceptions.vws_exceptions.TargetStatusNotSuccessError: The diff --git a/tests/test_async_vws_exceptions.py b/tests/test_async_vws_exceptions.py index cd35854f1..17ad8518a 100644 --- a/tests/test_async_vws_exceptions.py +++ b/tests/test_async_vws_exceptions.py @@ -1,6 +1,7 @@ """Tests for VWS exceptions raised from async clients.""" import io +import json import uuid from http import HTTPStatus @@ -10,21 +11,26 @@ from mock_vws.states import States from vws import AsyncVuMarkService, AsyncVWS +from vws.exceptions.base_exceptions import VWSError from vws.exceptions.custom_exceptions import ( ServerError, ) from vws.exceptions.vws_exceptions import ( AuthenticationFailureError, + AuthorizationFailedError, BadImageError, FailError, ImageTooLargeError, InvalidInstanceIdError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectInactiveError, + QuotaExceededError, TargetNameExistError, TargetStatusProcessingError, UnknownTargetError, ) +from vws.response import Response from vws.vumark_accept import VuMarkAccept @@ -302,3 +308,76 @@ async def test_invalid_instance_id( ) assert exc.value.response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + argnames=("result_code", "exception_type", "status_code"), + argvalues=[ + ("QuotaExceeded", QuotaExceededError, HTTPStatus.FORBIDDEN), + ("LicenseCheckFailed", LicenseCheckFailedError, HTTPStatus.FORBIDDEN), + ( + "AuthorizationFailed", + AuthorizationFailedError, + HTTPStatus.UNAUTHORIZED, + ), + ], +) +async def test_documented_vumark_error_codes( + *, + result_code: str, + exception_type: type[VWSError], + status_code: HTTPStatus, +) -> None: + """ + Documented VuMark Generation API error result codes raise matching + exceptions. + """ + + class _AsyncVuMarkErrorTransport: + """Async transport that returns a documented VuMark error result + code. + """ + + async def aclose(self) -> None: + """Close the transport.""" + + async def __call__( + self, + *, + method: str, + url: str, + headers: dict[str, str], + data: bytes, + request_timeout: float | tuple[float, float], + ) -> Response: + """Return a VuMark error response.""" + del method, headers, request_timeout + body = { + "transaction_id": uuid.uuid4().hex, + "result_code": result_code, + } + text = json.dumps(obj=body) + return Response( + text=text, + url=url, + status_code=status_code, + headers={"Content-Type": "application/json"}, + request_body=data, + tell_position=0, + content=text.encode(), + ) + + async with AsyncVuMarkService( + server_access_key=uuid.uuid4().hex, + server_secret_key=uuid.uuid4().hex, + transport=_AsyncVuMarkErrorTransport(), + ) as vumark_service: + with pytest.raises(expected_exception=exception_type) as exc: + await vumark_service.generate_vumark_instance( + target_id="example_target_id", + instance_id="example_instance_id", + accept=VuMarkAccept.PNG, + ) + + assert exc.value.response.status_code == status_code diff --git a/tests/test_vws_exceptions.py b/tests/test_vws_exceptions.py index 798d15f71..aab08eeae 100644 --- a/tests/test_vws_exceptions.py +++ b/tests/test_vws_exceptions.py @@ -1,6 +1,7 @@ """Tests for VWS exceptions.""" import io +import json import uuid from http import HTTPStatus @@ -17,6 +18,7 @@ ) from vws.exceptions.vws_exceptions import ( AuthenticationFailureError, + AuthorizationFailedError, BadImageError, BadRequestError, DateRangeError, @@ -25,10 +27,12 @@ InvalidAcceptHeaderError, InvalidInstanceIdError, InvalidTargetTypeError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectHasNoAPIAccessError, ProjectInactiveError, ProjectSuspendedError, + QuotaExceededError, RequestQuotaReachedError, RequestTimeTooSkewedError, TargetNameExistError, @@ -354,6 +358,7 @@ def test_vwsexception_inheritance() -> None: """VWS-related exceptions should inherit from VWSException.""" subclasses = [ AuthenticationFailureError, + AuthorizationFailedError, BadImageError, BadRequestError, DateRangeError, @@ -362,10 +367,12 @@ def test_vwsexception_inheritance() -> None: InvalidAcceptHeaderError, InvalidInstanceIdError, InvalidTargetTypeError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectInactiveError, ProjectHasNoAPIAccessError, ProjectSuspendedError, + QuotaExceededError, RequestQuotaReachedError, RequestTimeTooSkewedError, TargetNameExistError, @@ -434,6 +441,79 @@ def test_invalid_target_type( assert exc.value.response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY +@pytest.mark.parametrize( + argnames=("result_code", "exception_type", "status_code"), + argvalues=[ + ("QuotaExceeded", QuotaExceededError, HTTPStatus.FORBIDDEN), + ("LicenseCheckFailed", LicenseCheckFailedError, HTTPStatus.FORBIDDEN), + ( + "AuthorizationFailed", + AuthorizationFailedError, + HTTPStatus.UNAUTHORIZED, + ), + ], +) +def test_documented_vumark_error_codes( + *, + result_code: str, + exception_type: type[VWSError], + status_code: HTTPStatus, +) -> None: + """ + Documented VuMark Generation API error result codes raise matching + exceptions. + """ + + class _VuMarkErrorTransport: + """Transport that returns a documented VuMark error result + code. + """ + + def close(self) -> None: + """Close the transport.""" + + def __call__( + self, + *, + method: str, + url: str, + headers: dict[str, str], + data: bytes, + request_timeout: float | tuple[float, float], + ) -> Response: + """Return a VuMark error response.""" + del method, headers, request_timeout + body = { + "transaction_id": uuid.uuid4().hex, + "result_code": result_code, + } + text = json.dumps(obj=body) + return Response( + text=text, + url=url, + status_code=status_code, + headers={"Content-Type": "application/json"}, + request_body=data, + tell_position=0, + content=text.encode(), + ) + + vumark_service = VuMarkService( + server_access_key=uuid.uuid4().hex, + server_secret_key=uuid.uuid4().hex, + transport=_VuMarkErrorTransport(), + ) + + with pytest.raises(expected_exception=exception_type) as exc: + vumark_service.generate_vumark_instance( + target_id="example_target_id", + instance_id="example_instance_id", + accept=VuMarkAccept.PNG, + ) + + assert exc.value.response.status_code == status_code + + def test_base_exception( *, vws_client: VWS,