Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
run: uv run --extra=dev prek run --all-files --hook-stage ${{ matrix.hook-stage }}
--verbose
env:
# Avoid intermittent uv distribution cache rename failures while
# prek installs hook environments on Windows.
UV_NO_CACHE: '1'
UV_PYTHON: ${{ matrix.python-version }}

- uses: pre-commit-ci/lite-action@v1.1.0
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ jobs:
- tests/mock_vws/test_requests_mock_usage.py
- tests/mock_vws/test_respx_mock_usage.py
- tests/mock_vws/test_flask_app_usage.py
- tests/mock_vws/test_model_target_web_api.py
- tests/mock_vws/test_vumark_generation_api.py
- tests/mock_vws/test_target_validators.py
- tests/mock_vws/test_docker.py
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ VWS Mock
.. contents::
:local:

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

Mocking calls made to Vuforia
------------------------------
Expand Down
36 changes: 35 additions & 1 deletion admin/create_secrets_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import vws_web_tools
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.remote.webdriver import WebDriver
from vws_web_tools import DatabaseDict, VuMarkDatabaseDict
from vws_web_tools import (
DatabaseDict,
ModelTargetWebAPIDict,
VuMarkDatabaseDict,
)

VUMARK_TEMPLATE_SVG_FILE_PATH = Path(__file__).with_name(
name="vumark_template.svg",
Expand Down Expand Up @@ -72,11 +76,13 @@ def _create_and_get_vumark_details(


def _generate_secrets_file_content(
*,
cloud_database_details: DatabaseDict,
vumark_details: VuMarkDatabaseDict,
inactive_database_details: DatabaseDict,
inactive_vumark_details: VuMarkDatabaseDict,
vumark_target_id: str,
model_target_web_api_details: ModelTargetWebAPIDict,
) -> str:
"""Generate the content of a secrets file."""
return textwrap.dedent(
Expand All @@ -101,6 +107,10 @@ def _generate_secrets_file_content(
INACTIVE_VUMARK_VUFORIA_TARGET_MANAGER_DATABASE_NAME={inactive_vumark_details["database_name"]}
INACTIVE_VUMARK_VUFORIA_SERVER_ACCESS_KEY={inactive_vumark_details["server_access_key"]}
INACTIVE_VUMARK_VUFORIA_SERVER_SECRET_KEY={inactive_vumark_details["server_secret_key"]}

MODEL_TARGET_VUFORIA_CLIENT_ID={model_target_web_api_details["client_id"]}
MODEL_TARGET_VUFORIA_CLIENT_SECRET={model_target_web_api_details["client_secret"]}
MODEL_TARGET_VUFORIA_CAD_DATA_URL={model_target_web_api_details["cad_data_url"]}
""",
)

Expand Down Expand Up @@ -193,6 +203,21 @@ def _create_and_get_inactive_vumark_details(
return vumark_database_details


def _get_model_target_web_api_details(
driver: WebDriver,
email_address: str,
password: str,
) -> ModelTargetWebAPIDict:
"""Get credentials and input data for the Model Target Web API."""
vws_web_tools.log_in(
driver=driver,
email_address=email_address,
password=password,
)
vws_web_tools.wait_for_logged_in(driver=driver)
return vws_web_tools.get_model_target_web_api_details(driver=driver)


def _create_vuforia_resource_names() -> tuple[str, str, str, str]:
"""Create names for Vuforia resources."""
time = datetime.datetime.now(tz=datetime.UTC).strftime(
Expand Down Expand Up @@ -236,6 +261,14 @@ def main() -> None:
)
inactive_vumark_driver.quit()

model_target_driver = vws_web_tools.create_chrome_driver()
model_target_web_api_details = _get_model_target_web_api_details(
driver=model_target_driver,
email_address=email_address,
password=password,
)
model_target_driver.quit()

num_databases = 100
required_files = [
(new_secrets_dir / f"vuforia_secrets_{i}.env")
Expand Down Expand Up @@ -291,6 +324,7 @@ def main() -> None:
inactive_database_details=inactive_database_details,
inactive_vumark_details=inactive_vumark_details,
vumark_target_id=vumark_target_id,
model_target_web_api_details=model_target_web_api_details,
)
file.write_text(data=file_contents)
sys.stdout.write(f"Created database {file.name}\n")
Expand Down
7 changes: 7 additions & 0 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ The mock returns a fixed minimal image in the requested format.
The ``instance_id`` value is not encoded into the response image.
Real Vuforia encodes the instance ID into the VuMark pattern.

Model Target datasets
---------------------

The Model Target Web API mock supports OAuth2 token requests, standard and advanced dataset creation, status polling, dataset downloads, and deletion.
The generated dataset download is a small valid zip file containing request metadata, not a real Vuforia Engine Model Target dataset.
Model Target API routes accept any non-empty bearer token.

Header cases
------------

Expand Down
1 change: 1 addition & 0 deletions newsfragments/2114.change
Original file line number Diff line number Diff line change
@@ -0,0 +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.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ optional-dependencies.dev = [
"vulture==2.16",
"vws-python==2026.2.25.1",
"vws-test-fixtures==2023.3.5",
"vws-web-tools==2026.2.22.1",
"vws-web-tools==2026.5.21",
"yamlfix==1.19.1",
"zizmor==1.25.2",
]
Expand Down
Binary file modified secrets.tar.gpg
Binary file not shown.
1 change: 1 addition & 0 deletions spelling_private_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ KiB
MPixel
MiB
MissingSchema
OAuth
Ubuntu
VuMark
admin
Expand Down
195 changes: 194 additions & 1 deletion src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@
TargetStatuses,
)
from mock_vws._database_matchers import get_database_matching_server_keys
from mock_vws._mock_common import json_dump
from mock_vws._flask_server.target_manager import TARGET_MANAGER
from mock_vws._mock_common import RequestData, json_dump
from mock_vws._model_target_web_api import (
create_model_target_dataset,
delete_model_target_dataset,
download_model_target_dataset,
get_model_target_dataset_status,
)
from mock_vws._model_target_web_api import (
oauth2_token as model_target_oauth2_token,
)
from mock_vws._services_validators import run_services_validators
from mock_vws._services_validators.exceptions import (
FailError,
Expand All @@ -44,7 +54,9 @@
ImageMatcher,
StructuralSimilarityMatcher,
)
from mock_vws.model_target import ModelTargetDatasetType
from mock_vws.target import ImageTarget
from mock_vws.target_manager import TargetManager
from mock_vws.target_raters import (
HardcodedTargetTrackingRater,
)
Expand Down Expand Up @@ -117,6 +129,32 @@ def get_all_vumark_databases() -> set[VuMarkDatabase]:
}


@beartype
def _flask_request_data() -> RequestData:
"""Return the current Flask request as shared request data."""
return RequestData(
method=request.method,
path=request.path,
headers=dict(request.headers),
body=request.data,
)


@beartype
def _model_target_manager() -> TargetManager:
"""Return the target manager backing the Flask app."""
return TARGET_MANAGER


@beartype
def _to_flask_response(
api_response: tuple[int, dict[str, str], str | bytes],
) -> Response:
"""Convert a shared API response to a Flask response."""
status_code, headers, body = api_response
return Response(response=body, status=status_code, headers=headers)


@VWS_FLASK_APP.before_request
@beartype
def set_terminate_wsgi_input() -> None:
Expand Down Expand Up @@ -154,6 +192,10 @@ def validate_request() -> None:
"""
if request.endpoint == "generate_vumark_instance":
return
if request.path == "/oauth2/token" or request.path.startswith(
"/modeltargets/",
):
return
run_services_validators(
request_headers=dict(request.headers),
request_body=request.data,
Expand Down Expand Up @@ -187,6 +229,157 @@ def handle_exceptions(exc: ValidatorError) -> Response:
return response


@VWS_FLASK_APP.route(rule="/oauth2/token", methods=[HTTPMethod.POST])
@beartype
def oauth2_token() -> Response:
"""Obtain an OAuth2 token for the Model Target Web API."""
return _to_flask_response(
api_response=model_target_oauth2_token(
request=_flask_request_data(),
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/datasets",
methods=[HTTPMethod.POST],
)
@beartype
def create_standard_model_target_dataset() -> Response:
"""Create a standard Model Target dataset."""
settings = VWSSettings.model_validate(obj={})
return _to_flask_response(
api_response=create_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
processing_time_seconds=settings.processing_time_seconds,
dataset_type=ModelTargetDatasetType.STANDARD,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/advancedDatasets",
methods=[HTTPMethod.POST],
)
@beartype
def create_advanced_model_target_dataset() -> Response:
"""Create an advanced Model Target dataset."""
settings = VWSSettings.model_validate(obj={})
return _to_flask_response(
api_response=create_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
processing_time_seconds=settings.processing_time_seconds,
dataset_type=ModelTargetDatasetType.ADVANCED,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/datasets/<string:dataset_uuid>/status",
methods=[HTTPMethod.GET],
)
@beartype
def get_standard_model_target_dataset_status(
dataset_uuid: str,
) -> Response:
"""Return a standard Model Target dataset creation status."""
return _to_flask_response(
api_response=get_model_target_dataset_status(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>/status",
methods=[HTTPMethod.GET],
)
@beartype
def get_advanced_model_target_dataset_status(
dataset_uuid: str,
) -> Response:
"""Return an advanced Model Target dataset creation status."""
return _to_flask_response(
api_response=get_model_target_dataset_status(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/datasets/<string:dataset_uuid>/dataset",
methods=[HTTPMethod.GET],
)
@beartype
def download_standard_model_target_dataset(
dataset_uuid: str,
) -> Response:
"""Download a standard Model Target dataset."""
return _to_flask_response(
api_response=download_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>/dataset",
methods=[HTTPMethod.GET],
)
@beartype
def download_advanced_model_target_dataset(
dataset_uuid: str,
) -> Response:
"""Download an advanced Model Target dataset."""
return _to_flask_response(
api_response=download_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/datasets/<string:dataset_uuid>",
methods=[HTTPMethod.DELETE],
)
@beartype
def delete_standard_model_target_dataset(dataset_uuid: str) -> Response:
"""Delete a standard Model Target dataset."""
return _to_flask_response(
api_response=delete_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(
rule="/modeltargets/advancedDatasets/<string:dataset_uuid>",
methods=[HTTPMethod.DELETE],
)
@beartype
def delete_advanced_model_target_dataset(dataset_uuid: str) -> Response:
"""Delete an advanced Model Target dataset."""
return _to_flask_response(
api_response=delete_model_target_dataset(
request=_flask_request_data(),
target_manager=_model_target_manager(),
dataset_uuid=dataset_uuid,
),
)


@VWS_FLASK_APP.route(rule="/targets", methods=[HTTPMethod.POST])
@beartype
def add_target() -> Response:
Expand Down
Loading
Loading