From abf7232ecf2e08a9c9259329176304507a82ff1d Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 4 Aug 2026 17:30:57 +0100 Subject: [PATCH 1/2] Handle documented VuMark generation failures --- newsfragments/3090.change.rst | 1 + pyproject.toml | 2 +- spelling_private_dict.txt | 3 ++ src/vws/async_vumark_service.py | 6 ++++ src/vws/exceptions/vws_exceptions.py | 24 +++++++++++++ src/vws/vumark_service.py | 6 ++++ tests/test_async_vws_exceptions.py | 49 +++++++++++++++++++++++++- tests/test_vws_exceptions.py | 52 +++++++++++++++++++++++++++- 8 files changed, 140 insertions(+), 3 deletions(-) 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/pyproject.toml b/pyproject.toml index eabcc8401..43b3771a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ optional-dependencies.dev = [ "ty==0.0.65", "types-requests==2.33.0.20260712", "vulture==2.16", - "vws-python-mock==2026.8.4.1", + "vws-python-mock==2026.8.4.2", "vws-test-fixtures==2023.3.5", "yamlfix==1.19.1", "zizmor==1.28.0", diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 382e8ccd3..45ed889ed 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -1,4 +1,5 @@ AuthenticationFailure +AuthorizationFailed BadImage ConnectionErrorPossiblyImageTooLarge DateRangeError @@ -6,6 +7,7 @@ Falsy ImageTooLarge InactiveProject JSONDecodeError +LicenseCheckFailed MatchProcessing MaxNumResultsOutOfRange MetadataTooLarge @@ -14,6 +16,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 9a050afdd..6575f7716 100644 --- a/src/vws/async_vumark_service.py +++ b/src/vws/async_vumark_service.py @@ -87,6 +87,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. @@ -96,6 +98,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 3749b5619..a93dd6004 100644 --- a/src/vws/exceptions/vws_exceptions.py +++ b/src/vws/exceptions/vws_exceptions.py @@ -206,9 +206,31 @@ class InvalidTargetTypeError(VWSError): """ +@beartype +class QuotaExceededError(VWSError): + """Exception raised when Vuforia returns a response with a result code + ``QuotaExceeded``. + """ + + +@beartype +class LicenseCheckFailedError(VWSError): + """Exception raised when Vuforia returns a response with a result code + ``LicenseCheckFailed``. + """ + + +@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, @@ -217,10 +239,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 0a5eb98a1..168e8b6f8 100644 --- a/src/vws/vumark_service.py +++ b/src/vws/vumark_service.py @@ -71,6 +71,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. @@ -80,6 +82,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 daa57423a..cc661c9f4 100644 --- a/tests/test_async_vws_exceptions.py +++ b/tests/test_async_vws_exceptions.py @@ -5,7 +5,7 @@ from http import HTTPStatus import pytest -from mock_vws import MockVWS +from mock_vws import MockVWS, VuMarkGenerationFailure from mock_vws.database import CloudDatabase from mock_vws.states import States @@ -16,14 +16,17 @@ ) from vws.exceptions.vws_exceptions import ( AuthenticationFailureError, + AuthorizationFailedError, BadImageError, FailError, ImageTooLargeError, InvalidInstanceIdError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectHasNoAPIAccessError, ProjectInactiveError, ProjectSuspendedError, + QuotaExceededError, RequestQuotaReachedError, TargetNameExistError, TargetQuotaReachedError, @@ -377,3 +380,47 @@ async def test_invalid_instance_id( ) assert exc.value.response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + argnames=("failure", "exception_type", "status_code"), + argvalues=[ + ( + VuMarkGenerationFailure.QUOTA_EXCEEDED, + QuotaExceededError, + HTTPStatus.FORBIDDEN, + ), + ( + VuMarkGenerationFailure.LICENSE_CHECK_FAILED, + LicenseCheckFailedError, + HTTPStatus.FORBIDDEN, + ), + ( + VuMarkGenerationFailure.AUTHORIZATION_FAILED, + AuthorizationFailedError, + HTTPStatus.UNAUTHORIZED, + ), + ], +) +async def test_documented_vumark_error_codes( + *, + failure: VuMarkGenerationFailure, + exception_type: type[VWSError], + status_code: HTTPStatus, +) -> None: + """Documented VuMark failures raise matching exceptions.""" + with MockVWS(vumark_generation_failure=failure): + async with AsyncVuMarkService( + server_access_key=uuid.uuid4().hex, + server_secret_key=uuid.uuid4().hex, + ) as vumark_service: + with pytest.raises(expected_exception=exception_type) as exc: + await vumark_service.generate_vumark_instance( + target_id="exampletargetid", + instance_id="example_instance_id", + accept=VuMarkAccept.PNG, + ) + + assert exc.value.response.status_code == status_code + assert failure.value in exc.value.response.text diff --git a/tests/test_vws_exceptions.py b/tests/test_vws_exceptions.py index f05436e5c..73ab9f849 100644 --- a/tests/test_vws_exceptions.py +++ b/tests/test_vws_exceptions.py @@ -6,7 +6,7 @@ import pytest from freezegun import freeze_time -from mock_vws import MockVWS +from mock_vws import MockVWS, VuMarkGenerationFailure from mock_vws.database import CloudDatabase from mock_vws.states import States @@ -17,6 +17,7 @@ ) from vws.exceptions.vws_exceptions import ( AuthenticationFailureError, + AuthorizationFailedError, BadImageError, BadRequestError, DateRangeError, @@ -25,10 +26,12 @@ InvalidAcceptHeaderError, InvalidInstanceIdError, InvalidTargetTypeError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectHasNoAPIAccessError, ProjectInactiveError, ProjectSuspendedError, + QuotaExceededError, RequestQuotaReachedError, RequestTimeTooSkewedError, TargetNameExistError, @@ -412,6 +415,7 @@ def test_vwsexception_inheritance() -> None: """VWS-related exceptions should inherit from VWSException.""" subclasses = [ AuthenticationFailureError, + AuthorizationFailedError, BadImageError, BadRequestError, DateRangeError, @@ -420,10 +424,12 @@ def test_vwsexception_inheritance() -> None: InvalidAcceptHeaderError, InvalidInstanceIdError, InvalidTargetTypeError, + LicenseCheckFailedError, MetadataTooLargeError, ProjectInactiveError, ProjectHasNoAPIAccessError, ProjectSuspendedError, + QuotaExceededError, RequestQuotaReachedError, RequestTimeTooSkewedError, TargetNameExistError, @@ -492,6 +498,50 @@ def test_invalid_target_type( assert exc.value.response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY +@pytest.mark.parametrize( + argnames=("failure", "exception_type", "status_code"), + argvalues=[ + ( + VuMarkGenerationFailure.QUOTA_EXCEEDED, + QuotaExceededError, + HTTPStatus.FORBIDDEN, + ), + ( + VuMarkGenerationFailure.LICENSE_CHECK_FAILED, + LicenseCheckFailedError, + HTTPStatus.FORBIDDEN, + ), + ( + VuMarkGenerationFailure.AUTHORIZATION_FAILED, + AuthorizationFailedError, + HTTPStatus.UNAUTHORIZED, + ), + ], +) +def test_documented_vumark_error_codes( + *, + failure: VuMarkGenerationFailure, + exception_type: type[VWSError], + status_code: HTTPStatus, +) -> None: + """Documented VuMark failures raise matching exceptions.""" + with MockVWS(vumark_generation_failure=failure): + vumark_service = VuMarkService( + server_access_key=uuid.uuid4().hex, + server_secret_key=uuid.uuid4().hex, + ) + + with pytest.raises(expected_exception=exception_type) as exc: + vumark_service.generate_vumark_instance( + target_id="exampletargetid", + instance_id="example_instance_id", + accept=VuMarkAccept.PNG, + ) + + assert exc.value.response.status_code == status_code + assert failure.value in exc.value.response.text + + def test_base_exception( *, vws_client: VWS, From 6fdd76d837bb0d673cdc3cb465e929765acb2eb6 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Wed, 5 Aug 2026 11:00:50 +0100 Subject: [PATCH 2/2] Fix async VuMark test coverage --- tests/test_async_vws_exceptions.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_async_vws_exceptions.py b/tests/test_async_vws_exceptions.py index cc661c9f4..efc3c05ae 100644 --- a/tests/test_async_vws_exceptions.py +++ b/tests/test_async_vws_exceptions.py @@ -411,16 +411,17 @@ async def test_documented_vumark_error_codes( ) -> None: """Documented VuMark failures raise matching exceptions.""" with MockVWS(vumark_generation_failure=failure): - async with AsyncVuMarkService( + vumark_service = AsyncVuMarkService( server_access_key=uuid.uuid4().hex, server_secret_key=uuid.uuid4().hex, - ) as vumark_service: - with pytest.raises(expected_exception=exception_type) as exc: - await vumark_service.generate_vumark_instance( - target_id="exampletargetid", - instance_id="example_instance_id", - accept=VuMarkAccept.PNG, - ) + ) + with pytest.raises(expected_exception=exception_type) as exc: + await vumark_service.generate_vumark_instance( + target_id="exampletargetid", + instance_id="example_instance_id", + accept=VuMarkAccept.PNG, + ) + await vumark_service.aclose() assert exc.value.response.status_code == status_code assert failure.value in exc.value.response.text