vws-python-mock 2026.8.26__tar.gz → 2026.9.6__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 (189) hide show
  1. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/test.yml +18 -6
  2. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/CHANGELOG.rst +35 -0
  3. {vws_python_mock-2026.8.26/src/vws_python_mock.egg-info → vws_python_mock-2026.9.6}/PKG-INFO +43 -20
  4. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/README.rst +21 -1
  5. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/conftest.py +13 -0
  6. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/basic-example.rst +2 -1
  7. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/contributing.rst +38 -0
  8. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/differences-to-vws.rst +128 -38
  9. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/docker.rst +11 -0
  10. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/getting-started.rst +2 -0
  11. vws_python_mock-2026.9.6/docs/source/httpx2-example.rst +20 -0
  12. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/index.rst +3 -0
  13. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/mock-api-reference.rst +8 -0
  14. vws_python_mock-2026.9.6/docs/source/unverified-behavior.rst +239 -0
  15. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/prek.toml +9 -10
  16. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/pyproject.toml +35 -19
  17. vws_python_mock-2026.9.6/secrets.tar.gpg +0 -0
  18. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/spelling_private_dict.txt +12 -0
  19. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/__init__.py +4 -0
  20. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/target_manager.py +437 -129
  21. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/vws.py +20 -8
  22. vws_python_mock-2026.9.6/src/mock_vws/_httpx2_mock_server/__init__.py +1 -0
  23. vws_python_mock-2026.9.6/src/mock_vws/_httpx2_mock_server/decorators.py +396 -0
  24. vws_python_mock-2026.9.6/src/mock_vws/_matching.py +92 -0
  25. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_tools.py +13 -23
  26. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/exceptions.py +23 -0
  27. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/fields_validators.py +6 -11
  28. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/image_validators.py +4 -10
  29. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/include_target_data_validators.py +4 -11
  30. vws_python_mock-2026.9.6/src/mock_vws/_query_validators/multipart.py +109 -0
  31. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/num_results_validators.py +4 -11
  32. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_reco_counts_web_api.py +45 -1
  33. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_requests_mock_server/mock_web_query_api.py +1 -1
  34. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_requests_mock_server/mock_web_services_api.py +76 -93
  35. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_respx_mock_server/decorators.py +17 -0
  36. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/__init__.py +22 -35
  37. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/auth_validators.py +7 -4
  38. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/database_id_validators.py +3 -21
  39. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/image_validators.py +7 -1
  40. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/json_validators.py +29 -8
  41. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/name_validators.py +5 -31
  42. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/project_state_validators.py +3 -19
  43. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/request_quota_validators.py +2 -21
  44. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/request_rate_validators.py +3 -15
  45. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/target_quota_validators.py +2 -15
  46. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/target_validators.py +3 -20
  47. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/database.py +11 -0
  48. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/decorators.py +103 -7
  49. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/image_matchers.py +27 -6
  50. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/model_target.py +35 -0
  51. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/reco_counts.py +21 -6
  52. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/target.py +20 -4
  53. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6/src/vws_python_mock.egg-info}/PKG-INFO +43 -20
  54. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/SOURCES.txt +12 -0
  55. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/requires.txt +21 -18
  56. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/scm_file_list.json +12 -0
  57. vws_python_mock-2026.9.6/src/vws_python_mock.egg-info/scm_version.json +8 -0
  58. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/conftest.py +27 -2
  59. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/fixtures/prepared_requests.py +48 -0
  60. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/fixtures/vuforia_backends.py +71 -4
  61. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_add_target.py +25 -0
  62. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_cloud_query_failure_response.py +15 -4
  63. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_database_summary.py +12 -3
  64. vws_python_mock-2026.9.6/tests/mock_vws/test_docker.py +685 -0
  65. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_flask_app_usage.py +291 -0
  66. vws_python_mock-2026.9.6/tests/mock_vws/test_httpx2_mock_usage.py +538 -0
  67. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_invalid_given_id.py +2 -2
  68. vws_python_mock-2026.9.6/tests/mock_vws/test_invalid_json.py +306 -0
  69. vws_python_mock-2026.9.6/tests/mock_vws/test_model_target_failure_response.py +210 -0
  70. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_model_target_generation_failure.py +23 -3
  71. vws_python_mock-2026.9.6/tests/mock_vws/test_model_target_retries.py +219 -0
  72. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_model_target_web_api.py +83 -14
  73. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_query.py +241 -12
  74. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_reco_counts_report.py +72 -0
  75. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_requests_mock_usage.py +457 -3
  76. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_respx_mock_usage.py +25 -0
  77. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_target_summary.py +49 -0
  78. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_target_validators.py +1 -33
  79. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_update_target.py +61 -0
  80. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_vumark_generation_failure.py +18 -4
  81. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/utils/__init__.py +52 -6
  82. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/utils/assertions.py +22 -3
  83. vws_python_mock-2026.9.6/tests/mock_vws/utils/httpx2_transports.py +156 -0
  84. vws_python_mock-2026.9.6/tests/mock_vws/utils/model_target_retries.py +161 -0
  85. vws_python_mock-2026.9.6/tests/mock_vws/utils/recognition_counts.py +70 -0
  86. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/utils/retries.py +17 -0
  87. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/uv.lock +295 -233
  88. vws_python_mock-2026.8.26/secrets.tar.gpg +0 -0
  89. vws_python_mock-2026.8.26/src/vws_python_mock.egg-info/scm_version.json +0 -8
  90. vws_python_mock-2026.8.26/tests/mock_vws/test_docker.py +0 -394
  91. vws_python_mock-2026.8.26/tests/mock_vws/test_invalid_json.py +0 -194
  92. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.dockerignore +0 -0
  93. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.git_archival.txt +0 -0
  94. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.gitattributes +0 -0
  95. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/dependabot.yml +0 -0
  96. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/autofix.yml +0 -0
  97. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/dependabot-merge.yml +0 -0
  98. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/docker-build.yml +0 -0
  99. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/lint.yml +0 -0
  100. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/publish-site.yml +0 -0
  101. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.github/workflows/release.yml +0 -0
  102. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.gitignore +0 -0
  103. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.prettierrc +0 -0
  104. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.vale.ini +0 -0
  105. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.vscode/extensions.json +0 -0
  106. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/.vscode/settings.json +0 -0
  107. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/CODE_OF_CONDUCT.rst +0 -0
  108. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/LICENSE +0 -0
  109. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/MANIFEST.in +0 -0
  110. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/admin/__init__.py +0 -0
  111. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/admin/create_secrets_files.py +0 -0
  112. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/admin/vumark_template.svg +0 -0
  113. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/ci/__init__.py +0 -0
  114. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/ci/decrypt_secret.sh +0 -0
  115. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docker-bake.hcl +0 -0
  116. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/__init__.py +0 -0
  117. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/changelog.rst +0 -0
  118. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/ci-setup.rst +0 -0
  119. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/conf.py +0 -0
  120. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/httpx-example.rst +0 -0
  121. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/installation.rst +0 -0
  122. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/release-process.rst +0 -0
  123. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/unreleased.rst +0 -0
  124. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/source/versioning-and-api-stability.rst +0 -0
  125. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/docs/towncrier_template.rst.jinja +0 -0
  126. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/newsfragments/.gitkeep +0 -0
  127. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/setup.cfg +0 -0
  128. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_base64_decoding.py +0 -0
  129. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_constants.py +0 -0
  130. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_database_matchers.py +0 -0
  131. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/Dockerfile +0 -0
  132. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/__init__.py +0 -0
  133. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/healthcheck.py +0 -0
  134. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_flask_server/vwq.py +0 -0
  135. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_image_opening.py +0 -0
  136. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_mock_common.py +0 -0
  137. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_model_target_web_api.py +0 -0
  138. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/__init__.py +0 -0
  139. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
  140. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/auth_validators.py +0 -0
  141. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
  142. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
  143. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/date_validators.py +0 -0
  144. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
  145. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
  146. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_respx_mock_server/__init__.py +0 -0
  147. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
  148. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
  149. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
  150. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/date_validators.py +0 -0
  151. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/exceptions.py +0 -0
  152. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/instance_id_validators.py +0 -0
  153. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/key_validators.py +0 -0
  154. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
  155. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/_services_validators/width_validators.py +0 -0
  156. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/cloud_query.py +0 -0
  157. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/database_type.py +0 -0
  158. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/py.typed +0 -0
  159. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/request_rate_limits.py +0 -0
  160. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/states.py +0 -0
  161. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/target_manager.py +0 -0
  162. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/target_raters.py +0 -0
  163. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/mock_vws/vumark.py +0 -0
  164. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
  165. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
  166. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/src/vws_python_mock.egg-info/top_level.txt +0 -0
  167. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/__init__.py +0 -0
  168. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/backend_harness.py +0 -0
  169. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/__init__.py +0 -0
  170. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/fixtures/__init__.py +0 -0
  171. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/fixtures/credentials.py +0 -0
  172. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/fixtures/model_target_prepared_requests.py +0 -0
  173. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_authorization_header.py +0 -0
  174. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_content_length.py +0 -0
  175. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_date_header.py +0 -0
  176. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_delete_target.py +0 -0
  177. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_get_duplicates.py +0 -0
  178. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_get_target.py +0 -0
  179. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_healthcheck.py +0 -0
  180. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_model_target_generation_warning.py +0 -0
  181. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_model_target_training_allowance.py +0 -0
  182. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_target_list.py +0 -0
  183. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_target_raters.py +0 -0
  184. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_unexpected_json.py +0 -0
  185. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/test_vumark_generation_api.py +0 -0
  186. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/utils/too_many_requests.py +0 -0
  187. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
  188. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/vuforia_secrets.env.example +0 -0
  189. {vws_python_mock-2026.8.26 → vws_python_mock-2026.9.6}/zizmor.yml +0 -0
@@ -46,6 +46,7 @@ jobs:
46
46
  - tests/mock_vws/test_query.py::TestTargetStatusFailed
47
47
  - tests/mock_vws/test_query.py::TestDateFormats
48
48
  - tests/mock_vws/test_query.py::TestInactiveProject
49
+ - tests/mock_vws/test_query.py::TestTruncatedBody
49
50
  - tests/mock_vws/test_add_target.py::TestContentTypes
50
51
  - tests/mock_vws/test_add_target.py::TestMissingData
51
52
  - tests/mock_vws/test_add_target.py::TestWidth
@@ -85,7 +86,10 @@ jobs:
85
86
  - tests/mock_vws/test_get_duplicates.py
86
87
  - tests/mock_vws/test_get_target.py
87
88
  - tests/mock_vws/test_invalid_given_id.py
88
- - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_invalid_json
89
+ - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_json
90
+ - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_an_object
91
+ - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_not_utf_8
92
+ - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_empty_body
89
93
  - tests/mock_vws/test_invalid_json.py::TestInvalidJSON::test_invalid_json_with_skewed_time
90
94
  - tests/mock_vws/test_target_list.py
91
95
  - tests/mock_vws/test_reco_counts_report.py
@@ -101,11 +105,13 @@ jobs:
101
105
  - tests/mock_vws/test_update_target.py::TestImage::test_image_valid
102
106
  - tests/mock_vws/test_update_target.py::TestImage::test_bad_image_format_or_color_space
103
107
  - tests/mock_vws/test_update_target.py::TestImage::test_corrupted
108
+ - tests/mock_vws/test_update_target.py::TestImage::test_truncated
104
109
  - tests/mock_vws/test_update_target.py::TestImage::test_image_too_large
105
110
  - tests/mock_vws/test_update_target.py::TestImage::test_not_base64_encoded_processable
106
111
  - tests/mock_vws/test_update_target.py::TestImage::test_not_base64_encoded_not_processable
107
112
  - tests/mock_vws/test_update_target.py::TestImage::test_not_image
108
113
  - tests/mock_vws/test_update_target.py::TestImage::test_invalid_type
114
+ - tests/mock_vws/test_update_target.py::TestImage::test_rating_does_not_return_to_minus_one
109
115
  - tests/mock_vws/test_update_target.py::TestImage::test_rating_can_change
110
116
  - tests/mock_vws/test_update_target.py::TestTargetName::test_name_valid
111
117
  - tests/mock_vws/test_update_target.py::TestTargetName::test_name_invalid
@@ -115,16 +121,19 @@ jobs:
115
121
  - tests/mock_vws/test_update_target.py::TestUpdate
116
122
  - tests/mock_vws/test_update_target.py::TestWidth
117
123
  - tests/mock_vws/test_update_target.py::TestInactiveProject
118
- - tests/mock_vws/test_requests_mock_usage.py
119
- - tests/mock_vws/test_respx_mock_usage.py
120
- - tests/mock_vws/test_flask_app_usage.py
121
124
  - tests/mock_vws/test_model_target_generation_failure.py
122
125
  - tests/mock_vws/test_model_target_generation_warning.py
126
+ - tests/mock_vws/test_model_target_failure_response.py
123
127
  - tests/mock_vws/test_model_target_training_allowance.py
124
128
  - tests/mock_vws/test_model_target_web_api.py
129
+ - tests/mock_vws/test_model_target_retries.py
125
130
  - tests/mock_vws/test_vumark_generation_api.py
126
131
  - tests/mock_vws/test_vumark_generation_failure.py
127
132
  - tests/mock_vws/test_target_validators.py
133
+ - tests/mock_vws/test_requests_mock_usage.py
134
+ - tests/mock_vws/test_respx_mock_usage.py
135
+ - tests/mock_vws/test_httpx2_mock_usage.py
136
+ - tests/mock_vws/test_flask_app_usage.py
128
137
  - tests/mock_vws/test_healthcheck.py
129
138
  - tests/mock_vws/test_docker.py
130
139
  - README.rst
@@ -151,8 +160,11 @@ jobs:
151
160
  # See the "CI Setup" document for details of how this was set up.
152
161
  ci/decrypt_secret.sh
153
162
  tar xvf "${HOME}"/secrets/secrets.tar
154
- # Matrix jobs are 0-based; only secrets 0-99 exist. Wrap for overflow
155
- # (e.g. docs/ at index 100 after the suite grew past 100 patterns).
163
+ # Matrix jobs are 0-based; only secrets 0-99 exist, so patterns
164
+ # past index 99 wrap and share a database with an earlier job.
165
+ # The ci_pattern list is therefore ordered so that every pattern
166
+ # from index 100 on is one which never parametrises over the Real
167
+ # Vuforia backend, and so never uses these credentials.
156
168
  SECRET_INDEX=$((JOB_INDEX % 100))
157
169
  cp "./ci_secrets/vuforia_secrets_${SECRET_INDEX}.env" ./vuforia_secrets.env
158
170
 
@@ -3,6 +3,41 @@ Changelog
3
3
 
4
4
  .. towncrier release notes start
5
5
 
6
+ 2026.09.06
7
+ ----------
8
+
9
+ - Add ``MockVWS.set_target_recognition_counts`` and a matching target manager endpoint for the Flask and Docker mock, for setting the recognition counts which the target summary report and the reco counts report show for a target.
10
+ A reco counts report now has a row for each target with recognitions in the requested month, and the recognition counts and recognition threshold of a cloud database are kept when the database is created in the Flask and Docker mock.
11
+
12
+ - Order Query API and ``GET /duplicates/{target_id}`` results by match score, with the best match first, as the real Query API does.
13
+ Matches with equal scores keep the existing upload date and then target ID order.
14
+
15
+ Image matchers now return a score, or ``None`` for no match, rather than a ``bool``.
16
+ ``StructuralSimilarityMatcher`` returns the images' SSIM score, and ``ExactMatcher`` gives every match the same score.
17
+ A custom ``query_match_checker`` or ``duplicate_match_checker`` which returns a ``bool`` now raises a ``TypeError``; return a score, such as ``1.0``, for a match and ``None`` for no match instead.
18
+ The mock's ranking is its matcher's opinion, not Vuforia's proprietary one, so the mock's order still need not agree with the real Query API's order.
19
+
20
+ - Respond, rather than failing to respond, to a request body which is not UTF-8 and to a query request whose ``multipart/form-data`` body ends before its closing boundary.
21
+
22
+ - Return a ``400`` response which names the offending field and the accepted values, rather than a ``500`` response, when the target manager container is given an invalid cloud database or VuMark database to create.
23
+ Return a ``404`` response, rather than a ``500`` response, when the target manager container is asked to add a target to a database which does not exist.
24
+
25
+ - Say which claims about the real Vuforia Web Services the mock has not had checked against them. :doc:`unverified-behavior` lists each such claim and what would verify it, and ``differences-to-vws`` marks the unverified assumptions among its deliberate differences.
26
+
27
+ - Return a ``BadImage`` response, rather than failing to respond, when an image given to the Target API is truncated before the end of its image data.
28
+
29
+ - ``MockVWS`` now intercepts ``httpx2`` requests, synchronous and asynchronous, alongside ``requests`` and ``httpx``. The ``httpx2`` path uses native ``httpx2`` requests and responses, and does not need ``httpx2.alias_httpx()``.
30
+
31
+ - Fix nested ``MockVWS`` instances on the ``httpx`` backend: an inner mock is now the only one which answers while it is running, matching the ``requests`` and ``httpx2`` backends. Previously the outer mock kept answering and requests to the inner mock's URL were refused.
32
+
33
+ - Return a response, rather than raising an uncaught ``JSONDecodeError``, when an empty body is given to a VWS endpoint which takes a JSON body.
34
+ As real Vuforia does, ``POST /targets`` and ``PUT /targets/<target_id>`` now return a 500 ``Fail`` response, the reco counts report endpoint returns a 400 ``Fail`` response, and the VuMark instance generation endpoint returns a 400 ``BadRequest`` response.
35
+
36
+ 2026.08.26.1
37
+ ------------
38
+
39
+ - Add configurable Model Target HTTP failure responses for selected dataset request phases.
40
+
6
41
  2026.08.26
7
42
  ----------
8
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python-mock
3
- Version: 2026.8.26
3
+ Version: 2026.9.6
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,9 +20,11 @@ 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: httpx2>=2.12
23
24
  Requires-Dist: numpy>=2.4.4
24
25
  Requires-Dist: opencv-contrib-python-headless>=5.0.0.93
25
26
  Requires-Dist: pillow>=12.2.0
27
+ Requires-Dist: pydantic>=2.6
26
28
  Requires-Dist: pydantic-settings>=2.6.1
27
29
  Requires-Dist: pyteenybrisque>=0.1.1
28
30
  Requires-Dist: requests>=2.32.3
@@ -35,37 +37,38 @@ Provides-Extra: dev
35
37
  Requires-Dist: actionlint-py==1.7.12.24; extra == "dev"
36
38
  Requires-Dist: check-manifest==0.51; extra == "dev"
37
39
  Requires-Dist: check-wheel-contents==0.6.3; extra == "dev"
38
- Requires-Dist: coverage==7.15.4; extra == "dev"
40
+ Requires-Dist: coverage==7.16.0; extra == "dev"
39
41
  Requires-Dist: deptry==0.25.1; extra == "dev"
40
42
  Requires-Dist: dirty-equals==0.11; extra == "dev"
41
43
  Requires-Dist: doc8==2.0.0; extra == "dev"
42
- Requires-Dist: doccmd==2026.8.16; extra == "dev"
44
+ Requires-Dist: doccmd==2026.8.30.1; extra == "dev"
43
45
  Requires-Dist: docker==7.2.0; extra == "dev"
44
46
  Requires-Dist: freezegun==1.5.5; extra == "dev"
45
47
  Requires-Dist: furo==2025.12.19; extra == "dev"
48
+ Requires-Dist: hadolint-coatl==2.15.1; extra == "dev"
46
49
  Requires-Dist: interrogate==1.7.0; extra == "dev"
47
50
  Requires-Dist: mypy[faster-cache]==2.3.1; extra == "dev"
48
- Requires-Dist: mypy-strict-kwargs==2026.7.19.1; extra == "dev"
49
- Requires-Dist: no-defaults==2.1.0; extra == "dev"
50
- Requires-Dist: prek==0.4.14; extra == "dev"
51
+ Requires-Dist: mypy-strict-kwargs==2026.8.25.1; extra == "dev"
52
+ Requires-Dist: no-defaults==2.3.0; extra == "dev"
53
+ Requires-Dist: prek==0.5.1; extra == "dev"
51
54
  Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
52
55
  Requires-Dist: pydocstyle==6.3; extra == "dev"
53
- Requires-Dist: pylint[spelling]==4.0.7; extra == "dev"
56
+ Requires-Dist: pylint[spelling]==4.0.8; extra == "dev"
54
57
  Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
55
- Requires-Dist: pyproject-fmt==2.28.0; extra == "dev"
58
+ Requires-Dist: pyproject-fmt==2.29.2; extra == "dev"
56
59
  Requires-Dist: pyrefly==1.2.0; extra == "dev"
57
60
  Requires-Dist: pyright==1.1.411; extra == "dev"
58
61
  Requires-Dist: pyroma==5.0.1; extra == "dev"
59
62
  Requires-Dist: pytest==9.1.1; extra == "dev"
60
63
  Requires-Dist: pytest-beartype-tests==2026.8.16; extra == "dev"
61
- Requires-Dist: pytest-partition-check==2026.8.10.1; extra == "dev"
64
+ Requires-Dist: pytest-partition-check==2026.8.23; extra == "dev"
62
65
  Requires-Dist: pytest-retry==1.7.0; extra == "dev"
63
66
  Requires-Dist: pytest-xdist==3.8.0; extra == "dev"
64
67
  Requires-Dist: pyyaml==6.0.3; extra == "dev"
65
68
  Requires-Dist: requests-mock-flask==2026.8.16; extra == "dev"
66
- Requires-Dist: ruff==0.16.4; extra == "dev"
69
+ Requires-Dist: ruff==0.16.5; extra == "dev"
67
70
  Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
68
- Requires-Dist: shfmt-py==4.0.0; extra == "dev"
71
+ Requires-Dist: shfmt-py==4.1.0; extra == "dev"
69
72
  Requires-Dist: sphinx==9.1.0; extra == "dev"
70
73
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
71
74
  Requires-Dist: sphinx-lint==1.0.2; extra == "dev"
@@ -76,22 +79,22 @@ Requires-Dist: sphinx-toolbox==4.3.0; extra == "dev"
76
79
  Requires-Dist: sphinxcontrib-httpdomain==2.0.0; extra == "dev"
77
80
  Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
78
81
  Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
79
- Requires-Dist: strict-kwargs==2026.8.16; extra == "dev"
82
+ Requires-Dist: strict-kwargs==2026.8.28.post2; extra == "dev"
80
83
  Requires-Dist: sybil==10.1.0; extra == "dev"
81
84
  Requires-Dist: tenacity==9.1.4; extra == "dev"
82
85
  Requires-Dist: towncrier==25.8.0; extra == "dev"
83
- Requires-Dist: ty==0.0.73; extra == "dev"
84
- Requires-Dist: types-docker==7.2.0.20260819; extra == "dev"
86
+ Requires-Dist: ty==0.0.77; extra == "dev"
87
+ Requires-Dist: types-docker==7.2.0.20260827; extra == "dev"
85
88
  Requires-Dist: types-pyyaml==6.0.12.20260815; extra == "dev"
86
89
  Requires-Dist: types-requests==2.33.0.20260712; extra == "dev"
87
90
  Requires-Dist: urllib3==2.7.0; extra == "dev"
88
- Requires-Dist: vale==3.17.1.0; extra == "dev"
91
+ Requires-Dist: vale==3.19.0.0; extra == "dev"
89
92
  Requires-Dist: vulture==2.16; extra == "dev"
90
- Requires-Dist: vws-python==2026.8.14; extra == "dev"
91
- Requires-Dist: vws-test-fixtures==2026.8.23; extra == "dev"
92
- Requires-Dist: vws-web-tools==2026.8.7; extra == "dev"
93
+ Requires-Dist: vws-python==2026.8.26; extra == "dev"
94
+ Requires-Dist: vws-test-fixtures==2026.8.26; extra == "dev"
95
+ Requires-Dist: vws-web-tools==2026.8.27; extra == "dev"
93
96
  Requires-Dist: yamlfix==1.19.1; extra == "dev"
94
- Requires-Dist: zizmor==1.29.0; extra == "dev"
97
+ Requires-Dist: zizmor==1.30.0; extra == "dev"
95
98
  Provides-Extra: release
96
99
  Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
97
100
  Requires-Dist: towncrier==25.8.0; extra == "release"
@@ -110,7 +113,7 @@ Mock for the Vuforia Web Services (VWS) API, the Vuforia Web Query API, and the
110
113
  Mocking calls made to Vuforia
111
114
  ------------------------------
112
115
 
113
- ``MockVWS`` intercepts requests made with `requests`_ or `httpx`_.
116
+ ``MockVWS`` intercepts requests made with `requests`_, `httpx`_ or `HTTPX2`_.
114
117
 
115
118
  .. code-block:: shell
116
119
 
@@ -150,10 +153,30 @@ This requires Python |minimum-python-version|\+.
150
153
  # This will use the Vuforia mock.
151
154
  httpx.get(url="https://vws.vuforia.com/summary", timeout=30)
152
155
 
156
+ ``MockVWS`` also intercepts `HTTPX2`_ requests, with no need for ``httpx2.alias_httpx()``:
157
+
158
+ .. code-block:: python
159
+
160
+ """Make a request to the Vuforia Web Services API mock using httpx2."""
161
+
162
+ import httpx2
163
+
164
+ from mock_vws import MockVWS
165
+ from mock_vws.database import CloudDatabase
166
+
167
+ with MockVWS() as mock:
168
+ database = CloudDatabase()
169
+ mock.add_cloud_database(cloud_database=database)
170
+ # This will use the Vuforia mock.
171
+ httpx2.get(url="https://vws.vuforia.com/summary", timeout=30)
172
+
173
+ Asynchronous ``httpx`` and `HTTPX2`_ clients are intercepted as well.
174
+
153
175
  By default, an exception will be raised if any requests to unmocked addresses are made.
154
176
 
155
177
  .. _requests: https://pypi.org/project/requests/
156
178
  .. _httpx: https://pypi.org/project/httpx/
179
+ .. _HTTPX2: https://httpx2.pydantic.dev/
157
180
 
158
181
  Using Docker to mock calls to Vuforia from any language
159
182
  -------------------------------------------------------
@@ -11,7 +11,7 @@ Mock for the Vuforia Web Services (VWS) API, the Vuforia Web Query API, and the
11
11
  Mocking calls made to Vuforia
12
12
  ------------------------------
13
13
 
14
- ``MockVWS`` intercepts requests made with `requests`_ or `httpx`_.
14
+ ``MockVWS`` intercepts requests made with `requests`_, `httpx`_ or `HTTPX2`_.
15
15
 
16
16
  .. code-block:: shell
17
17
 
@@ -51,10 +51,30 @@ This requires Python |minimum-python-version|\+.
51
51
  # This will use the Vuforia mock.
52
52
  httpx.get(url="https://vws.vuforia.com/summary", timeout=30)
53
53
 
54
+ ``MockVWS`` also intercepts `HTTPX2`_ requests, with no need for ``httpx2.alias_httpx()``:
55
+
56
+ .. code-block:: python
57
+
58
+ """Make a request to the Vuforia Web Services API mock using httpx2."""
59
+
60
+ import httpx2
61
+
62
+ from mock_vws import MockVWS
63
+ from mock_vws.database import CloudDatabase
64
+
65
+ with MockVWS() as mock:
66
+ database = CloudDatabase()
67
+ mock.add_cloud_database(cloud_database=database)
68
+ # This will use the Vuforia mock.
69
+ httpx2.get(url="https://vws.vuforia.com/summary", timeout=30)
70
+
71
+ Asynchronous ``httpx`` and `HTTPX2`_ clients are intercepted as well.
72
+
54
73
  By default, an exception will be raised if any requests to unmocked addresses are made.
55
74
 
56
75
  .. _requests: https://pypi.org/project/requests/
57
76
  .. _httpx: https://pypi.org/project/httpx/
77
+ .. _HTTPX2: https://httpx2.pydantic.dev/
58
78
 
59
79
  Using Docker to mock calls to Vuforia from any language
60
80
  -------------------------------------------------------
@@ -28,5 +28,18 @@ def pytest_set_filtered_exceptions() -> tuple[type[Exception], ...]:
28
28
 
29
29
  This is for ``pytest-retry``.
30
30
  The configuration for retries is in ``pyproject.toml``.
31
+
32
+ ``pytest-retry`` treats this as an allowlist: a test which fails
33
+ with anything else is not retried, whatever ``retries`` is set to.
34
+ That is deliberate, because widening it to ``AssertionError`` would
35
+ retry every failing assertion in the suite ten times.
36
+
37
+ The consequence is that a test which turns a response into an
38
+ ``AssertionError`` itself, rather than letting an exception out of
39
+ the client, is never retried here. The Model Target Web API tests
40
+ assert on ``requests.Response`` objects and so are in that
41
+ position; they get their own, narrower policy, applied before the
42
+ assertion, in
43
+ :py:mod:`tests.mock_vws.utils.model_target_retries`.
31
44
  """
32
45
  return TRANSIENT_VWS_EXCEPTIONS
@@ -1,4 +1,4 @@
1
- ``MockVWS`` intercepts requests to Vuforia made with `requests`_ or `httpx`_.
1
+ ``MockVWS`` intercepts requests to Vuforia made with `requests`_, `httpx`_ or `HTTPX2`_.
2
2
 
3
3
  .. code-block:: python
4
4
 
@@ -47,3 +47,4 @@ See :ref:`mock-api-reference` for details of what can be changed and how.
47
47
 
48
48
  .. _requests: https://pypi.org/project/requests/
49
49
  .. _httpx: https://pypi.org/project/httpx/
50
+ .. _HTTPX2: https://httpx2.pydantic.dev/
@@ -124,6 +124,42 @@ Use the following custom ``pytest`` options to skip some tests:
124
124
  --skip-docker_build_tests
125
125
  Skip tests for building Docker images
126
126
 
127
+ Verifying signed Model Target requests
128
+ --------------------------------------
129
+
130
+ Creating an advanced Model Target dataset with a state-based configuration or a standard dataset with inline CAD data is a "signed" request: the real Vuforia signs the trained dataset, and each signing consumes the account's Model Target training allowance.
131
+ The allowance is small (roughly 20 signings), it is shared by every CI job and every concurrent run, and it cannot be raised or reset.
132
+ Verifying signed requests on every run exhausted the allowance within hours and then made every CI run fail with ``TRAINING_ALLOWANCE_EXCEEDED``.
133
+
134
+ The signed test cases therefore run against the mock backends on every run, but are skipped against the real Vuforia by default.
135
+ To verify them against the real Vuforia, for example after the allowance has recovered, opt in with:
136
+
137
+ .. code-block:: text
138
+
139
+ --verify-model-target-signing
140
+ Run signed Model Target dataset tests against
141
+ the real Vuforia
142
+
143
+ The equivalent unsigned requests (a standard dataset, or an advanced dataset without a state-based configuration) are far cheaper and are verified against the real Vuforia on every run.
144
+ With enough traffic even unsigned dataset creation can be rejected with ``TRAINING_ALLOWANCE_EXCEEDED``; an unexpected allowance rejection is reported as an expected failure rather than a test failure, and the affected tests pass again automatically once the allowance recovers.
145
+
146
+ Retrying transient real Model Target failures
147
+ ---------------------------------------------
148
+
149
+ The load balancer in front of the real Model Target Web API sometimes answers a perfectly good request with a gateway error, or drops the connection.
150
+ That is nothing to do with the contract under test, and a rerun of the same job passes, so a small number of retries is applied to requests which are safe to repeat.
151
+
152
+ The policy is in ``tests/mock_vws/utils/model_target_retries.py``.
153
+ It applies only while a test is running against the real Model Target backend, and only to ``GET`` requests: repeating a dataset creation can create a second dataset and can consume the account's Model Target training allowance, so mutating requests are sent exactly once.
154
+ Three attempts are made, with an exponential backoff with jitter of up to ten seconds, using ``tenacity``.
155
+ A request which still fails transiently on the last attempt is not hidden: the response is returned as it is, so the assertion reports the real status and body.
156
+
157
+ The mock backends are unaffected, so tests which configure a mock to answer with a 5xx still get that response immediately.
158
+
159
+ This is separate from the repository-wide ``pytest-retry`` configuration, which retries a test only when it fails with one of the exception types returned by ``pytest_set_filtered_exceptions`` in the top level ``conftest.py``.
160
+ The Model Target tests assert on ``requests.Response`` objects, so a gateway error reaches ``pytest-retry`` as an ``AssertionError`` and is never retried by it.
161
+ Widening that allowlist would retry every failing assertion in the suite, which is why the Model Target policy sits in the request helper instead, before the assertion.
162
+
127
163
  Documentation
128
164
  -------------
129
165
 
@@ -154,6 +190,8 @@ This is because the target goes into a processing state.
154
190
  ``image`` is required for ``POST /targets``, but it is documented as not mandatory.
155
191
 
156
192
  The ``tracking_rating`` returned by ``GET /targets/<target_id>`` can be -1.
193
+ This happens only while a newly uploaded target is processing, and it lasts for about a second.
194
+ Updating a target returns it to the processing state but does not return the rating to -1; the new image's rating is reported straight away.
157
195
 
158
196
  The database summary from ``GET /summary`` has multiple undocumented return fields.
159
197
 
@@ -4,12 +4,21 @@ Differences between the mock and the real Vuforia Web Services
4
4
  The mock attempts to be realistic, but it was built without access to the source code of the original API.
5
5
  Please report any issues `here <https://github.com/VWS-Python/vws-python-mock/issues>`__.
6
6
 
7
+ This document mixes three kinds of statement, and it says which is which:
8
+
9
+ * A deliberate difference, where the mock does something else on purpose.
10
+ The image matchers are one.
11
+ * Behavior which the mock does not implement.
12
+ * An unverified assumption, where the mock follows Vuforia's documentation and nobody has checked that the documentation is accurate.
13
+ Each of these carries a note pointing at its entry in :doc:`unverified-behavior`, which says what would verify it.
14
+ These are the ones which can bite: the mock passes its tests, your tests pass, and the divergence appears in production.
15
+
7
16
  Image matching
8
17
  --------------
9
18
 
10
19
  Vuforia's image matching is proprietary and we do not intend to accurately copy it.
11
20
  Instead, we aim for simple algorithms which are fast and are good enough for testing purposes.
12
- The image matcher is configurable, using :paramref:`~mock_vws.MockVWS.match_checker`.
21
+ The image matchers are configurable, using :paramref:`~mock_vws.MockVWS.query_match_checker` and :paramref:`~mock_vws.MockVWS.duplicate_match_checker`.
13
22
 
14
23
  Speed and summary accuracy
15
24
  --------------------------
@@ -23,7 +32,22 @@ In the real Vuforia Web Services, the processing stage takes varying lengths of
23
32
  The database summary in the real Vuforia Web Services takes some time to account for images and recognitions.
24
33
  Sometimes the real summary skips image states such as the processing state.
25
34
  The mock is accurate immediately with regards to images.
35
+
26
36
  The mock does not count recognitions.
37
+ Real Vuforia's recognition counts lag behind its queries by far longer than a
38
+ test runs, so a query in the mock does not change any count either.
39
+ Set the counts you want to see instead:
40
+ :paramref:`mock_vws.database.CloudDatabase.total_recos`,
41
+ :paramref:`mock_vws.database.CloudDatabase.current_month_recos`,
42
+ :paramref:`mock_vws.database.CloudDatabase.previous_month_recos` and
43
+ :paramref:`mock_vws.database.CloudDatabase.reco_threshold` for the database
44
+ summary report, and
45
+ :meth:`mock_vws.MockVWS.set_target_recognition_counts` for the counts of a
46
+ target, which the target summary report and the reco counts report show.
47
+ Targets are created by API requests, so their counts are set after the target
48
+ is created.
49
+ The Flask and Docker mock has an equivalent target manager endpoint, described
50
+ in :doc:`docker`.
27
51
 
28
52
  Image quality and ratings
29
53
  -------------------------
@@ -32,6 +56,10 @@ Targets are assigned a rating between 0 and 5 of how good they are for tracking
32
56
  In the mock this is calculated from the image quality, differently to how Vuforia does this.
33
57
  This is customizable with the :paramref:`~mock_vws.MockVWS.target_tracking_rater` parameter.
34
58
 
59
+ A target which is being processed after an upload reports a rating of -1 for a short time, and then the image's rating, while it is still processing.
60
+ An update does not start a new -1 window: it returns the target to the processing state and reports the new image's rating straight away.
61
+ The mock does the same, in proportion to :paramref:`~mock_vws.MockVWS.processing_time_seconds` rather than to the real timings.
62
+
35
63
  Image targets which are not suited to detection are given 'failed' statuses.
36
64
  The criteria for these images is not defined by the Vuforia documentation.
37
65
  The mock is more forgiving than the real Vuforia Web Services.
@@ -41,14 +69,14 @@ Result ordering
41
69
  ---------------
42
70
 
43
71
  The real Query API orders results by match score, with the best match first.
44
- The mock has no match score, so it cannot reproduce that order.
45
- Instead, the mock orders the targets it returns by upload date and then by target ID.
46
- This makes repeated runs agree with each other, but it means that the mock's order is not a ranking.
47
- Do not rely on the first result of a mock query being the best match.
72
+ The mock does the same, with the score which its image matcher gives each match.
73
+ That matcher is not Vuforia's, so the mock's ranking is not Vuforia's ranking.
74
+ The order decides which results survive ``max_num_results``, and which result gets target data with ``include_target_data=top``.
48
75
 
49
- This affects which results survive ``max_num_results``, and which result gets target data with ``include_target_data=top``.
76
+ Matches with the same score, which is every match of :class:`~mock_vws.image_matchers.ExactMatcher`, are ordered by upload date and then by target ID.
77
+ The real Vuforia Web Services give no such guarantee.
50
78
 
51
- ``GET /targets`` and ``GET /duplicates/{target_id}`` use the same order.
79
+ ``GET /duplicates/{target_id}`` is ordered by match score too, and ``GET /targets`` by upload date and then by target ID.
52
80
  The real Vuforia Web Services do not document an order for those endpoints.
53
81
 
54
82
  Matching recently deleted targets
@@ -111,7 +139,11 @@ NGINX Error cases
111
139
 
112
140
  Vuforia uses NGINX.
113
141
  This has error handling which is not duplicated in the mock.
114
- For example, Vuforia returns a 400 (``BAD REQUEST``) response if a header or cookie is given which is larger than 8 KiB.
142
+ For example, Vuforia is documented as returning a 400 (``BAD REQUEST``) response if a header or cookie is given which is larger than 8 KiB.
143
+
144
+ .. admonition:: Unverified assumption
145
+
146
+ :ref:`unverified-nginx-oversized-header-or-cookie`
115
147
 
116
148
  Result codes
117
149
  ------------
@@ -127,11 +159,12 @@ These are:
127
159
  Request quota exhaustion
128
160
  ------------------------
129
161
 
130
- The mock returns ``RequestQuotaReached`` when a
131
- :class:`mock_vws.database.CloudDatabase` is created with
132
- ``request_quota=0``. This behavior follows the public Vuforia documentation,
133
- but the response has not been verified against a real database with an
134
- exhausted quota.
162
+ The mock returns ``RequestQuotaReached`` when a :class:`mock_vws.database.CloudDatabase` is created with ``request_quota=0``.
163
+ This behavior follows the public Vuforia documentation.
164
+
165
+ .. admonition:: Unverified assumption
166
+
167
+ :ref:`unverified-request-quota-exhaustion`
135
168
 
136
169
  Request rate limits
137
170
  -------------------
@@ -141,11 +174,14 @@ endpoints in general, with 45 requests per second for
141
174
  ``GET /targets/{target_id}``, 10 requests per second for
142
175
  ``GET /duplicates/{target_id}``, and 1 request per minute for ``GET /targets``.
143
176
 
144
- The mock models these limits separately for each group of endpoints, but it
145
- applies no limit by default. The documented numbers have not been verified
146
- against a real database, and applying a limit of 1 request per minute to
147
- ``GET /targets`` by default would break the tests of anything which uses the
148
- mock. Set ``request_rate_limits`` to
177
+ The mock models these limits separately for each group of endpoints, but it applies no limit by default.
178
+ Applying a limit of 1 request per minute to ``GET /targets`` by default would break the tests of anything which uses the mock.
179
+
180
+ .. admonition:: Unverified assumption
181
+
182
+ :ref:`unverified-request-rate-limits`
183
+
184
+ Set ``request_rate_limits`` to
149
185
  :data:`mock_vws.request_rate_limits.DOCUMENTED_REQUEST_RATE_LIMITS` to apply
150
186
  the documented limits::
151
187
 
@@ -167,9 +203,11 @@ the documented limits::
167
203
  VWS endpoints together, and it is tracked separately from the per-endpoint
168
204
  limits.
169
205
 
170
- Vuforia also documents that ``GET /targets`` fails for databases with more than
171
- 1 million images. The mock does not implement this, as the behavior is not
172
- reproducible against a test account.
206
+ Vuforia also documents that ``GET /targets`` fails for databases with more than 1 million images, which the mock does not implement.
207
+
208
+ .. admonition:: Unverified assumption
209
+
210
+ :ref:`unverified-targets-over-one-million-images`
173
211
 
174
212
  Configurable Cloud Query failures
175
213
  ---------------------------------
@@ -178,7 +216,7 @@ The Vuforia Cloud Query API documents failure responses with JSON, arbitrary
178
216
  content, or no body. Use
179
217
  :paramref:`mock_vws.MockVWS.cloud_query_failure_response` to make every Cloud
180
218
  Query request return a particular documented failure shape through the
181
- in-process ``requests`` and ``httpx`` backends::
219
+ in-process ``requests``, ``httpx`` and ``httpx2`` backends::
182
220
 
183
221
  from mock_vws import CloudQueryFailureResponse, MockVWS
184
222
 
@@ -196,11 +234,45 @@ The configured response bypasses normal Cloud Query validation and image
196
234
  matching. Omitting it preserves the normal successful-query behavior. This
197
235
  configuration is not supported by the Flask/Docker backend.
198
236
 
237
+ Configurable Model Target failures
238
+ ----------------------------------
239
+
240
+ Use :paramref:`mock_vws.MockVWS.model_target_failure_response` to return a
241
+ particular HTTP failure from selected Model Target dataset request phases. The
242
+ OAuth2 token request is still handled normally, so this exercises client
243
+ behavior after successful token acquisition::
244
+
245
+ from mock_vws import (
246
+ MockVWS,
247
+ ModelTargetFailureResponse,
248
+ ModelTargetRequest,
249
+ )
250
+
251
+ failure = ModelTargetFailureResponse(
252
+ status_code=503,
253
+ headers={"Content-Type": "text/plain", "Retry-After": "10"},
254
+ body=b"Temporarily unavailable",
255
+ requests=frozenset({ModelTargetRequest.STATUS}),
256
+ )
257
+
258
+ with MockVWS(model_target_failure_response=failure):
259
+ # Model Target status calls return the configured response.
260
+ ...
261
+
262
+ Omit ``requests`` to affect create, status, download, and delete requests.
263
+ Other phases retain their normal behavior. This configuration works with
264
+ the in-process ``requests``, ``httpx`` and ``httpx2`` backends and is not
265
+ supported by the Flask/Docker backend.
266
+
199
267
  Other configurable result codes
200
268
  -------------------------------
201
269
 
202
- The mock also supports four other result codes which have not been verified
203
- against real databases in the corresponding states:
270
+ The mock also supports four other result codes which come from Vuforia's result codes table rather than from a response which a real database gave:
271
+
272
+ .. admonition:: Unverified assumption
273
+
274
+ :ref:`unverified-additional-result-codes`
275
+
204
276
 
205
277
  * ``TargetQuotaReached`` is returned when adding a target to a
206
278
  :class:`mock_vws.database.CloudDatabase` which already contains
@@ -318,14 +390,15 @@ Real Vuforia uses ``userId:<numeric-user-id>`` where the numeric portion is per-
318
390
  Standard and advanced routes share datasets by UUID. Access to each route
319
391
  family is separated by its corresponding OAuth scope.
320
392
 
321
- Some Model Target Web API paths remain mock-only in
322
- ``tests/mock_vws/test_model_target_web_api.py::TestAdditionalBehaviors``.
393
+ Some Model Target Web API paths remain mock-only in ``tests/mock_vws/test_model_target_web_api.py::TestAdditionalBehaviors``.
323
394
  Downloads of still-processing datasets are mock-only because exercising the path against real Vuforia would require creating a dataset on every test run; the mock drives the processing window deterministically.
324
- A download request for a dataset which is not ready reports the dataset's
325
- training status. The mock reports ``not-started`` for the whole processing
326
- window, as real Vuforia does for a dataset which was just created, and
327
- ``failed`` for a dataset whose generation failed. The name which real Vuforia
328
- reports for a failed dataset has not been observed.
395
+ A download request for a dataset which is not ready reports the dataset's training status.
396
+ The mock reports ``not-started`` for the whole processing window, as real Vuforia does for a dataset which was just created, and ``failed`` for a dataset whose generation failed.
397
+
398
+ .. admonition:: Unverified assumption
399
+
400
+ :ref:`unverified-model-target-failed-dataset-name`
401
+
329
402
  Some malformed State-Based Model Target configuration documents remain
330
403
  mock-only because real Vuforia returns an internal server error for them.
331
404
 
@@ -334,9 +407,24 @@ Reco counts reports
334
407
 
335
408
  The mock does not count recognitions, so a generated reco counts report
336
409
  contains only the ``target_id,reco_count`` header row, ending with a carriage
337
- return and a line feed.
410
+ return and a line feed, until recognition counts are set on targets.
338
411
  That is what real Vuforia returns for a database with no recognitions.
339
- The mock returns the same report for the current month and the previous month.
412
+
413
+ A report for the current month has a row for each target with a non-zero
414
+ ``current_month_recos``, and a report for the previous month has a row for
415
+ each target with a non-zero ``previous_month_recos``.
416
+ Each row ends with a carriage return and a line feed, as the header row does.
417
+ The mock takes the counts when the report is requested, so counts which are
418
+ set after that are not in that report.
419
+ The mock orders the rows by target ID.
420
+
421
+ .. admonition:: Unverified assumption
422
+
423
+ :ref:`unverified-reco-counts-report-row-order`
424
+
425
+ Setting recognition counts is mock-only, because real Vuforia's counts are
426
+ delayed for longer than a test runs, so the tests for reports with rows in
427
+ ``tests/mock_vws/test_reco_counts_report.py`` run against the mocks only.
340
428
  As with real Vuforia, the report is served with a ``text/plain`` content type
341
429
  rather than a CSV one.
342
430
 
@@ -375,8 +463,10 @@ The mock's URL returns a 404 response until the report is ready, and requires
375
463
  no authorization.
376
464
  The lack of authorization matches real Vuforia, whose URL carries its own
377
465
  signature.
378
- The 404 has not been verified, because no request for a real report has caught
379
- one before it was generated.
466
+
467
+ .. admonition:: Unverified assumption
468
+
469
+ :ref:`unverified-reco-counts-report-not-ready`
380
470
 
381
471
  Paths which the mock does not serve
382
472
  -----------------------------------
@@ -389,9 +479,9 @@ For any other request which it does not serve, such as ``DELETE /summary`` or
389
479
  and the path of the request.
390
480
  The Flask and Docker mock reproduces both response shapes.
391
481
 
392
- The ``requests`` and ``httpx`` backends mock only the paths which the mock
393
- serves, so a request to any other path raises a connection error rather than
394
- giving the 404 response which real Vuforia gives.
482
+ The ``requests``, ``httpx`` and ``httpx2`` backends mock only the paths which
483
+ the mock serves, so a request to any other path raises a connection error
484
+ rather than giving the 404 response which real Vuforia gives.
395
485
 
396
486
  Header cases
397
487
  ------------