vws-python-mock 2026.2.22__tar.gz → 2026.2.22.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 (142) hide show
  1. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/CHANGELOG.rst +8 -0
  2. {vws_python_mock-2026.2.22/src/vws_python_mock.egg-info → vws_python_mock-2026.2.22.2}/PKG-INFO +1 -1
  3. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_requests_mock_server/decorators.py +10 -6
  4. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_respx_mock_server/decorators.py +21 -9
  5. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2/src/vws_python_mock.egg-info}/PKG-INFO +1 -1
  6. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_requests_mock_usage.py +82 -1
  7. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_respx_mock_usage.py +118 -1
  8. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.git_archival.txt +0 -0
  9. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.gitattributes +0 -0
  10. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/dependabot.yml +0 -0
  11. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/dependabot-merge.yml +0 -0
  12. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/docker-build.yml +0 -0
  13. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/lint.yml +0 -0
  14. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/publish-site.yml +0 -0
  15. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/release.yml +0 -0
  16. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.github/workflows/test.yml +0 -0
  17. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.gitignore +0 -0
  18. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.pre-commit-config.yaml +0 -0
  19. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.prettierrc +0 -0
  20. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.vscode/extensions.json +0 -0
  21. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/.vscode/settings.json +0 -0
  22. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/CODE_OF_CONDUCT.rst +0 -0
  23. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/LICENSE +0 -0
  24. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/MANIFEST.in +0 -0
  25. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/README.rst +0 -0
  26. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/admin/__init__.py +0 -0
  27. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/admin/create_secrets_files.py +0 -0
  28. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/admin/vumark_template.svg +0 -0
  29. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/ci/__init__.py +0 -0
  30. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/ci/decrypt_secret.sh +0 -0
  31. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/ci/test_custom_linters.py +0 -0
  32. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/conftest.py +0 -0
  33. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docker-bake.hcl +0 -0
  34. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/__init__.py +0 -0
  35. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/basic-example.rst +0 -0
  36. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/changelog.rst +0 -0
  37. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/ci-setup.rst +0 -0
  38. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/conf.py +0 -0
  39. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/contributing.rst +0 -0
  40. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/differences-to-vws.rst +0 -0
  41. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/docker.rst +0 -0
  42. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/getting-started.rst +0 -0
  43. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/httpx-example.rst +0 -0
  44. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/index.rst +0 -0
  45. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/installation.rst +0 -0
  46. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/mock-api-reference.rst +0 -0
  47. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/release-process.rst +0 -0
  48. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/docs/source/versioning-and-api-stability.rst +0 -0
  49. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/pyproject.toml +0 -0
  50. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/secrets.tar.gpg +0 -0
  51. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/setup.cfg +0 -0
  52. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/spelling_private_dict.txt +0 -0
  53. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/__init__.py +0 -0
  54. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_base64_decoding.py +0 -0
  55. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_constants.py +0 -0
  56. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_database_matchers.py +0 -0
  57. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/Dockerfile +0 -0
  58. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/__init__.py +0 -0
  59. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/healthcheck.py +0 -0
  60. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/target_manager.py +0 -0
  61. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/vwq.py +0 -0
  62. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_flask_server/vws.py +0 -0
  63. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_mock_common.py +0 -0
  64. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_tools.py +0 -0
  65. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/__init__.py +0 -0
  66. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
  67. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/auth_validators.py +0 -0
  68. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
  69. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
  70. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/date_validators.py +0 -0
  71. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/exceptions.py +0 -0
  72. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/fields_validators.py +0 -0
  73. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/image_validators.py +0 -0
  74. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/include_target_data_validators.py +0 -0
  75. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
  76. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
  77. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
  78. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_requests_mock_server/mock_web_query_api.py +0 -0
  79. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_requests_mock_server/mock_web_services_api.py +0 -0
  80. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_respx_mock_server/__init__.py +0 -0
  81. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/__init__.py +0 -0
  82. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
  83. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/auth_validators.py +0 -0
  84. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
  85. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
  86. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/date_validators.py +0 -0
  87. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/exceptions.py +0 -0
  88. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/image_validators.py +0 -0
  89. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/json_validators.py +0 -0
  90. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/key_validators.py +0 -0
  91. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
  92. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/name_validators.py +0 -0
  93. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
  94. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/target_validators.py +0 -0
  95. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/_services_validators/width_validators.py +0 -0
  96. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/database.py +0 -0
  97. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/database_type.py +0 -0
  98. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/image_matchers.py +0 -0
  99. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/py.typed +0 -0
  100. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/states.py +0 -0
  101. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/target.py +0 -0
  102. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/target_manager.py +0 -0
  103. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/mock_vws/target_raters.py +0 -0
  104. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/vws_python_mock.egg-info/SOURCES.txt +0 -0
  105. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
  106. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
  107. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/vws_python_mock.egg-info/requires.txt +0 -0
  108. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/src/vws_python_mock.egg-info/top_level.txt +0 -0
  109. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/__init__.py +0 -0
  110. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/conftest.py +0 -0
  111. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/__init__.py +0 -0
  112. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/fixtures/__init__.py +0 -0
  113. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/fixtures/credentials.py +0 -0
  114. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/fixtures/prepared_requests.py +0 -0
  115. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
  116. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_add_target.py +0 -0
  117. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_authorization_header.py +0 -0
  118. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_content_length.py +0 -0
  119. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_database_summary.py +0 -0
  120. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_date_header.py +0 -0
  121. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_delete_target.py +0 -0
  122. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_docker.py +0 -0
  123. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_flask_app_usage.py +0 -0
  124. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_get_duplicates.py +0 -0
  125. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_get_target.py +0 -0
  126. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_invalid_given_id.py +0 -0
  127. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_invalid_json.py +0 -0
  128. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_query.py +0 -0
  129. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_target_list.py +0 -0
  130. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_target_raters.py +0 -0
  131. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_target_summary.py +0 -0
  132. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_target_validators.py +0 -0
  133. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_unexpected_json.py +0 -0
  134. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_update_target.py +0 -0
  135. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/test_vumark_generation_api.py +0 -0
  136. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/utils/__init__.py +0 -0
  137. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/utils/assertions.py +0 -0
  138. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/utils/retries.py +0 -0
  139. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/utils/too_many_requests.py +0 -0
  140. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
  141. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/vuforia_secrets.env.example +0 -0
  142. {vws_python_mock-2026.2.22 → vws_python_mock-2026.2.22.2}/zizmor.yml +0 -0
@@ -4,6 +4,14 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.22.2
8
+ ------------
9
+
10
+
11
+ 2026.02.22.1
12
+ ------------
13
+
14
+
7
15
  2026.02.22
8
16
  ----------
9
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python-mock
3
- Version: 2026.2.22
3
+ Version: 2026.2.22.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
@@ -5,7 +5,7 @@ import time
5
5
  from collections.abc import Callable, Mapping
6
6
  from contextlib import ContextDecorator
7
7
  from typing import Any, Literal, Self
8
- from urllib.parse import urljoin, urlparse
8
+ from urllib.parse import urlparse
9
9
 
10
10
  import requests
11
11
  from beartype import BeartypeConf, beartype
@@ -162,6 +162,7 @@ class MockVWS(ContextDecorator):
162
162
  callback: _MockCallback,
163
163
  delay_seconds: float,
164
164
  sleep_fn: Callable[[float], None],
165
+ base_path: str,
165
166
  ) -> _ResponsesCallback:
166
167
  """Wrap a callback to add a response delay."""
167
168
 
@@ -197,9 +198,13 @@ class MockVWS(ContextDecorator):
197
198
  else:
198
199
  body_bytes = raw_body
199
200
 
201
+ path = request.path_url
202
+ if base_path and path.startswith(base_path):
203
+ path = path[len(base_path) :]
204
+
200
205
  request_data = RequestData(
201
206
  method=request.method or "",
202
- path=request.path_url,
207
+ path=path,
203
208
  headers=dict(request.headers),
204
209
  body=body_bytes,
205
210
  )
@@ -221,11 +226,9 @@ class MockVWS(ContextDecorator):
221
226
  (self._mock_vws_api, self._base_vws_url),
222
227
  (self._mock_vwq_api, self._base_vwq_url),
223
228
  ):
229
+ base_path = urlparse(url=base_url).path.rstrip("/")
224
230
  for route in api.routes:
225
- url_pattern = urljoin(
226
- base=base_url,
227
- url=f"{route.path_pattern}$",
228
- )
231
+ url_pattern = base_url.rstrip("/") + route.path_pattern + "$"
229
232
  compiled_url_pattern = re.compile(pattern=url_pattern)
230
233
 
231
234
  for http_method in route.http_methods:
@@ -237,6 +240,7 @@ class MockVWS(ContextDecorator):
237
240
  callback=original_callback,
238
241
  delay_seconds=self._response_delay_seconds,
239
242
  sleep_fn=self._sleep_fn,
243
+ base_path=base_path,
240
244
  ),
241
245
  content_type=None,
242
246
  )
@@ -5,7 +5,7 @@ import time
5
5
  from collections.abc import Callable, Mapping
6
6
  from contextlib import ContextDecorator
7
7
  from typing import Literal, Self
8
- from urllib.parse import urljoin, urlparse
8
+ from urllib.parse import urlparse
9
9
 
10
10
  import httpx
11
11
  import respx
@@ -36,19 +36,27 @@ _STRUCTURAL_SIMILARITY_MATCHER = StructuralSimilarityMatcher()
36
36
  _BRISQUE_TRACKING_RATER = BrisqueTargetTrackingRater()
37
37
 
38
38
 
39
- def _to_request_data(request: httpx.Request) -> RequestData:
39
+ def _to_request_data(
40
+ request: httpx.Request,
41
+ *,
42
+ base_path: str,
43
+ ) -> RequestData:
40
44
  """Convert an httpx.Request to a RequestData.
41
45
 
42
46
  Args:
43
47
  request: The httpx request to convert.
48
+ base_path: The base path prefix to strip from the request path.
44
49
 
45
50
  Returns:
46
51
  A RequestData with method, path, headers, and body set.
47
52
  """
53
+ path = request.url.raw_path.decode(encoding="ascii")
54
+ if base_path and path.startswith(base_path):
55
+ path = path[len(base_path) :]
48
56
  return RequestData(
49
57
  method=request.method,
50
- path=request.url.raw_path.decode(encoding="ascii"),
51
- headers=request.headers,
58
+ path=path,
59
+ headers={k.title(): v for k, v in request.headers.items()},
52
60
  body=request.content,
53
61
  )
54
62
 
@@ -155,12 +163,14 @@ class MockVWSForHttpx(ContextDecorator):
155
163
  def _make_callback(
156
164
  self,
157
165
  handler: Callable[[RequestData], _ResponseType],
166
+ base_path: str,
158
167
  ) -> Callable[[httpx.Request], httpx.Response]:
159
168
  """Create a respx-compatible callback from a handler.
160
169
 
161
170
  Args:
162
171
  handler: A handler that takes a RequestData and returns a
163
172
  response tuple.
173
+ base_path: The base path prefix to strip from the request path.
164
174
 
165
175
  Returns:
166
176
  A callback that takes an httpx.Request and returns an
@@ -183,7 +193,10 @@ class MockVWSForHttpx(ContextDecorator):
183
193
  Exception: A timeout error is raised when the response
184
194
  delay exceeds the read timeout.
185
195
  """
186
- request_data = _to_request_data(request=request)
196
+ request_data = _to_request_data(
197
+ request=request,
198
+ base_path=base_path,
199
+ )
187
200
  timeout_info: dict[str, float | None] = request.extensions.get(
188
201
  "timeout", {}
189
202
  )
@@ -237,11 +250,9 @@ class MockVWSForHttpx(ContextDecorator):
237
250
  (self._mock_vws_api, self._base_vws_url),
238
251
  (self._mock_vwq_api, self._base_vwq_url),
239
252
  ):
253
+ base_path = urlparse(url=base_url).path.rstrip("/")
240
254
  for route in api.routes:
241
- url_pattern = urljoin(
242
- base=base_url,
243
- url=f"{route.path_pattern}$",
244
- )
255
+ url_pattern = base_url.rstrip("/") + route.path_pattern + "$"
245
256
  compiled_url_pattern = re.compile(pattern=url_pattern)
246
257
 
247
258
  for http_method in route.http_methods:
@@ -252,6 +263,7 @@ class MockVWSForHttpx(ContextDecorator):
252
263
  ).mock(
253
264
  side_effect=self._make_callback(
254
265
  handler=original_callback,
266
+ base_path=base_path,
255
267
  ),
256
268
  )
257
269
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python-mock
3
- Version: 2026.2.22
3
+ Version: 2026.2.22.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
@@ -5,6 +5,7 @@ import email.utils
5
5
  import io
6
6
  import json
7
7
  import socket
8
+ from http import HTTPStatus
8
9
  from urllib.parse import urlparse
9
10
 
10
11
  import pytest
@@ -13,7 +14,7 @@ from beartype import beartype
13
14
  from freezegun import freeze_time
14
15
  from PIL import Image
15
16
  from vws import VWS, CloudRecoService
16
- from vws_auth_tools import rfc_1123_date
17
+ from vws_auth_tools import authorization_header, rfc_1123_date
17
18
 
18
19
  from mock_vws import MissingSchemeError, MockVWS
19
20
  from mock_vws.database import CloudDatabase, VuMarkDatabase
@@ -347,6 +348,86 @@ class TestCustomBaseURLs:
347
348
  timeout=30,
348
349
  )
349
350
 
351
+ @staticmethod
352
+ def test_custom_base_vws_url_with_path_prefix() -> None:
353
+ """A custom base VWS URL with a path prefix intercepts at the
354
+ prefix.
355
+ """
356
+ with MockVWS(
357
+ base_vws_url="https://vuforia.vws.example.com/prefix",
358
+ real_http=False,
359
+ ):
360
+ with pytest.raises(
361
+ expected_exception=requests.exceptions.ConnectionError
362
+ ):
363
+ requests.get(
364
+ url="https://vuforia.vws.example.com/summary",
365
+ timeout=30,
366
+ )
367
+
368
+ requests.get(
369
+ url="https://vuforia.vws.example.com/prefix/summary",
370
+ timeout=30,
371
+ )
372
+
373
+ @staticmethod
374
+ def test_custom_base_vwq_url_with_path_prefix() -> None:
375
+ """A custom base VWQ URL with a path prefix intercepts at the
376
+ prefix.
377
+ """
378
+ with MockVWS(
379
+ base_vwq_url="https://vuforia.vwq.example.com/prefix",
380
+ real_http=False,
381
+ ):
382
+ with pytest.raises(
383
+ expected_exception=requests.exceptions.ConnectionError
384
+ ):
385
+ requests.post(
386
+ url="https://vuforia.vwq.example.com/v1/query",
387
+ timeout=30,
388
+ )
389
+
390
+ requests.post(
391
+ url="https://vuforia.vwq.example.com/prefix/v1/query",
392
+ timeout=30,
393
+ )
394
+
395
+ @staticmethod
396
+ def test_vws_operations_work_with_path_prefix() -> None:
397
+ """VWS API operations work correctly with a base URL path
398
+ prefix.
399
+ """
400
+ database = CloudDatabase()
401
+ base_vws_url = "https://vuforia.vws.example.com/prefix"
402
+
403
+ with MockVWS(base_vws_url=base_vws_url) as mock:
404
+ mock.add_cloud_database(cloud_database=database)
405
+
406
+ request_path = "/targets"
407
+ date = rfc_1123_date()
408
+ auth = authorization_header(
409
+ access_key=database.server_access_key,
410
+ secret_key=database.server_secret_key,
411
+ method="GET",
412
+ content=b"",
413
+ content_type="",
414
+ date=date,
415
+ request_path=request_path,
416
+ )
417
+ response = requests.get(
418
+ url=base_vws_url + request_path,
419
+ headers={
420
+ "Authorization": auth,
421
+ "Date": date,
422
+ },
423
+ timeout=30,
424
+ )
425
+
426
+ assert response.status_code == HTTPStatus.OK
427
+ response_json = response.json()
428
+ assert response_json["result_code"] == "Success"
429
+ assert response_json["results"] == []
430
+
350
431
  @staticmethod
351
432
  def test_no_scheme() -> None:
352
433
  """An error if raised if a URL is given with no scheme."""
@@ -1,13 +1,17 @@
1
1
  """Tests for the usage of the mock for ``httpx`` via ``respx``."""
2
2
 
3
+ import json
3
4
  import socket
5
+ import uuid
6
+ from http import HTTPMethod, HTTPStatus
4
7
 
5
8
  import httpx
6
9
  import pytest
7
- from vws_auth_tools import rfc_1123_date
10
+ from vws_auth_tools import authorization_header, rfc_1123_date
8
11
 
9
12
  from mock_vws import MissingSchemeError, MockVWSForHttpx
10
13
  from mock_vws.database import CloudDatabase, VuMarkDatabase
14
+ from mock_vws.target import VuMarkTarget
11
15
 
12
16
 
13
17
  def _request_unmocked_address() -> None:
@@ -208,6 +212,82 @@ class TestCustomBaseURLs:
208
212
  timeout=30,
209
213
  )
210
214
 
215
+ @staticmethod
216
+ def test_custom_base_vws_url_with_path_prefix() -> None:
217
+ """A custom base VWS URL with a path prefix intercepts at the
218
+ prefix.
219
+ """
220
+ with MockVWSForHttpx(
221
+ base_vws_url="https://vuforia.vws.example.com/prefix",
222
+ real_http=False,
223
+ ):
224
+ with pytest.raises(expected_exception=httpx.ConnectError):
225
+ httpx.get(
226
+ url="https://vuforia.vws.example.com/summary",
227
+ timeout=30,
228
+ )
229
+
230
+ httpx.get(
231
+ url="https://vuforia.vws.example.com/prefix/summary",
232
+ timeout=30,
233
+ )
234
+
235
+ @staticmethod
236
+ def test_custom_base_vwq_url_with_path_prefix() -> None:
237
+ """A custom base VWQ URL with a path prefix intercepts at the
238
+ prefix.
239
+ """
240
+ with MockVWSForHttpx(
241
+ base_vwq_url="https://vuforia.vwq.example.com/prefix",
242
+ real_http=False,
243
+ ):
244
+ with pytest.raises(expected_exception=httpx.ConnectError):
245
+ httpx.post(
246
+ url="https://vuforia.vwq.example.com/v1/query",
247
+ timeout=30,
248
+ )
249
+
250
+ httpx.post(
251
+ url="https://vuforia.vwq.example.com/prefix/v1/query",
252
+ timeout=30,
253
+ )
254
+
255
+ @staticmethod
256
+ def test_vws_operations_work_with_path_prefix() -> None:
257
+ """VWS API operations work correctly with a base URL path
258
+ prefix.
259
+ """
260
+ database = CloudDatabase()
261
+ base_vws_url = "https://vuforia.vws.example.com/prefix"
262
+
263
+ with MockVWSForHttpx(base_vws_url=base_vws_url) as mock:
264
+ mock.add_cloud_database(cloud_database=database)
265
+
266
+ request_path = "/targets"
267
+ date = rfc_1123_date()
268
+ auth = authorization_header(
269
+ access_key=database.server_access_key,
270
+ secret_key=database.server_secret_key,
271
+ method="GET",
272
+ content=b"",
273
+ content_type="",
274
+ date=date,
275
+ request_path=request_path,
276
+ )
277
+ response = httpx.get(
278
+ url=base_vws_url + request_path,
279
+ headers={
280
+ "Authorization": auth,
281
+ "Date": date,
282
+ },
283
+ timeout=30,
284
+ )
285
+
286
+ assert response.status_code == HTTPStatus.OK
287
+ response_json = response.json()
288
+ assert response_json["result_code"] == "Success"
289
+ assert response_json["results"] == []
290
+
211
291
  @staticmethod
212
292
  def test_no_scheme() -> None:
213
293
  """An error is raised if a URL is given with no scheme."""
@@ -348,3 +428,40 @@ class TestVWSEndpoints:
348
428
  )
349
429
  # We just verify we get a response (auth will fail but endpoint works)
350
430
  assert response.status_code is not None
431
+
432
+ @staticmethod
433
+ def test_vumark_bytes_response() -> None:
434
+ """The VuMark endpoint returns bytes content via httpx."""
435
+ vumark_target = VuMarkTarget(name="test-target")
436
+ database = VuMarkDatabase(vumark_targets={vumark_target})
437
+ target_id = vumark_target.target_id
438
+ request_path = f"/targets/{target_id}/instances"
439
+ content_type = "application/json"
440
+ content = json.dumps(obj={"instance_id": uuid.uuid4().hex}).encode(
441
+ encoding="utf-8"
442
+ )
443
+ date = rfc_1123_date()
444
+ auth = authorization_header(
445
+ access_key=database.server_access_key,
446
+ secret_key=database.server_secret_key,
447
+ method=HTTPMethod.POST,
448
+ content=content,
449
+ content_type=content_type,
450
+ date=date,
451
+ request_path=request_path,
452
+ )
453
+ with MockVWSForHttpx() as mock:
454
+ mock.add_vumark_database(vumark_database=database)
455
+ response = httpx.post(
456
+ url="https://vws.vuforia.com" + request_path,
457
+ headers={
458
+ "Accept": "image/png",
459
+ "Authorization": auth,
460
+ "Content-Length": str(object=len(content)),
461
+ "Content-Type": content_type,
462
+ "Date": date,
463
+ },
464
+ content=content,
465
+ timeout=30,
466
+ )
467
+ assert response.status_code == HTTPStatus.OK