From 53555ab6f1bafa043043c53625ecfe36d7160711 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Sun, 16 Aug 2026 15:04:12 +0100 Subject: [PATCH] Mock exhausted Model Target training allowance --- .github/workflows/test.yml | 1 + docs/source/differences-to-vws.rst | 6 ++ docs/source/docker.rst | 7 ++ newsfragments/3462.change | 1 + src/mock_vws/_flask_server/vws.py | 7 ++ src/mock_vws/_model_target_web_api.py | 15 ++- .../mock_web_services_api.py | 13 +++ src/mock_vws/decorators.py | 8 ++ .../test_model_target_training_allowance.py | 96 +++++++++++++++++++ 9 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 newsfragments/3462.change create mode 100644 tests/mock_vws/test_model_target_training_allowance.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 927f7d41c..cc8e5047c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -120,6 +120,7 @@ jobs: - tests/mock_vws/test_flask_app_usage.py - tests/mock_vws/test_model_target_generation_failure.py - tests/mock_vws/test_model_target_generation_warning.py + - tests/mock_vws/test_model_target_training_allowance.py - tests/mock_vws/test_model_target_web_api.py - tests/mock_vws/test_vumark_generation_api.py - tests/mock_vws/test_vumark_generation_failure.py diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index da9d522c9..bd36da5cf 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -238,6 +238,12 @@ in-process Model Target datasets finish with a ``failed`` status and an :paramref:`~mock_vws.MockVWS.processing_time_seconds`, so callers can test both processing and failed states. This configuration is not supported by the Flask/Docker backend. +Use +:paramref:`mock_vws.MockVWS.model_target_training_allowance_exceeded` to make +in-process Model Target dataset creation return Vuforia's +``TRAINING_ALLOWANCE_EXCEEDED`` response. Set the +:envvar:`MODEL_TARGET_TRAINING_ALLOWANCE_EXCEEDED` environment variable to +``true`` to configure the same response in the Flask/Docker backend. Use :paramref:`mock_vws.MockVWS.model_target_generation_warning` to make successful in-process Model Target datasets include a Vuforia-shaped ``warning`` object after processing completes. This configuration is not diff --git a/docs/source/docker.rst b/docs/source/docker.rst index 0dd25c2b1..6e9e5516d 100644 --- a/docs/source/docker.rst +++ b/docs/source/docker.rst @@ -158,6 +158,13 @@ VWS container Default: ``2.0`` +.. envvar:: MODEL_TARGET_TRAINING_ALLOWANCE_EXCEEDED + + Whether Model Target dataset creation returns Vuforia's + ``TRAINING_ALLOWANCE_EXCEEDED`` response. + + Default: ``false`` + .. envvar:: VWS_BASE_URL The base URL which clients use to reach the VWS container. diff --git a/newsfragments/3462.change b/newsfragments/3462.change new file mode 100644 index 000000000..6033bad9a --- /dev/null +++ b/newsfragments/3462.change @@ -0,0 +1 @@ +- Add configurable Model Target ``TRAINING_ALLOWANCE_EXCEEDED`` responses to the in-process and Flask/Docker mocks. diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index 3696f48d3..eb096d59d 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -129,6 +129,7 @@ class VWSSettings(BaseSettings): _ImageMatcherChoice.STRUCTURAL_SIMILARITY ) response_delay_seconds: float = 0.0 + model_target_training_allowance_exceeded: bool = False @beartype @@ -504,6 +505,9 @@ def create_standard_model_target_dataset() -> Response: dataset_type=ModelTargetDatasetType.STANDARD, generation_failure=None, generation_warning=None, + training_allowance_exceeded=( + settings.model_target_training_allowance_exceeded + ), ), ) @@ -524,6 +528,9 @@ def create_advanced_model_target_dataset() -> Response: dataset_type=ModelTargetDatasetType.ADVANCED, generation_failure=None, generation_warning=None, + training_allowance_exceeded=( + settings.model_target_training_allowance_exceeded + ), ), ) diff --git a/src/mock_vws/_model_target_web_api.py b/src/mock_vws/_model_target_web_api.py index c869fef82..c6084486c 100644 --- a/src/mock_vws/_model_target_web_api.py +++ b/src/mock_vws/_model_target_web_api.py @@ -1364,6 +1364,7 @@ def create_model_target_dataset( dataset_type: ModelTargetDatasetType, generation_failure: ModelTargetGenerationFailure | None, generation_warning: ModelTargetGenerationWarning | None, + training_allowance_exceeded: bool, ) -> _ResponseType: """Create a standard or advanced Model Target dataset.""" content_length_error = _content_length_error(request=request) @@ -1395,12 +1396,20 @@ def create_model_target_dataset( if state_scope_error is not None: return state_scope_error - validation_error = _validate_dataset_request( + creation_error = _validate_dataset_request( request_json=request_json_or_error, dataset_type=dataset_type, ) - if validation_error is not None: - return validation_error + if creation_error is None and training_allowance_exceeded: + creation_error = _error_response( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + code="TRAINING_ALLOWANCE_EXCEEDED", + message="User has reached total number of allowed trainings", + target=_MOCK_USER_TARGET, + details=None, + ) + if creation_error is not None: + return creation_error dataset = ModelTargetDataset( request_body=request_json_or_error, diff --git a/src/mock_vws/_requests_mock_server/mock_web_services_api.py b/src/mock_vws/_requests_mock_server/mock_web_services_api.py index fe9de4fd4..2e095f6e0 100644 --- a/src/mock_vws/_requests_mock_server/mock_web_services_api.py +++ b/src/mock_vws/_requests_mock_server/mock_web_services_api.py @@ -145,6 +145,7 @@ def __init__( processing_time_seconds: float, model_target_generation_failure: (ModelTargetGenerationFailure | None), model_target_generation_warning: (ModelTargetGenerationWarning | None), + model_target_training_allowance_exceeded: bool, duplicate_match_checker: ImageMatcher, target_tracking_rater: TargetTrackingRater, vumark_generation_failure: VuMarkGenerationFailure | None, @@ -162,6 +163,9 @@ def __init__( after Model Target dataset processing completes. model_target_generation_warning: A configured warning returned after Model Target dataset processing completes. + model_target_training_allowance_exceeded: Whether Model Target + dataset creation is rejected because the account has no + training allowance remaining. duplicate_match_checker: A callable which takes two image values and returns whether they are duplicates. @@ -179,6 +183,9 @@ def __init__( self._processing_time_seconds = processing_time_seconds self._model_target_generation_failure = model_target_generation_failure self._model_target_generation_warning = model_target_generation_warning + self._model_target_training_allowance_exceeded = ( + model_target_training_allowance_exceeded + ) self._duplicate_match_checker = duplicate_match_checker self._target_tracking_rater = target_tracking_rater self._vumark_generation_failure = vumark_generation_failure @@ -270,6 +277,9 @@ def create_standard_model_target_dataset( dataset_type=ModelTargetDatasetType.STANDARD, generation_failure=self._model_target_generation_failure, generation_warning=self._model_target_generation_warning, + training_allowance_exceeded=( + self._model_target_training_allowance_exceeded + ), ) @route( @@ -288,6 +298,9 @@ def create_advanced_model_target_dataset( dataset_type=ModelTargetDatasetType.ADVANCED, generation_failure=self._model_target_generation_failure, generation_warning=self._model_target_generation_warning, + training_allowance_exceeded=( + self._model_target_training_allowance_exceeded + ), ) @route( diff --git a/src/mock_vws/decorators.py b/src/mock_vws/decorators.py index e69972d1d..d9df3bb4b 100644 --- a/src/mock_vws/decorators.py +++ b/src/mock_vws/decorators.py @@ -70,6 +70,7 @@ def __init__( model_target_generation_warning: ( ModelTargetGenerationWarning | None ) = None, + model_target_training_allowance_exceeded: bool = False, target_tracking_rater: TargetTrackingRater = _BRISQUE_TRACKING_RATER, real_http: bool = False, response_delay_seconds: float = 0.0, @@ -96,6 +97,10 @@ def __init__( Model Target dataset finishes processing. By default, Model Target datasets finish without warnings. This cannot be combined with ``model_target_generation_failure``. + model_target_training_allowance_exceeded: Whether Model Target + dataset creation returns Vuforia's + ``TRAINING_ALLOWANCE_EXCEEDED`` response. By default, creation + is allowed. base_vwq_url: The base URL for the VWQ API. base_vws_url: The base URL for the VWS API. cloud_query_failure_response: A response to return for every Cloud @@ -153,6 +158,9 @@ def __init__( processing_time_seconds=float(processing_time_seconds), model_target_generation_failure=model_target_generation_failure, model_target_generation_warning=model_target_generation_warning, + model_target_training_allowance_exceeded=( + model_target_training_allowance_exceeded + ), duplicate_match_checker=duplicate_match_checker, target_tracking_rater=target_tracking_rater, vumark_generation_failure=vumark_generation_failure, diff --git a/tests/mock_vws/test_model_target_training_allowance.py b/tests/mock_vws/test_model_target_training_allowance.py new file mode 100644 index 000000000..db0174741 --- /dev/null +++ b/tests/mock_vws/test_model_target_training_allowance.py @@ -0,0 +1,96 @@ +"""Tests for exhausted Model Target training allowance responses.""" + +from http import HTTPStatus +from typing import Any + +import pytest +import requests + +from mock_vws import MockVWS +from mock_vws._flask_server.vws import VWS_FLASK_APP + +_AUTHORIZATION = ( + "Bearer eyJhbGciOiJtb2NrIn0." + "eyJzY29wZSI6Im1vZGVsdGFyZ2V0cy5hbGwifQ." + "c2lnbmF0dXJl" +) +_REQUEST_BODY: dict[str, Any] = { + "name": "dataset-name", + "targetSdk": "10.18", + "models": [ + { + "name": "model-name", + "cadDataUrl": "https://example.com/model.glb", + "views": [ + { + "name": "view-name", + "guideViewPosition": { + "translation": [0, 0, 5], + "rotation": [0, 0, 0, 1], + }, + }, + ], + }, + ], +} +_EXPECTED_BODY = { + "error": { + "code": "TRAINING_ALLOWANCE_EXCEEDED", + "message": "User has reached total number of allowed trainings", + "target": "userId:mock", + }, +} + + +@pytest.mark.parametrize( + argnames="dataset_path", + argvalues=[ + pytest.param("modeltargets/datasets", id="standard"), + pytest.param("modeltargets/advancedDatasets", id="advanced"), + ], +) +def test_requests_mock_training_allowance_exceeded(dataset_path: str) -> None: + """The in-process mock can reject creation when allowance is spent.""" + with MockVWS(model_target_training_allowance_exceeded=True): + response = requests.post( + url=f"https://vws.vuforia.com/{dataset_path}", + headers={"Authorization": _AUTHORIZATION}, + json=_REQUEST_BODY, + timeout=30, + ) + + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert response.json() == _EXPECTED_BODY + + +@pytest.mark.parametrize( + argnames="dataset_path", + argvalues=[ + pytest.param("/modeltargets/datasets", id="standard"), + pytest.param("/modeltargets/advancedDatasets", id="advanced"), + ], +) +def test_flask_training_allowance_exceeded( + *, + dataset_path: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The Flask mock supports the exhausted allowance configuration.""" + monkeypatch.setenv( + name="MODEL_TARGET_TRAINING_ALLOWANCE_EXCEEDED", + value="true", + ) + monkeypatch.setenv( + name="TARGET_MANAGER_BASE_URL", + value="http://target-manager.example.com", + ) + + with VWS_FLASK_APP.test_client() as client: + response = client.post( + path=dataset_path, + headers={"Authorization": _AUTHORIZATION}, + json=_REQUEST_BODY, + ) + + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert response.json == _EXPECTED_BODY