Skip to content

Commit ada7f78

Browse files
committed
Add Model Target Web API mock
1 parent 351b7ef commit ada7f78

18 files changed

Lines changed: 1430 additions & 4 deletions

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ jobs:
115115
- tests/mock_vws/test_requests_mock_usage.py
116116
- tests/mock_vws/test_respx_mock_usage.py
117117
- tests/mock_vws/test_flask_app_usage.py
118+
- tests/mock_vws/test_model_target_web_api.py
118119
- tests/mock_vws/test_vumark_generation_api.py
119120
- tests/mock_vws/test_target_validators.py
120121
- tests/mock_vws/test_docker.py

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ VWS Mock
66
.. contents::
77
:local:
88

9-
Mock for the Vuforia Web Services (VWS) API and the Vuforia Web Query API.
9+
Mock for the Vuforia Web Services (VWS) API, the Vuforia Web Query API, and the Model Target Web API.
1010

1111
Mocking calls made to Vuforia
1212
------------------------------

admin/create_secrets_files.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
import vws_web_tools
1313
from selenium.common.exceptions import TimeoutException
1414
from selenium.webdriver.remote.webdriver import WebDriver
15-
from vws_web_tools import DatabaseDict, VuMarkDatabaseDict
15+
from vws_web_tools import (
16+
DatabaseDict,
17+
ModelTargetWebAPIDict,
18+
VuMarkDatabaseDict,
19+
)
1620

1721
VUMARK_TEMPLATE_SVG_FILE_PATH = Path(__file__).with_name(
1822
name="vumark_template.svg",
@@ -77,6 +81,7 @@ def _generate_secrets_file_content(
7781
inactive_database_details: DatabaseDict,
7882
inactive_vumark_details: VuMarkDatabaseDict,
7983
vumark_target_id: str,
84+
model_target_web_api_details: ModelTargetWebAPIDict,
8085
) -> str:
8186
"""Generate the content of a secrets file."""
8287
return textwrap.dedent(
@@ -101,6 +106,10 @@ def _generate_secrets_file_content(
101106
INACTIVE_VUMARK_VUFORIA_TARGET_MANAGER_DATABASE_NAME={inactive_vumark_details["database_name"]}
102107
INACTIVE_VUMARK_VUFORIA_SERVER_ACCESS_KEY={inactive_vumark_details["server_access_key"]}
103108
INACTIVE_VUMARK_VUFORIA_SERVER_SECRET_KEY={inactive_vumark_details["server_secret_key"]}
109+
110+
MODEL_TARGET_VUFORIA_CLIENT_ID={model_target_web_api_details["client_id"]}
111+
MODEL_TARGET_VUFORIA_CLIENT_SECRET={model_target_web_api_details["client_secret"]}
112+
MODEL_TARGET_VUFORIA_CAD_DATA_URL={model_target_web_api_details["cad_data_url"]}
104113
""",
105114
)
106115

@@ -193,6 +202,21 @@ def _create_and_get_inactive_vumark_details(
193202
return vumark_database_details
194203

195204

205+
def _get_model_target_web_api_details(
206+
driver: WebDriver,
207+
email_address: str,
208+
password: str,
209+
) -> ModelTargetWebAPIDict:
210+
"""Get credentials and input data for the Model Target Web API."""
211+
vws_web_tools.log_in(
212+
driver=driver,
213+
email_address=email_address,
214+
password=password,
215+
)
216+
vws_web_tools.wait_for_logged_in(driver=driver)
217+
return vws_web_tools.get_model_target_web_api_details(driver=driver)
218+
219+
196220
def _create_vuforia_resource_names() -> tuple[str, str, str, str]:
197221
"""Create names for Vuforia resources."""
198222
time = datetime.datetime.now(tz=datetime.UTC).strftime(
@@ -236,6 +260,14 @@ def main() -> None:
236260
)
237261
inactive_vumark_driver.quit()
238262

263+
model_target_driver = vws_web_tools.create_chrome_driver()
264+
model_target_web_api_details = _get_model_target_web_api_details(
265+
driver=model_target_driver,
266+
email_address=email_address,
267+
password=password,
268+
)
269+
model_target_driver.quit()
270+
239271
num_databases = 100
240272
required_files = [
241273
(new_secrets_dir / f"vuforia_secrets_{i}.env")
@@ -291,6 +323,7 @@ def main() -> None:
291323
inactive_database_details=inactive_database_details,
292324
inactive_vumark_details=inactive_vumark_details,
293325
vumark_target_id=vumark_target_id,
326+
model_target_web_api_details=model_target_web_api_details,
294327
)
295328
file.write_text(data=file_contents)
296329
sys.stdout.write(f"Created database {file.name}\n")

docs/source/differences-to-vws.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ The mock returns a fixed minimal image in the requested format.
110110
The ``instance_id`` value is not encoded into the response image.
111111
Real Vuforia encodes the instance ID into the VuMark pattern.
112112

113+
Model Target datasets
114+
---------------------
115+
116+
The Model Target Web API mock supports OAuth2 token requests, standard and advanced dataset creation, status polling, dataset downloads, and deletion.
117+
The generated dataset download is a small valid zip file containing request metadata, not a real Vuforia Engine Model Target dataset.
118+
Model Target API routes accept any non-empty bearer token.
119+
113120
Header cases
114121
------------
115122

newsfragments/2114.change

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a mock implementation of the Model Target Web API, including OAuth2 token creation, standard and advanced dataset creation, status polling, dataset download, and deletion.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ optional-dependencies.dev = [
111111
"vulture==2.16",
112112
"vws-python==2026.2.25.1",
113113
"vws-test-fixtures==2023.3.5",
114-
"vws-web-tools==2026.2.22.1",
114+
"vws-web-tools==2026.5.21",
115115
"yamlfix==1.19.1",
116116
"zizmor==1.25.2",
117117
]

src/mock_vws/_flask_server/vws.py

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@
2727
TargetStatuses,
2828
)
2929
from mock_vws._database_matchers import get_database_matching_server_keys
30-
from mock_vws._mock_common import json_dump
30+
from mock_vws._mock_common import RequestData, json_dump
31+
from mock_vws._model_target_web_api import (
32+
create_model_target_dataset,
33+
delete_model_target_dataset,
34+
download_model_target_dataset,
35+
get_model_target_dataset_status,
36+
)
37+
from mock_vws._model_target_web_api import (
38+
oauth2_token as model_target_oauth2_token,
39+
)
3140
from mock_vws._services_validators import run_services_validators
3241
from mock_vws._services_validators.exceptions import (
3342
FailError,
@@ -44,7 +53,9 @@
4453
ImageMatcher,
4554
StructuralSimilarityMatcher,
4655
)
56+
from mock_vws.model_target import ModelTargetDatasetType
4757
from mock_vws.target import ImageTarget
58+
from mock_vws.target_manager import TargetManager
4859
from mock_vws.target_raters import (
4960
HardcodedTargetTrackingRater,
5061
)
@@ -117,6 +128,36 @@ def get_all_vumark_databases() -> set[VuMarkDatabase]:
117128
}
118129

119130

131+
@beartype
132+
def _flask_request_data() -> RequestData:
133+
"""Return the current Flask request as shared request data."""
134+
return RequestData(
135+
method=request.method,
136+
path=request.path,
137+
headers=dict(request.headers),
138+
body=request.data,
139+
)
140+
141+
142+
@beartype
143+
def _model_target_manager() -> TargetManager:
144+
"""Return the target manager backing the Flask app."""
145+
from mock_vws._flask_server.target_manager import ( # noqa: PLC0415
146+
TARGET_MANAGER,
147+
)
148+
149+
return TARGET_MANAGER
150+
151+
152+
@beartype
153+
def _to_flask_response(
154+
api_response: tuple[int, dict[str, str], str | bytes],
155+
) -> Response:
156+
"""Convert a shared API response to a Flask response."""
157+
status_code, headers, body = api_response
158+
return Response(response=body, status=status_code, headers=headers)
159+
160+
120161
@VWS_FLASK_APP.before_request
121162
@beartype
122163
def set_terminate_wsgi_input() -> None:
@@ -154,6 +195,10 @@ def validate_request() -> None:
154195
"""
155196
if request.endpoint == "generate_vumark_instance":
156197
return
198+
if request.path == "/oauth2/token" or request.path.startswith(
199+
"/modeltargets/",
200+
):
201+
return
157202
run_services_validators(
158203
request_headers=dict(request.headers),
159204
request_body=request.data,
@@ -187,6 +232,157 @@ def handle_exceptions(exc: ValidatorError) -> Response:
187232
return response
188233

189234

235+
@VWS_FLASK_APP.route(rule="/oauth2/token", methods=[HTTPMethod.POST])
236+
@beartype
237+
def oauth2_token() -> Response:
238+
"""Obtain an OAuth2 token for the Model Target Web API."""
239+
return _to_flask_response(
240+
api_response=model_target_oauth2_token(
241+
request=_flask_request_data(),
242+
),
243+
)
244+
245+
246+
@VWS_FLASK_APP.route(
247+
rule="/modeltargets/datasets",
248+
methods=[HTTPMethod.POST],
249+
)
250+
@beartype
251+
def create_standard_model_target_dataset() -> Response:
252+
"""Create a standard Model Target dataset."""
253+
settings = VWSSettings.model_validate(obj={})
254+
return _to_flask_response(
255+
api_response=create_model_target_dataset(
256+
request=_flask_request_data(),
257+
target_manager=_model_target_manager(),
258+
processing_time_seconds=settings.processing_time_seconds,
259+
dataset_type=ModelTargetDatasetType.STANDARD,
260+
),
261+
)
262+
263+
264+
@VWS_FLASK_APP.route(
265+
rule="/modeltargets/advancedDatasets",
266+
methods=[HTTPMethod.POST],
267+
)
268+
@beartype
269+
def create_advanced_model_target_dataset() -> Response:
270+
"""Create an advanced Model Target dataset."""
271+
settings = VWSSettings.model_validate(obj={})
272+
return _to_flask_response(
273+
api_response=create_model_target_dataset(
274+
request=_flask_request_data(),
275+
target_manager=_model_target_manager(),
276+
processing_time_seconds=settings.processing_time_seconds,
277+
dataset_type=ModelTargetDatasetType.ADVANCED,
278+
),
279+
)
280+
281+
282+
@VWS_FLASK_APP.route(
283+
rule="/modeltargets/datasets/<string:dataset_uuid>/status",
284+
methods=[HTTPMethod.GET],
285+
)
286+
@beartype
287+
def get_standard_model_target_dataset_status(
288+
dataset_uuid: str,
289+
) -> Response:
290+
"""Return a standard Model Target dataset creation status."""
291+
return _to_flask_response(
292+
api_response=get_model_target_dataset_status(
293+
request=_flask_request_data(),
294+
target_manager=_model_target_manager(),
295+
dataset_uuid=dataset_uuid,
296+
),
297+
)
298+
299+
300+
@VWS_FLASK_APP.route(
301+
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>/status",
302+
methods=[HTTPMethod.GET],
303+
)
304+
@beartype
305+
def get_advanced_model_target_dataset_status(
306+
dataset_uuid: str,
307+
) -> Response:
308+
"""Return an advanced Model Target dataset creation status."""
309+
return _to_flask_response(
310+
api_response=get_model_target_dataset_status(
311+
request=_flask_request_data(),
312+
target_manager=_model_target_manager(),
313+
dataset_uuid=dataset_uuid,
314+
),
315+
)
316+
317+
318+
@VWS_FLASK_APP.route(
319+
rule="/modeltargets/datasets/<string:dataset_uuid>/dataset",
320+
methods=[HTTPMethod.GET],
321+
)
322+
@beartype
323+
def download_standard_model_target_dataset(
324+
dataset_uuid: str,
325+
) -> Response:
326+
"""Download a standard Model Target dataset."""
327+
return _to_flask_response(
328+
api_response=download_model_target_dataset(
329+
request=_flask_request_data(),
330+
target_manager=_model_target_manager(),
331+
dataset_uuid=dataset_uuid,
332+
),
333+
)
334+
335+
336+
@VWS_FLASK_APP.route(
337+
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>/dataset",
338+
methods=[HTTPMethod.GET],
339+
)
340+
@beartype
341+
def download_advanced_model_target_dataset(
342+
dataset_uuid: str,
343+
) -> Response:
344+
"""Download an advanced Model Target dataset."""
345+
return _to_flask_response(
346+
api_response=download_model_target_dataset(
347+
request=_flask_request_data(),
348+
target_manager=_model_target_manager(),
349+
dataset_uuid=dataset_uuid,
350+
),
351+
)
352+
353+
354+
@VWS_FLASK_APP.route(
355+
rule="/modeltargets/datasets/<string:dataset_uuid>",
356+
methods=[HTTPMethod.DELETE],
357+
)
358+
@beartype
359+
def delete_standard_model_target_dataset(dataset_uuid: str) -> Response:
360+
"""Delete a standard Model Target dataset."""
361+
return _to_flask_response(
362+
api_response=delete_model_target_dataset(
363+
request=_flask_request_data(),
364+
target_manager=_model_target_manager(),
365+
dataset_uuid=dataset_uuid,
366+
),
367+
)
368+
369+
370+
@VWS_FLASK_APP.route(
371+
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>",
372+
methods=[HTTPMethod.DELETE],
373+
)
374+
@beartype
375+
def delete_advanced_model_target_dataset(dataset_uuid: str) -> Response:
376+
"""Delete an advanced Model Target dataset."""
377+
return _to_flask_response(
378+
api_response=delete_model_target_dataset(
379+
request=_flask_request_data(),
380+
target_manager=_model_target_manager(),
381+
dataset_uuid=dataset_uuid,
382+
),
383+
)
384+
385+
190386
@VWS_FLASK_APP.route(rule="/targets", methods=[HTTPMethod.POST])
191387
@beartype
192388
def add_target() -> Response:

0 commit comments

Comments
 (0)