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.
Files changed (158) hide show
  1. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/test.yml +2 -0
  2. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/CHANGELOG.rst +16 -3
  3. {vws_python_mock-2026.8.4/src/vws_python_mock.egg-info → vws_python_mock-2026.8.4.2}/PKG-INFO +5 -7
  4. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/differences-to-vws.rst +34 -3
  5. vws_python_mock-2026.8.4.2/docs/source/installation.rst +8 -0
  6. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/mock-api-reference.rst +8 -0
  7. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/pyproject.toml +4 -14
  8. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/__init__.py +4 -0
  9. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/target_manager.py +12 -0
  10. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/vws.py +2 -0
  11. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_model_target_web_api.py +33 -0
  12. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/decorators.py +14 -0
  13. {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
  14. {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
  15. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/__init__.py +14 -0
  16. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/exceptions.py +31 -0
  17. vws_python_mock-2026.8.4.2/src/mock_vws/_services_validators/request_rate_validators.py +83 -0
  18. vws_python_mock-2026.8.4.2/src/mock_vws/cloud_query.py +22 -0
  19. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/database.py +10 -0
  20. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/image_matchers.py +16 -47
  21. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/model_target.py +3 -2
  22. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target_manager.py +10 -0
  23. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target_raters.py +13 -16
  24. vws_python_mock-2026.8.4.2/src/mock_vws/vumark.py +23 -0
  25. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2/src/vws_python_mock.egg-info}/PKG-INFO +5 -7
  26. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/SOURCES.txt +5 -0
  27. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/requires.txt +4 -6
  28. {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
  29. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/scm_version.json +2 -2
  30. vws_python_mock-2026.8.4.2/tests/mock_vws/test_cloud_query_failure_response.py +132 -0
  31. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_flask_app_usage.py +20 -0
  32. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_model_target_web_api.py +52 -1
  33. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_requests_mock_usage.py +68 -3
  34. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_respx_mock_usage.py +3 -2
  35. vws_python_mock-2026.8.4.2/tests/mock_vws/test_vumark_generation_failure.py +79 -0
  36. vws_python_mock-2026.8.4/docs/source/installation.rst +0 -34
  37. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.git_archival.txt +0 -0
  38. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.gitattributes +0 -0
  39. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/dependabot.yml +0 -0
  40. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/dependabot-merge.yml +0 -0
  41. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/docker-build.yml +0 -0
  42. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/lint.yml +0 -0
  43. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/publish-site.yml +0 -0
  44. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.github/workflows/release.yml +0 -0
  45. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.gitignore +0 -0
  46. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.pre-commit-config.yaml +0 -0
  47. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.prettierrc +0 -0
  48. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.vscode/extensions.json +0 -0
  49. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/.vscode/settings.json +0 -0
  50. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/CODE_OF_CONDUCT.rst +0 -0
  51. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/LICENSE +0 -0
  52. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/MANIFEST.in +0 -0
  53. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/README.rst +0 -0
  54. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/__init__.py +0 -0
  55. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/create_secrets_files.py +0 -0
  56. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/admin/vumark_template.svg +0 -0
  57. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/__init__.py +0 -0
  58. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/decrypt_secret.sh +0 -0
  59. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/ci/test_custom_linters.py +0 -0
  60. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/conftest.py +0 -0
  61. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docker-bake.hcl +0 -0
  62. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/__init__.py +0 -0
  63. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/basic-example.rst +0 -0
  64. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/changelog.rst +0 -0
  65. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/ci-setup.rst +0 -0
  66. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/conf.py +0 -0
  67. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/contributing.rst +0 -0
  68. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/docker.rst +0 -0
  69. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/getting-started.rst +0 -0
  70. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/httpx-example.rst +0 -0
  71. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/index.rst +0 -0
  72. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/release-process.rst +0 -0
  73. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/unreleased.rst +0 -0
  74. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/source/versioning-and-api-stability.rst +0 -0
  75. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/docs/towncrier_template.rst.jinja +0 -0
  76. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/newsfragments/.gitkeep +0 -0
  77. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/secrets.tar.gpg +0 -0
  78. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/setup.cfg +0 -0
  79. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/spelling_private_dict.txt +0 -0
  80. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_base64_decoding.py +0 -0
  81. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_constants.py +0 -0
  82. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_database_matchers.py +0 -0
  83. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/Dockerfile +0 -0
  84. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/__init__.py +0 -0
  85. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/healthcheck.py +0 -0
  86. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_flask_server/vwq.py +0 -0
  87. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_mock_common.py +0 -0
  88. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_tools.py +0 -0
  89. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/__init__.py +0 -0
  90. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
  91. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/auth_validators.py +0 -0
  92. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
  93. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
  94. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/date_validators.py +0 -0
  95. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/exceptions.py +0 -0
  96. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/fields_validators.py +0 -0
  97. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/image_validators.py +0 -0
  98. {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
  99. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
  100. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
  101. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
  102. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_respx_mock_server/__init__.py +0 -0
  103. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_respx_mock_server/decorators.py +0 -0
  104. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
  105. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/auth_validators.py +0 -0
  106. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
  107. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
  108. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/date_validators.py +0 -0
  109. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/image_validators.py +0 -0
  110. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/json_validators.py +0 -0
  111. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/key_validators.py +0 -0
  112. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
  113. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/name_validators.py +0 -0
  114. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
  115. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/request_quota_validators.py +0 -0
  116. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/target_quota_validators.py +0 -0
  117. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/target_validators.py +0 -0
  118. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/_services_validators/width_validators.py +0 -0
  119. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/database_type.py +0 -0
  120. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/py.typed +0 -0
  121. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/states.py +0 -0
  122. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/mock_vws/target.py +0 -0
  123. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
  124. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
  125. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/src/vws_python_mock.egg-info/top_level.txt +0 -0
  126. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/__init__.py +0 -0
  127. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/conftest.py +0 -0
  128. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/__init__.py +0 -0
  129. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/__init__.py +0 -0
  130. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/credentials.py +0 -0
  131. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/prepared_requests.py +0 -0
  132. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
  133. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_add_target.py +0 -0
  134. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_authorization_header.py +0 -0
  135. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_content_length.py +0 -0
  136. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_database_summary.py +0 -0
  137. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_date_header.py +0 -0
  138. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_delete_target.py +0 -0
  139. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_docker.py +0 -0
  140. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_get_duplicates.py +0 -0
  141. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_get_target.py +0 -0
  142. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_invalid_given_id.py +0 -0
  143. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_invalid_json.py +0 -0
  144. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_query.py +0 -0
  145. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_list.py +0 -0
  146. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_raters.py +0 -0
  147. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_summary.py +0 -0
  148. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_target_validators.py +0 -0
  149. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_unexpected_json.py +0 -0
  150. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_update_target.py +0 -0
  151. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/test_vumark_generation_api.py +0 -0
  152. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/__init__.py +0 -0
  153. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/assertions.py +0 -0
  154. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/retries.py +0 -0
  155. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/too_many_requests.py +0 -0
  156. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
  157. {vws_python_mock-2026.8.4 → vws_python_mock-2026.8.4.2}/vuforia_secrets.env.example +0 -0
  158. {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
 
@@ -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>=1.26.4
24
- Requires-Dist: pillow>=11.0.0
25
- Requires-Dist: piq>=0.8.0
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 three other result codes which have not been verified
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 syntactically JSON Web Token-shaped bearer token, such as the token returned by the mock OAuth2 route.
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``.
@@ -0,0 +1,8 @@
1
+ Installation
2
+ ------------
3
+
4
+ .. code-block:: console
5
+
6
+ $ pip install vws-python-mock
7
+
8
+ This requires Python |minimum-python-version|\+.
@@ -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>=1.26.4",
41
- "pillow>=11.0.0",
42
- "piq>=0.8.0",
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
  ]
@@ -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(
@@ -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
- and
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."""