Skip to content

Commit 26277db

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

23 files changed

Lines changed: 1682 additions & 6 deletions

.github/workflows/lint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ jobs:
4242
run: uv run --extra=dev prek run --all-files --hook-stage ${{ matrix.hook-stage }}
4343
--verbose
4444
env:
45+
# Avoid intermittent uv distribution cache rename failures while
46+
# prek installs hook environments on Windows.
47+
UV_NO_CACHE: '1'
4548
UV_PYTHON: ${{ matrix.python-version }}
4649

4750
- uses: pre-commit-ci/lite-action@v1.1.0

.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: 35 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",
@@ -72,11 +76,13 @@ def _create_and_get_vumark_details(
7276

7377

7478
def _generate_secrets_file_content(
79+
*,
7580
cloud_database_details: DatabaseDict,
7681
vumark_details: VuMarkDatabaseDict,
7782
inactive_database_details: DatabaseDict,
7883
inactive_vumark_details: VuMarkDatabaseDict,
7984
vumark_target_id: str,
85+
model_target_web_api_details: ModelTargetWebAPIDict,
8086
) -> str:
8187
"""Generate the content of a secrets file."""
8288
return textwrap.dedent(
@@ -101,6 +107,10 @@ def _generate_secrets_file_content(
101107
INACTIVE_VUMARK_VUFORIA_TARGET_MANAGER_DATABASE_NAME={inactive_vumark_details["database_name"]}
102108
INACTIVE_VUMARK_VUFORIA_SERVER_ACCESS_KEY={inactive_vumark_details["server_access_key"]}
103109
INACTIVE_VUMARK_VUFORIA_SERVER_SECRET_KEY={inactive_vumark_details["server_secret_key"]}
110+
111+
MODEL_TARGET_VUFORIA_CLIENT_ID={model_target_web_api_details["client_id"]}
112+
MODEL_TARGET_VUFORIA_CLIENT_SECRET={model_target_web_api_details["client_secret"]}
113+
MODEL_TARGET_VUFORIA_CAD_DATA_URL={model_target_web_api_details["cad_data_url"]}
104114
""",
105115
)
106116

@@ -193,6 +203,21 @@ def _create_and_get_inactive_vumark_details(
193203
return vumark_database_details
194204

195205

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

264+
model_target_driver = vws_web_tools.create_chrome_driver()
265+
model_target_web_api_details = _get_model_target_web_api_details(
266+
driver=model_target_driver,
267+
email_address=email_address,
268+
password=password,
269+
)
270+
model_target_driver.quit()
271+
239272
num_databases = 100
240273
required_files = [
241274
(new_secrets_dir / f"vuforia_secrets_{i}.env")
@@ -291,6 +324,7 @@ def main() -> None:
291324
inactive_database_details=inactive_database_details,
292325
inactive_vumark_details=inactive_vumark_details,
293326
vumark_target_id=vumark_target_id,
327+
model_target_web_api_details=model_target_web_api_details,
294328
)
295329
file.write_text(data=file_contents)
296330
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
]

secrets.tar.gpg

1.09 KB
Binary file not shown.

spelling_private_dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ KiB
22
MPixel
33
MiB
44
MissingSchema
5+
OAuth
56
Ubuntu
67
VuMark
78
admin

src/mock_vws/_flask_server/vws.py

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,17 @@
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._flask_server.target_manager import TARGET_MANAGER
31+
from mock_vws._mock_common import RequestData, json_dump
32+
from mock_vws._model_target_web_api import (
33+
create_model_target_dataset,
34+
delete_model_target_dataset,
35+
download_model_target_dataset,
36+
get_model_target_dataset_status,
37+
)
38+
from mock_vws._model_target_web_api import (
39+
oauth2_token as model_target_oauth2_token,
40+
)
3141
from mock_vws._services_validators import run_services_validators
3242
from mock_vws._services_validators.exceptions import (
3343
FailError,
@@ -44,7 +54,9 @@
4454
ImageMatcher,
4555
StructuralSimilarityMatcher,
4656
)
57+
from mock_vws.model_target import ModelTargetDatasetType
4758
from mock_vws.target import ImageTarget
59+
from mock_vws.target_manager import TargetManager
4860
from mock_vws.target_raters import (
4961
HardcodedTargetTrackingRater,
5062
)
@@ -117,6 +129,32 @@ def get_all_vumark_databases() -> set[VuMarkDatabase]:
117129
}
118130

119131

132+
@beartype
133+
def _flask_request_data() -> RequestData:
134+
"""Return the current Flask request as shared request data."""
135+
return RequestData(
136+
method=request.method,
137+
path=request.path,
138+
headers=dict(request.headers),
139+
body=request.data,
140+
)
141+
142+
143+
@beartype
144+
def _model_target_manager() -> TargetManager:
145+
"""Return the target manager backing the Flask app."""
146+
return TARGET_MANAGER
147+
148+
149+
@beartype
150+
def _to_flask_response(
151+
api_response: tuple[int, dict[str, str], str | bytes],
152+
) -> Response:
153+
"""Convert a shared API response to a Flask response."""
154+
status_code, headers, body = api_response
155+
return Response(response=body, status=status_code, headers=headers)
156+
157+
120158
@VWS_FLASK_APP.before_request
121159
@beartype
122160
def set_terminate_wsgi_input() -> None:
@@ -154,6 +192,10 @@ def validate_request() -> None:
154192
"""
155193
if request.endpoint == "generate_vumark_instance":
156194
return
195+
if request.path == "/oauth2/token" or request.path.startswith(
196+
"/modeltargets/",
197+
):
198+
return
157199
run_services_validators(
158200
request_headers=dict(request.headers),
159201
request_body=request.data,
@@ -187,6 +229,157 @@ def handle_exceptions(exc: ValidatorError) -> Response:
187229
return response
188230

189231

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

0 commit comments

Comments
 (0)