vws-python-mock 2026.8.4__tar.gz → 2026.8.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/test.yml +2 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/CHANGELOG.rst +16 -3
- {vws_python_mock-2026.8.4/src/vws_python_mock.egg-info → vws_python_mock-2026.8.4.2}/PKG-INFO +5 -7
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/differences-to-vws.rst +34 -3
- vws_python_mock-2026.8.4.2/docs/source/installation.rst +8 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/mock-api-reference.rst +8 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/pyproject.toml +4 -14
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/__init__.py +4 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/target_manager.py +12 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/vws.py +2 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_model_target_web_api.py +33 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/decorators.py +14 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/mock_web_query_api.py +14 -2
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/mock_web_services_api.py +36 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/__init__.py +14 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/exceptions.py +31 -0
- vws_python_mock-2026.8.4.2/src/mock_vws/_services_validators/request_rate_validators.py +83 -0
- vws_python_mock-2026.8.4.2/src/mock_vws/cloud_query.py +22 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/database.py +10 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/image_matchers.py +16 -47
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/model_target.py +3 -2
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target_manager.py +10 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target_raters.py +13 -16
- vws_python_mock-2026.8.4.2/src/mock_vws/vumark.py +23 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2/src/vws_python_mock.egg-info}/PKG-INFO +5 -7
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/SOURCES.txt +5 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/requires.txt +4 -6
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/scm_file_list.json +5 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/scm_version.json +2 -2
- vws_python_mock-2026.8.4.2/tests/mock_vws/test_cloud_query_failure_response.py +132 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_flask_app_usage.py +20 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_model_target_web_api.py +52 -1
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_requests_mock_usage.py +68 -3
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_respx_mock_usage.py +3 -2
- vws_python_mock-2026.8.4.2/tests/mock_vws/test_vumark_generation_failure.py +79 -0
- vws_python_mock-2026.8.4/docs/source/installation.rst +0 -34
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.git_archival.txt +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.gitattributes +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/dependabot.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/dependabot-merge.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/docker-build.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/lint.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/publish-site.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/release.yml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.gitignore +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.pre-commit-config.yaml +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.prettierrc +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.vscode/extensions.json +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.vscode/settings.json +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/CODE_OF_CONDUCT.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/LICENSE +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/MANIFEST.in +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/README.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/create_secrets_files.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/vumark_template.svg +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/decrypt_secret.sh +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/test_custom_linters.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/conftest.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docker-bake.hcl +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/basic-example.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/changelog.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/ci-setup.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/conf.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/contributing.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/docker.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/getting-started.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/httpx-example.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/index.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/release-process.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/unreleased.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/versioning-and-api-stability.rst +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/towncrier_template.rst.jinja +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/newsfragments/.gitkeep +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/secrets.tar.gpg +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/setup.cfg +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/spelling_private_dict.txt +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_base64_decoding.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_constants.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_database_matchers.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/Dockerfile +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/healthcheck.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/vwq.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_mock_common.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_tools.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/auth_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/date_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/exceptions.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/fields_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/image_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/include_target_data_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_respx_mock_server/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_respx_mock_server/decorators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/auth_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/date_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/image_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/json_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/key_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/name_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/request_quota_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/target_quota_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/target_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/width_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/database_type.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/py.typed +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/states.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/top_level.txt +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/conftest.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/credentials.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/prepared_requests.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_add_target.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_authorization_header.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_content_length.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_database_summary.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_date_header.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_delete_target.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_docker.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_get_duplicates.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_get_target.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_invalid_given_id.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_invalid_json.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_query.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_list.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_raters.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_summary.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_validators.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_unexpected_json.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_update_target.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_vumark_generation_api.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/__init__.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/assertions.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/retries.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/too_many_requests.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/vuforia_secrets.env.example +0 -0
- {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/zizmor.yml +0 -0
|
@@ -59,6 +59,7 @@ jobs:
|
|
|
59
59
|
- tests/mock_vws/test_authorization_header.py::TestMalformed::test_one_part_with_space
|
|
60
60
|
- tests/mock_vws/test_authorization_header.py::TestMalformed::test_missing_signature
|
|
61
61
|
- tests/mock_vws/test_authorization_header.py::TestBadKey
|
|
62
|
+
- tests/mock_vws/test_cloud_query_failure_response.py
|
|
62
63
|
- tests/mock_vws/test_content_length.py::TestIncorrect::test_not_integer
|
|
63
64
|
- tests/mock_vws/test_content_length.py::TestIncorrect::test_too_large
|
|
64
65
|
- tests/mock_vws/test_content_length.py::TestIncorrect::test_too_small
|
|
@@ -117,6 +118,7 @@ jobs:
|
|
|
117
118
|
- tests/mock_vws/test_flask_app_usage.py
|
|
118
119
|
- tests/mock_vws/test_model_target_web_api.py
|
|
119
120
|
- tests/mock_vws/test_vumark_generation_api.py
|
|
121
|
+
- tests/mock_vws/test_vumark_generation_failure.py
|
|
120
122
|
- tests/mock_vws/test_target_validators.py
|
|
121
123
|
- tests/mock_vws/test_docker.py
|
|
122
124
|
- ci/test_custom_linters.py
|
|
@@ -3,19 +3,27 @@ Changelog
|
|
|
3
3
|
|
|
4
4
|
.. towncrier release notes start
|
|
5
5
|
|
|
6
|
-
2026.08.04
|
|
7
|
-
|
|
6
|
+
2026.08.04.2
|
|
7
|
+
------------
|
|
8
|
+
|
|
9
|
+
- Replace the PyTorch image-quality stack with OpenCV and a lightweight BRISQUE
|
|
10
|
+
implementation. This reduces dependency download and installation sizes and
|
|
11
|
+
removes the need to configure PyTorch's CPU-only package index.
|
|
12
|
+
|
|
13
|
+
- Allow VuMark generation requests to be configured to return ``QuotaExceeded``, ``LicenseCheckFailed``, or ``AuthorizationFailed`` responses.
|
|
8
14
|
|
|
9
15
|
- Cloud databases with ``request_quota=0`` now return a
|
|
10
16
|
``RequestQuotaReached`` response from VWS endpoints.
|
|
11
17
|
|
|
12
18
|
- 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.
|
|
13
19
|
|
|
14
|
-
- Improve Model Target Web API mock authentication failure responses
|
|
20
|
+
- Improve Model Target Web API mock authentication failure responses, including
|
|
21
|
+
malformed and unsecured JSON Web Token headers.
|
|
15
22
|
|
|
16
23
|
- Match real Vuforia Model Target dataset creation validation error shape, including per-request UUID, details list, and status codes (415 for unsupported media type, 400 with ``BAD_REQUEST`` validation details).
|
|
17
24
|
|
|
18
25
|
- Match real Vuforia Model Target unknown-dataset response shape (``NOT_FOUND`` code, ``Could not find a model-view database with uuid <uuid>`` message, ``userId:`` target).
|
|
26
|
+
Keep each Model Target dataset status response internally consistent when processing completes while the response is being generated.
|
|
19
27
|
|
|
20
28
|
- Make synthetic Model Target dataset zip downloads byte-for-byte reproducible.
|
|
21
29
|
|
|
@@ -24,6 +32,11 @@ Changelog
|
|
|
24
32
|
- Add configurable ``TargetQuotaReached``, ``ProjectSuspended``, and
|
|
25
33
|
``ProjectHasNoAPIAccess`` responses from VWS endpoints.
|
|
26
34
|
|
|
35
|
+
- Add configurable ``TooManyRequests`` responses from VWS endpoints using the
|
|
36
|
+
``CloudDatabase.requests_per_second_limit`` setting.
|
|
37
|
+
|
|
38
|
+
- Add ``CloudQueryFailureResponse`` and the ``MockVWS.cloud_query_failure_response`` parameter for returning configurable Cloud Query failure status codes, headers, and raw bodies through the ``requests`` and ``httpx`` backends.
|
|
39
|
+
|
|
27
40
|
2026.04.26
|
|
28
41
|
----------
|
|
29
42
|
|
{vws_python_mock-2026.8.4/src/vws_python_mock.egg-info → vws_python_mock-2026.8.4.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vws-python-mock
|
|
3
|
-
Version: 2026.8.4
|
|
3
|
+
Version: 2026.8.4.2
|
|
4
4
|
Summary: A mock for the Vuforia Web Services (VWS) API.
|
|
5
5
|
Author-email: Adam Dangoor <adamdangoor@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,16 +20,14 @@ License-File: LICENSE
|
|
|
20
20
|
Requires-Dist: beartype>=0.22.9
|
|
21
21
|
Requires-Dist: flask>=3.0.3
|
|
22
22
|
Requires-Dist: httpx>=0.27.0
|
|
23
|
-
Requires-Dist: numpy>=
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: numpy>=2.4.4
|
|
24
|
+
Requires-Dist: opencv-contrib-python-headless>=5.0.0.93
|
|
25
|
+
Requires-Dist: pillow>=12.2.0
|
|
26
26
|
Requires-Dist: pydantic-settings>=2.6.1
|
|
27
|
+
Requires-Dist: pyteenybrisque>=0.1.1
|
|
27
28
|
Requires-Dist: requests>=2.32.3
|
|
28
29
|
Requires-Dist: responses>=0.25.3
|
|
29
30
|
Requires-Dist: respx>=0.21.0
|
|
30
|
-
Requires-Dist: torch>=2.5.1
|
|
31
|
-
Requires-Dist: torchmetrics>=1.5.1
|
|
32
|
-
Requires-Dist: torchvision>=0.20.1
|
|
33
31
|
Requires-Dist: tzdata; sys_platform == "win32"
|
|
34
32
|
Requires-Dist: vws-auth-tools>=2024.7.12
|
|
35
33
|
Requires-Dist: werkzeug>=3.1.2
|
|
@@ -92,7 +92,6 @@ There are some result codes which the mock cannot return.
|
|
|
92
92
|
These are:
|
|
93
93
|
|
|
94
94
|
* ``DateRangeError``
|
|
95
|
-
* ``TooManyRequests``
|
|
96
95
|
|
|
97
96
|
Request quota exhaustion
|
|
98
97
|
------------------------
|
|
@@ -103,10 +102,35 @@ The mock returns ``RequestQuotaReached`` when a
|
|
|
103
102
|
but the response has not been verified against a real database with an
|
|
104
103
|
exhausted quota.
|
|
105
104
|
|
|
105
|
+
Configurable Cloud Query failures
|
|
106
|
+
---------------------------------
|
|
107
|
+
|
|
108
|
+
The Vuforia Cloud Query API documents failure responses with JSON, arbitrary
|
|
109
|
+
content, or no body. Use
|
|
110
|
+
:paramref:`mock_vws.MockVWS.cloud_query_failure_response` to make every Cloud
|
|
111
|
+
Query request return a particular documented failure shape through the
|
|
112
|
+
in-process ``requests`` and ``httpx`` backends::
|
|
113
|
+
|
|
114
|
+
from mock_vws import CloudQueryFailureResponse, MockVWS
|
|
115
|
+
|
|
116
|
+
failure = CloudQueryFailureResponse(
|
|
117
|
+
status_code=503,
|
|
118
|
+
headers={"Content-Type": "text/plain", "Retry-After": "10"},
|
|
119
|
+
body=b"Temporarily unavailable",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
with MockVWS(cloud_query_failure_response=failure):
|
|
123
|
+
# Cloud Query calls return the configured response.
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
The configured response bypasses normal Cloud Query validation and image
|
|
127
|
+
matching. Omitting it preserves the normal successful-query behavior. This
|
|
128
|
+
configuration is not supported by the Flask/Docker backend.
|
|
129
|
+
|
|
106
130
|
Other configurable result codes
|
|
107
131
|
-------------------------------
|
|
108
132
|
|
|
109
|
-
The mock also supports
|
|
133
|
+
The mock also supports four other result codes which have not been verified
|
|
110
134
|
against real databases in the corresponding states:
|
|
111
135
|
|
|
112
136
|
* ``TargetQuotaReached`` is returned when adding a target to a
|
|
@@ -116,6 +140,11 @@ against real databases in the corresponding states:
|
|
|
116
140
|
:attr:`mock_vws.states.States.PROJECT_SUSPENDED` state.
|
|
117
141
|
* ``ProjectHasNoAPIAccess`` is returned by VWS endpoints when a database uses
|
|
118
142
|
the :attr:`mock_vws.states.States.PROJECT_HAS_NO_API_ACCESS` state.
|
|
143
|
+
* ``TooManyRequests`` is returned when a
|
|
144
|
+
:class:`mock_vws.database.CloudDatabase` exceeds its
|
|
145
|
+
``requests_per_second_limit``. Set the limit to ``0`` to return this result
|
|
146
|
+
code for every VWS request. By default, the mock does not apply a per-second
|
|
147
|
+
request limit.
|
|
119
148
|
|
|
120
149
|
``Content-Length`` headers
|
|
121
150
|
--------------------------
|
|
@@ -134,7 +163,9 @@ Model Target datasets
|
|
|
134
163
|
|
|
135
164
|
The Model Target Web API mock supports OAuth2 token requests, standard and advanced dataset creation, status polling, dataset downloads, and deletion.
|
|
136
165
|
The generated dataset download is a small valid zip file containing request metadata, not a real Vuforia Engine Model Target dataset.
|
|
137
|
-
Model Target API routes require a
|
|
166
|
+
Model Target API routes require a three-part JSON Web Token with a JSON object
|
|
167
|
+
header and a non-``none`` ``alg`` value, such as the token returned by the mock
|
|
168
|
+
OAuth2 route.
|
|
138
169
|
The mock does not verify token signatures, claims, expiry, or revocation.
|
|
139
170
|
|
|
140
171
|
For unknown Model Target datasets, the mock returns an error whose ``target`` is ``userId:mock``.
|
|
@@ -11,6 +11,14 @@ API Reference
|
|
|
11
11
|
:members:
|
|
12
12
|
:undoc-members:
|
|
13
13
|
|
|
14
|
+
.. autoclass:: mock_vws.CloudQueryFailureResponse(*, status_code, headers={}, body=b'')
|
|
15
|
+
:members:
|
|
16
|
+
:undoc-members:
|
|
17
|
+
|
|
18
|
+
.. autoclass:: mock_vws.VuMarkGenerationFailure
|
|
19
|
+
:members:
|
|
20
|
+
:undoc-members:
|
|
21
|
+
|
|
14
22
|
.. Many parts of the CloudDatabase API are used for the Flask target
|
|
15
23
|
.. database app, but Python users are not expected to use them.
|
|
16
24
|
.. Therefore, they are not documented.
|
|
@@ -37,16 +37,14 @@ dependencies = [
|
|
|
37
37
|
"beartype>=0.22.9",
|
|
38
38
|
"flask>=3.0.3",
|
|
39
39
|
"httpx>=0.27.0",
|
|
40
|
-
"numpy>=
|
|
41
|
-
"
|
|
42
|
-
"
|
|
40
|
+
"numpy>=2.4.4",
|
|
41
|
+
"opencv-contrib-python-headless>=5.0.0.93",
|
|
42
|
+
"pillow>=12.2.0",
|
|
43
43
|
"pydantic-settings>=2.6.1",
|
|
44
|
+
"pyteenybrisque>=0.1.1",
|
|
44
45
|
"requests>=2.32.3",
|
|
45
46
|
"responses>=0.25.3",
|
|
46
47
|
"respx>=0.21.0",
|
|
47
|
-
"torch>=2.5.1",
|
|
48
|
-
"torchmetrics>=1.5.1",
|
|
49
|
-
"torchvision>=0.20.1",
|
|
50
48
|
"tzdata; sys_platform=='win32'",
|
|
51
49
|
"vws-auth-tools>=2024.7.12",
|
|
52
50
|
"werkzeug>=3.1.2",
|
|
@@ -145,11 +143,6 @@ version_scheme = "post-release"
|
|
|
145
143
|
# This must be a PEP 440 compliant version.
|
|
146
144
|
fallback_version = "0.0.0"
|
|
147
145
|
|
|
148
|
-
[tool.uv]
|
|
149
|
-
sources.torch = { index = "pytorch-cpu" }
|
|
150
|
-
sources.torchvision = { index = "pytorch-cpu" }
|
|
151
|
-
index = [ { name = "pytorch-cpu", url = "https://download.pytorch.org/whl/cpu", explicit = true } ]
|
|
152
|
-
|
|
153
146
|
[tool.ruff]
|
|
154
147
|
line-length = 79
|
|
155
148
|
lint.select = [
|
|
@@ -352,9 +345,6 @@ per_rule_ignores.DEP002 = [
|
|
|
352
345
|
# tzdata is needed on Windows for zoneinfo to work.
|
|
353
346
|
# See https://docs.python.org/3/library/zoneinfo.html#data-sources.
|
|
354
347
|
"tzdata",
|
|
355
|
-
# torchvision is used transitively via piq, but must be a direct dependency
|
|
356
|
-
# so that tool.uv.sources can route it to the CPU-only PyTorch index.
|
|
357
|
-
"torchvision",
|
|
358
348
|
]
|
|
359
349
|
optional_dependencies_dev_groups = [
|
|
360
350
|
"dev",
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from mock_vws._mock_common import MissingSchemeError
|
|
4
4
|
from mock_vws._requests_mock_server.decorators import MockVWS
|
|
5
|
+
from mock_vws.cloud_query import CloudQueryFailureResponse
|
|
6
|
+
from mock_vws.vumark import VuMarkGenerationFailure
|
|
5
7
|
|
|
6
8
|
__all__ = [
|
|
9
|
+
"CloudQueryFailureResponse",
|
|
7
10
|
"MissingSchemeError",
|
|
8
11
|
"MockVWS",
|
|
12
|
+
"VuMarkGenerationFailure",
|
|
9
13
|
]
|
{vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/target_manager.py
RENAMED
|
@@ -162,6 +162,10 @@ def create_cloud_database() -> Response:
|
|
|
162
162
|
:reqjson int target_quota: (Optional) The target quota. Once this many
|
|
163
163
|
targets exist, adding another returns ``TargetQuotaReached``.
|
|
164
164
|
|
|
165
|
+
:reqjson int requests_per_second_limit: (Optional) The maximum number of
|
|
166
|
+
VWS requests accepted in a rolling one-second window. Set this to zero
|
|
167
|
+
to make VWS endpoints return ``TooManyRequests``.
|
|
168
|
+
|
|
165
169
|
:reqjson string server_access_key: (Optional) The server access key for the
|
|
166
170
|
cloud database.
|
|
167
171
|
|
|
@@ -184,6 +188,9 @@ def create_cloud_database() -> Response:
|
|
|
184
188
|
|
|
185
189
|
:resjson int target_quota: The target quota.
|
|
186
190
|
|
|
191
|
+
:resjson int requests_per_second_limit: The per-second request limit, or
|
|
192
|
+
null when rate limiting is disabled.
|
|
193
|
+
|
|
187
194
|
:resjson string server_access_key: The server access key for the cloud
|
|
188
195
|
database.
|
|
189
196
|
|
|
@@ -234,6 +241,10 @@ def create_cloud_database() -> Response:
|
|
|
234
241
|
"target_quota",
|
|
235
242
|
random_database.target_quota,
|
|
236
243
|
)
|
|
244
|
+
requests_per_second_limit = request_json.get(
|
|
245
|
+
"requests_per_second_limit",
|
|
246
|
+
random_database.requests_per_second_limit,
|
|
247
|
+
)
|
|
237
248
|
|
|
238
249
|
state = States[state_name]
|
|
239
250
|
database_type = DatabaseType[database_type_name]
|
|
@@ -248,6 +259,7 @@ def create_cloud_database() -> Response:
|
|
|
248
259
|
database_type=database_type,
|
|
249
260
|
request_quota=request_quota,
|
|
250
261
|
target_quota=target_quota,
|
|
262
|
+
requests_per_second_limit=requests_per_second_limit,
|
|
251
263
|
)
|
|
252
264
|
try:
|
|
253
265
|
TARGET_MANAGER.add_cloud_database(cloud_database=database)
|
|
@@ -202,6 +202,7 @@ def validate_request() -> None:
|
|
|
202
202
|
request_method=request.method,
|
|
203
203
|
request_path=request.path,
|
|
204
204
|
databases=get_all_cloud_databases(),
|
|
205
|
+
request_rate_limiter=TARGET_MANAGER.request_rate_limiter,
|
|
205
206
|
)
|
|
206
207
|
|
|
207
208
|
|
|
@@ -590,6 +591,7 @@ def generate_vumark_instance(target_id: str) -> Response:
|
|
|
590
591
|
request_method=request.method,
|
|
591
592
|
request_path=request.path,
|
|
592
593
|
databases=all_databases,
|
|
594
|
+
request_rate_limiter=TARGET_MANAGER.request_rate_limiter,
|
|
593
595
|
)
|
|
594
596
|
|
|
595
597
|
database = get_database_matching_server_keys(
|
{vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_model_target_web_api.py
RENAMED
|
@@ -127,6 +127,30 @@ def _basic_auth_credentials(auth_header: str | None) -> tuple[str, str] | None:
|
|
|
127
127
|
return client_id, client_secret
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
@beartype
|
|
131
|
+
def _jwt_header_error(*, bearer_token: str) -> str | None:
|
|
132
|
+
"""Return the Vuforia error for an invalid JSON Web Token header."""
|
|
133
|
+
encoded_header = bearer_token.partition(".")[0]
|
|
134
|
+
try:
|
|
135
|
+
padding = "=" * (-len(encoded_header) % 4)
|
|
136
|
+
decoded_header = base64.b64decode(
|
|
137
|
+
s=encoded_header + padding,
|
|
138
|
+
altchars=b"-_",
|
|
139
|
+
validate=True,
|
|
140
|
+
)
|
|
141
|
+
header = json.loads(s=decoded_header)
|
|
142
|
+
except ValueError:
|
|
143
|
+
header = None
|
|
144
|
+
|
|
145
|
+
if not isinstance(header, dict):
|
|
146
|
+
return "Invalid unsecured/JWS/JWE header: Invalid JSON object"
|
|
147
|
+
if "alg" not in header:
|
|
148
|
+
return 'Missing "alg" in header JSON object'
|
|
149
|
+
if header["alg"] == "none":
|
|
150
|
+
return "Unsecured (plain) JWTs are rejected, extend class to handle"
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
130
154
|
@beartype
|
|
131
155
|
def _require_bearer_token(request: RequestData) -> _ResponseType | None:
|
|
132
156
|
"""Return an error response if the request has no bearer token."""
|
|
@@ -156,6 +180,15 @@ def _require_bearer_token(request: RequestData) -> _ResponseType | None:
|
|
|
156
180
|
target="jwt",
|
|
157
181
|
details=None,
|
|
158
182
|
)
|
|
183
|
+
jwt_header_error = _jwt_header_error(bearer_token=bearer_token)
|
|
184
|
+
if jwt_header_error is not None:
|
|
185
|
+
return _error_response(
|
|
186
|
+
status_code=HTTPStatus.UNAUTHORIZED,
|
|
187
|
+
code="401",
|
|
188
|
+
message=jwt_header_error,
|
|
189
|
+
target="jwt",
|
|
190
|
+
details=None,
|
|
191
|
+
)
|
|
159
192
|
return None
|
|
160
193
|
|
|
161
194
|
|
|
@@ -14,6 +14,7 @@ from responses import RequestsMock
|
|
|
14
14
|
|
|
15
15
|
from mock_vws._mock_common import MissingSchemeError, RequestData
|
|
16
16
|
from mock_vws._respx_mock_server.decorators import start_respx_router
|
|
17
|
+
from mock_vws.cloud_query import CloudQueryFailureResponse
|
|
17
18
|
from mock_vws.database import CloudDatabase, VuMarkDatabase
|
|
18
19
|
from mock_vws.image_matchers import (
|
|
19
20
|
ImageMatcher,
|
|
@@ -24,6 +25,7 @@ from mock_vws.target_raters import (
|
|
|
24
25
|
BrisqueTargetTrackingRater,
|
|
25
26
|
TargetTrackingRater,
|
|
26
27
|
)
|
|
28
|
+
from mock_vws.vumark import VuMarkGenerationFailure
|
|
27
29
|
|
|
28
30
|
from .mock_web_query_api import MockVuforiaWebQueryAPI
|
|
29
31
|
from .mock_web_services_api import MockVuforiaWebServicesAPI
|
|
@@ -51,6 +53,7 @@ class MockVWS(ContextDecorator):
|
|
|
51
53
|
*,
|
|
52
54
|
base_vws_url: str = "https://vws.vuforia.com",
|
|
53
55
|
base_vwq_url: str = "https://cloudreco.vuforia.com",
|
|
56
|
+
cloud_query_failure_response: CloudQueryFailureResponse | None = None,
|
|
54
57
|
duplicate_match_checker: ImageMatcher = _STRUCTURAL_SIMILARITY_MATCHER,
|
|
55
58
|
query_match_checker: ImageMatcher = _STRUCTURAL_SIMILARITY_MATCHER,
|
|
56
59
|
processing_time_seconds: float = 2.0,
|
|
@@ -58,6 +61,7 @@ class MockVWS(ContextDecorator):
|
|
|
58
61
|
real_http: bool = False,
|
|
59
62
|
response_delay_seconds: float = 0.0,
|
|
60
63
|
sleep_fn: Callable[[float], None] = time.sleep,
|
|
64
|
+
vumark_generation_failure: VuMarkGenerationFailure | None = None,
|
|
61
65
|
) -> None:
|
|
62
66
|
"""Route requests to Vuforia's Web Service APIs to fakes of those
|
|
63
67
|
APIs.
|
|
@@ -74,6 +78,14 @@ class MockVWS(ContextDecorator):
|
|
|
74
78
|
In the real Vuforia Web Services, this is not deterministic.
|
|
75
79
|
base_vwq_url: The base URL for the VWQ API.
|
|
76
80
|
base_vws_url: The base URL for the VWS API.
|
|
81
|
+
cloud_query_failure_response: A response to return for every Cloud
|
|
82
|
+
Query request, bypassing normal request validation and image
|
|
83
|
+
matching. By default, Cloud Query requests are handled
|
|
84
|
+
normally.
|
|
85
|
+
vumark_generation_failure: A failure to return for every VuMark
|
|
86
|
+
generation request, bypassing normal request validation and
|
|
87
|
+
instance generation. By default, VuMark generation requests
|
|
88
|
+
are handled normally.
|
|
77
89
|
query_match_checker: A callable which takes two image values and
|
|
78
90
|
returns whether they will match in a query request.
|
|
79
91
|
duplicate_match_checker: A callable which takes two image values
|
|
@@ -109,11 +121,13 @@ class MockVWS(ContextDecorator):
|
|
|
109
121
|
processing_time_seconds=float(processing_time_seconds),
|
|
110
122
|
duplicate_match_checker=duplicate_match_checker,
|
|
111
123
|
target_tracking_rater=target_tracking_rater,
|
|
124
|
+
vumark_generation_failure=vumark_generation_failure,
|
|
112
125
|
)
|
|
113
126
|
|
|
114
127
|
self._mock_vwq_api = MockVuforiaWebQueryAPI(
|
|
115
128
|
target_manager=self._target_manager,
|
|
116
129
|
query_match_checker=query_match_checker,
|
|
130
|
+
failure_response=cloud_query_failure_response,
|
|
117
131
|
)
|
|
118
132
|
|
|
119
133
|
def add_cloud_database(self, cloud_database: CloudDatabase) -> None:
|
|
@@ -19,12 +19,13 @@ from mock_vws._query_validators import run_query_validators
|
|
|
19
19
|
from mock_vws._query_validators.exceptions import (
|
|
20
20
|
ValidatorError,
|
|
21
21
|
)
|
|
22
|
+
from mock_vws.cloud_query import CloudQueryFailureResponse
|
|
22
23
|
from mock_vws.image_matchers import ImageMatcher
|
|
23
24
|
from mock_vws.target_manager import TargetManager
|
|
24
25
|
|
|
25
26
|
_ROUTES: set[Route] = set()
|
|
26
27
|
|
|
27
|
-
_ResponseType = tuple[int, Mapping[str, str], str]
|
|
28
|
+
_ResponseType = tuple[int, Mapping[str, str], str | bytes]
|
|
28
29
|
_P = ParamSpec("_P")
|
|
29
30
|
|
|
30
31
|
|
|
@@ -86,13 +87,16 @@ class MockVuforiaWebQueryAPI:
|
|
|
86
87
|
self,
|
|
87
88
|
target_manager: TargetManager,
|
|
88
89
|
query_match_checker: ImageMatcher,
|
|
90
|
+
failure_response: CloudQueryFailureResponse | None,
|
|
89
91
|
) -> None:
|
|
90
92
|
"""
|
|
91
93
|
Args:
|
|
92
94
|
target_manager: The target manager which holds all databases.
|
|
93
95
|
query_match_checker: A callable which takes two image values
|
|
94
|
-
|
|
96
|
+
and
|
|
95
97
|
returns whether they match.
|
|
98
|
+
failure_response: A configured failure response which takes
|
|
99
|
+
precedence over normal query handling.
|
|
96
100
|
|
|
97
101
|
Attributes:
|
|
98
102
|
routes: The `Route`s to be used in the mock.
|
|
@@ -100,10 +104,18 @@ class MockVuforiaWebQueryAPI:
|
|
|
100
104
|
self.routes = _ROUTES
|
|
101
105
|
self._target_manager = target_manager
|
|
102
106
|
self._query_match_checker = query_match_checker
|
|
107
|
+
self._failure_response = failure_response
|
|
103
108
|
|
|
104
109
|
@route(path_pattern="/v1/query", http_methods={HTTPMethod.POST})
|
|
105
110
|
def query(self, request: RequestData) -> _ResponseType:
|
|
106
111
|
"""Perform an image recognition query."""
|
|
112
|
+
if self._failure_response is not None:
|
|
113
|
+
return (
|
|
114
|
+
self._failure_response.status_code,
|
|
115
|
+
self._failure_response.headers,
|
|
116
|
+
self._failure_response.body,
|
|
117
|
+
)
|
|
118
|
+
|
|
107
119
|
try:
|
|
108
120
|
run_query_validators(
|
|
109
121
|
request_path=request.path,
|
|
@@ -49,6 +49,7 @@ from mock_vws.model_target import ModelTargetDatasetType
|
|
|
49
49
|
from mock_vws.target import ImageTarget
|
|
50
50
|
from mock_vws.target_manager import TargetManager
|
|
51
51
|
from mock_vws.target_raters import TargetTrackingRater
|
|
52
|
+
from mock_vws.vumark import VuMarkGenerationFailure
|
|
52
53
|
|
|
53
54
|
if TYPE_CHECKING:
|
|
54
55
|
from mock_vws.database import CloudDatabase
|
|
@@ -125,6 +126,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
125
126
|
processing_time_seconds: float,
|
|
126
127
|
duplicate_match_checker: ImageMatcher,
|
|
127
128
|
target_tracking_rater: TargetTrackingRater,
|
|
129
|
+
vumark_generation_failure: VuMarkGenerationFailure | None,
|
|
128
130
|
) -> None:
|
|
129
131
|
"""
|
|
130
132
|
Args:
|
|
@@ -137,6 +139,8 @@ class MockVuforiaWebServicesAPI:
|
|
|
137
139
|
and returns whether they are duplicates.
|
|
138
140
|
target_tracking_rater: A callable for rating targets for
|
|
139
141
|
tracking.
|
|
142
|
+
vumark_generation_failure: A configured failure which takes
|
|
143
|
+
precedence over normal VuMark generation handling.
|
|
140
144
|
|
|
141
145
|
Attributes:
|
|
142
146
|
routes: The `Route`s to be used in the mock.
|
|
@@ -146,6 +150,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
146
150
|
self._processing_time_seconds = processing_time_seconds
|
|
147
151
|
self._duplicate_match_checker = duplicate_match_checker
|
|
148
152
|
self._target_tracking_rater = target_tracking_rater
|
|
153
|
+
self._vumark_generation_failure = vumark_generation_failure
|
|
149
154
|
|
|
150
155
|
@route(path_pattern="/oauth2/token", http_methods={HTTPMethod.POST})
|
|
151
156
|
def oauth2_token( # pylint: disable=no-self-use
|
|
@@ -317,6 +322,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
317
322
|
request_method=request.method,
|
|
318
323
|
request_path=request.path,
|
|
319
324
|
databases=self._target_manager.cloud_databases,
|
|
325
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
320
326
|
)
|
|
321
327
|
except ValidatorError as exc:
|
|
322
328
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -392,6 +398,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
392
398
|
request_method=request.method,
|
|
393
399
|
request_path=request.path,
|
|
394
400
|
databases=self._target_manager.cloud_databases,
|
|
401
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
395
402
|
)
|
|
396
403
|
except ValidatorError as exc:
|
|
397
404
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -453,6 +460,28 @@ class MockVuforiaWebServicesAPI:
|
|
|
453
460
|
)
|
|
454
461
|
def generate_vumark_instance(self, request: RequestData) -> _ResponseType:
|
|
455
462
|
"""Generate a VuMark instance."""
|
|
463
|
+
if self._vumark_generation_failure is not None:
|
|
464
|
+
body_json = json_dump(
|
|
465
|
+
body={
|
|
466
|
+
"transaction_id": uuid.uuid4().hex,
|
|
467
|
+
"result_code": self._vumark_generation_failure.value,
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
date = email.utils.formatdate(
|
|
471
|
+
timeval=None,
|
|
472
|
+
localtime=False,
|
|
473
|
+
usegmt=True,
|
|
474
|
+
)
|
|
475
|
+
return (
|
|
476
|
+
self._vumark_generation_failure.status_code,
|
|
477
|
+
{
|
|
478
|
+
"Content-Length": str(object=len(body_json)),
|
|
479
|
+
"Content-Type": "application/json",
|
|
480
|
+
"Date": date,
|
|
481
|
+
},
|
|
482
|
+
body_json,
|
|
483
|
+
)
|
|
484
|
+
|
|
456
485
|
valid_accept_types: dict[str, bytes] = {
|
|
457
486
|
"image/png": VUMARK_PNG,
|
|
458
487
|
"image/svg+xml": VUMARK_SVG,
|
|
@@ -469,6 +498,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
469
498
|
request_method=request.method,
|
|
470
499
|
request_path=request.path,
|
|
471
500
|
databases=all_databases,
|
|
501
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
472
502
|
)
|
|
473
503
|
|
|
474
504
|
database = get_database_matching_server_keys(
|
|
@@ -530,6 +560,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
530
560
|
request_method=request.method,
|
|
531
561
|
request_path=request.path,
|
|
532
562
|
databases=self._target_manager.cloud_databases,
|
|
563
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
533
564
|
)
|
|
534
565
|
except ValidatorError as exc:
|
|
535
566
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -591,6 +622,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
591
622
|
request_method=request.method,
|
|
592
623
|
request_path=request.path,
|
|
593
624
|
databases=self._target_manager.cloud_databases,
|
|
625
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
594
626
|
)
|
|
595
627
|
except ValidatorError as exc:
|
|
596
628
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -648,6 +680,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
648
680
|
request_method=request.method,
|
|
649
681
|
request_path=request.path,
|
|
650
682
|
databases=self._target_manager.cloud_databases,
|
|
683
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
651
684
|
)
|
|
652
685
|
except ValidatorError as exc:
|
|
653
686
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -717,6 +750,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
717
750
|
request_method=request.method,
|
|
718
751
|
request_path=request.path,
|
|
719
752
|
databases=self._target_manager.cloud_databases,
|
|
753
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
720
754
|
)
|
|
721
755
|
except ValidatorError as exc:
|
|
722
756
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -788,6 +822,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
788
822
|
request_method=request.method,
|
|
789
823
|
request_path=request.path,
|
|
790
824
|
databases=self._target_manager.cloud_databases,
|
|
825
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
791
826
|
)
|
|
792
827
|
except ValidatorError as exc:
|
|
793
828
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -902,6 +937,7 @@ class MockVuforiaWebServicesAPI:
|
|
|
902
937
|
request_method=request.method,
|
|
903
938
|
request_path=request.path,
|
|
904
939
|
databases=self._target_manager.cloud_databases,
|
|
940
|
+
request_rate_limiter=self._target_manager.request_rate_limiter,
|
|
905
941
|
)
|
|
906
942
|
except ValidatorError as exc:
|
|
907
943
|
return exc.status_code, exc.headers, exc.response_text
|
|
@@ -49,6 +49,10 @@ from .name_validators import (
|
|
|
49
49
|
)
|
|
50
50
|
from .project_state_validators import validate_project_state
|
|
51
51
|
from .request_quota_validators import validate_request_quota
|
|
52
|
+
from .request_rate_validators import (
|
|
53
|
+
RequestRateLimiter,
|
|
54
|
+
validate_request_rate,
|
|
55
|
+
)
|
|
52
56
|
from .target_quota_validators import validate_target_quota
|
|
53
57
|
from .target_validators import validate_target_id_exists
|
|
54
58
|
from .width_validators import validate_width
|
|
@@ -62,6 +66,7 @@ def run_services_validators(
|
|
|
62
66
|
request_body: bytes,
|
|
63
67
|
request_method: str,
|
|
64
68
|
databases: Iterable[AnyDatabase],
|
|
69
|
+
request_rate_limiter: RequestRateLimiter,
|
|
65
70
|
) -> None:
|
|
66
71
|
"""Run all validators.
|
|
67
72
|
|
|
@@ -71,6 +76,7 @@ def run_services_validators(
|
|
|
71
76
|
request_body: The body of the request.
|
|
72
77
|
request_method: The HTTP method of the request.
|
|
73
78
|
databases: All Vuforia databases.
|
|
79
|
+
request_rate_limiter: The rate limiter tracking recent requests.
|
|
74
80
|
"""
|
|
75
81
|
validate_auth_header_exists(request_headers=request_headers)
|
|
76
82
|
validate_auth_header_has_signature(request_headers=request_headers)
|
|
@@ -92,6 +98,14 @@ def run_services_validators(
|
|
|
92
98
|
request_path=request_path,
|
|
93
99
|
databases=databases,
|
|
94
100
|
)
|
|
101
|
+
validate_request_rate(
|
|
102
|
+
request_headers=request_headers,
|
|
103
|
+
request_body=request_body,
|
|
104
|
+
request_method=request_method,
|
|
105
|
+
request_path=request_path,
|
|
106
|
+
databases=databases,
|
|
107
|
+
request_rate_limiter=request_rate_limiter,
|
|
108
|
+
)
|
|
95
109
|
validate_project_state(
|
|
96
110
|
request_headers=request_headers,
|
|
97
111
|
request_body=request_body,
|
|
@@ -145,6 +145,37 @@ class RequestQuotaReachedError(ValidatorError):
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
@beartype
|
|
149
|
+
class TooManyRequestsError(ValidatorError):
|
|
150
|
+
"""Exception raised when a database exceeds its request rate limit."""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
"""Initialize a ``TooManyRequests`` response."""
|
|
154
|
+
super().__init__()
|
|
155
|
+
self.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
|
156
|
+
body = {
|
|
157
|
+
"transaction_id": uuid.uuid4().hex,
|
|
158
|
+
"result_code": ResultCodes.TOO_MANY_REQUESTS.value,
|
|
159
|
+
}
|
|
160
|
+
self.response_text = json_dump(body=body)
|
|
161
|
+
date = email.utils.formatdate(
|
|
162
|
+
timeval=None,
|
|
163
|
+
localtime=False,
|
|
164
|
+
usegmt=True,
|
|
165
|
+
)
|
|
166
|
+
self.headers = {
|
|
167
|
+
"Connection": "keep-alive",
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"server": "envoy",
|
|
170
|
+
"Date": date,
|
|
171
|
+
"x-envoy-upstream-service-time": "5",
|
|
172
|
+
"Content-Length": str(object=len(self.response_text)),
|
|
173
|
+
"strict-transport-security": "max-age=31536000",
|
|
174
|
+
"x-aws-region": "us-east-2, us-west-2",
|
|
175
|
+
"x-content-type-options": "nosniff",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
148
179
|
@beartype
|
|
149
180
|
class TargetQuotaReachedError(ValidatorError):
|
|
150
181
|
"""Exception raised when a database's target quota is exhausted."""
|