vws-python-mock 2026.2.18__tar.gz → 2026.2.18.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 (140) hide show
  1. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/CHANGELOG.rst +8 -0
  2. {vws_python_mock-2026.2.18/src/vws_python_mock.egg-info → vws_python_mock-2026.2.18.2}/PKG-INFO +1 -1
  3. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/differences-to-vws.rst +7 -0
  4. vws_python_mock-2026.2.18.2/docs/source/installation.rst +34 -0
  5. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_constants.py +18 -0
  6. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/Dockerfile +3 -7
  7. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/vws.py +28 -3
  8. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/image_validators.py +41 -39
  9. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_requests_mock_server/mock_web_services_api.py +37 -10
  10. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/__init__.py +1 -1
  11. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/exceptions.py +116 -0
  12. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/json_validators.py +9 -2
  13. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2/src/vws_python_mock.egg-info}/PKG-INFO +1 -1
  14. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/conftest.py +1 -0
  15. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/fixtures/credentials.py +1 -1
  16. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/fixtures/prepared_requests.py +48 -0
  17. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_date_header.py +12 -0
  18. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_invalid_json.py +6 -1
  19. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_query.py +8 -1
  20. vws_python_mock-2026.2.18.2/tests/mock_vws/test_vumark_generation_api.py +137 -0
  21. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/utils/__init__.py +1 -1
  22. vws_python_mock-2026.2.18.2/vuforia_secrets.env.example +21 -0
  23. vws_python_mock-2026.2.18/docs/source/installation.rst +0 -8
  24. vws_python_mock-2026.2.18/tests/mock_vws/test_vumark_generation_api.py +0 -55
  25. vws_python_mock-2026.2.18/vuforia_secrets.env.example +0 -21
  26. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.git_archival.txt +0 -0
  27. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.gitattributes +0 -0
  28. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/dependabot.yml +0 -0
  29. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/dependabot-merge.yml +0 -0
  30. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/docker-build.yml +0 -0
  31. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/lint.yml +0 -0
  32. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/publish-site.yml +0 -0
  33. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/release.yml +0 -0
  34. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.github/workflows/test.yml +0 -0
  35. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.gitignore +0 -0
  36. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.pre-commit-config.yaml +0 -0
  37. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.prettierrc +0 -0
  38. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.vscode/extensions.json +0 -0
  39. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/.vscode/settings.json +0 -0
  40. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/CODE_OF_CONDUCT.rst +0 -0
  41. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/LICENSE +0 -0
  42. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/MANIFEST.in +0 -0
  43. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/README.rst +0 -0
  44. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/admin/__init__.py +0 -0
  45. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/admin/create_secrets_files.py +0 -0
  46. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/admin/vumark_template.svg +0 -0
  47. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/ci/__init__.py +0 -0
  48. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/ci/decrypt_secret.sh +0 -0
  49. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/ci/test_custom_linters.py +0 -0
  50. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/conftest.py +0 -0
  51. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docker-bake.hcl +0 -0
  52. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/__init__.py +0 -0
  53. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/basic-example.rst +0 -0
  54. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/changelog.rst +0 -0
  55. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/ci-setup.rst +0 -0
  56. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/conf.py +0 -0
  57. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/contributing.rst +0 -0
  58. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/docker.rst +0 -0
  59. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/getting-started.rst +0 -0
  60. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/index.rst +0 -0
  61. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/mock-api-reference.rst +0 -0
  62. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/release-process.rst +0 -0
  63. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/docs/source/versioning-and-api-stability.rst +0 -0
  64. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/pyproject.toml +0 -0
  65. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/secrets.tar.gpg +0 -0
  66. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/setup.cfg +0 -0
  67. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/spelling_private_dict.txt +0 -0
  68. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/__init__.py +0 -0
  69. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_base64_decoding.py +0 -0
  70. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_database_matchers.py +0 -0
  71. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/__init__.py +0 -0
  72. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/healthcheck.py +0 -0
  73. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/target_manager.py +0 -0
  74. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_flask_server/vwq.py +0 -0
  75. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_mock_common.py +0 -0
  76. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_tools.py +0 -0
  77. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/__init__.py +0 -0
  78. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
  79. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/auth_validators.py +0 -0
  80. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
  81. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
  82. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/date_validators.py +0 -0
  83. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/exceptions.py +0 -0
  84. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/fields_validators.py +0 -0
  85. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/include_target_data_validators.py +0 -0
  86. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
  87. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
  88. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
  89. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_requests_mock_server/decorators.py +0 -0
  90. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_requests_mock_server/mock_web_query_api.py +0 -0
  91. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
  92. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/auth_validators.py +0 -0
  93. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
  94. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
  95. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/date_validators.py +0 -0
  96. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/image_validators.py +0 -0
  97. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/key_validators.py +0 -0
  98. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
  99. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/name_validators.py +0 -0
  100. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
  101. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/target_validators.py +0 -0
  102. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/_services_validators/width_validators.py +0 -0
  103. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/database.py +0 -0
  104. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/image_matchers.py +0 -0
  105. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/py.typed +0 -0
  106. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/states.py +0 -0
  107. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/target.py +0 -0
  108. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/target_manager.py +0 -0
  109. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/mock_vws/target_raters.py +0 -0
  110. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/vws_python_mock.egg-info/SOURCES.txt +0 -0
  111. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
  112. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
  113. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/vws_python_mock.egg-info/requires.txt +0 -0
  114. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/src/vws_python_mock.egg-info/top_level.txt +0 -0
  115. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/__init__.py +0 -0
  116. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/__init__.py +0 -0
  117. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/fixtures/__init__.py +0 -0
  118. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
  119. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_add_target.py +0 -0
  120. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_authorization_header.py +0 -0
  121. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_content_length.py +0 -0
  122. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_database_summary.py +0 -0
  123. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_delete_target.py +0 -0
  124. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_docker.py +0 -0
  125. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_flask_app_usage.py +0 -0
  126. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_get_duplicates.py +0 -0
  127. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_get_target.py +0 -0
  128. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_invalid_given_id.py +0 -0
  129. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_requests_mock_usage.py +0 -0
  130. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_target_list.py +0 -0
  131. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_target_raters.py +0 -0
  132. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_target_summary.py +0 -0
  133. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_target_validators.py +0 -0
  134. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_unexpected_json.py +0 -0
  135. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/test_update_target.py +0 -0
  136. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/utils/assertions.py +0 -0
  137. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/utils/retries.py +0 -0
  138. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/utils/too_many_requests.py +0 -0
  139. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
  140. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.2}/zizmor.yml +0 -0
@@ -4,6 +4,14 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.18.2
8
+ ------------
9
+
10
+
11
+ 2026.02.18.1
12
+ ------------
13
+
14
+
7
15
  2026.02.18
8
16
  ----------
9
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python-mock
3
- Version: 2026.2.18
3
+ Version: 2026.2.18.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
@@ -103,6 +103,13 @@ These are:
103
103
 
104
104
  When the given ``Content-Length`` header does not match the length of the given data, the mock server (written with Flask) will not behave as the real Vuforia Web Services behaves.
105
105
 
106
+ VuMark instance images
107
+ ----------------------
108
+
109
+ The mock returns a fixed minimal image in the requested format.
110
+ The ``instance_id`` value is not encoded into the response image.
111
+ Real Vuforia encodes the instance ID into the VuMark pattern.
112
+
106
113
  Header cases
107
114
  ------------
108
115
 
@@ -0,0 +1,34 @@
1
+ Installation
2
+ ------------
3
+
4
+ .. code-block:: console
5
+
6
+ $ pip install vws-python-mock
7
+
8
+ This requires Python |minimum-python-version|\+.
9
+
10
+ Faster installation
11
+ ~~~~~~~~~~~~~~~~~~~
12
+
13
+ This package depends on `PyTorch`_, which pip installs from PyPI as a large CUDA-enabled build (~873 MB) even on CPU-only machines.
14
+ To get a much smaller CPU-only build (~200 MB, no CUDA dependencies), install ``torch`` and ``torchvision`` from PyTorch's CPU index before installing this package:
15
+
16
+ .. code-block:: console
17
+
18
+ $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
19
+ $ pip install vws-python-mock
20
+
21
+ If you manage dependencies with ``uv``, add the following to your ``pyproject.toml`` instead:
22
+
23
+ .. code-block:: toml
24
+
25
+ [[tool.uv.index]]
26
+ name = "pytorch-cpu"
27
+ url = "https://download.pytorch.org/whl/cpu"
28
+ explicit = true
29
+
30
+ [tool.uv.sources]
31
+ torch = { index = "pytorch-cpu" }
32
+ torchvision = { index = "pytorch-cpu" }
33
+
34
+ .. _PyTorch: https://pytorch.org
@@ -11,6 +11,21 @@ VUMARK_PNG = (
11
11
  b"\xaeB`\x82"
12
12
  )
13
13
 
14
+ VUMARK_SVG = (
15
+ b'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
16
+ )
17
+
18
+ VUMARK_PDF = (
19
+ b"%PDF-1.4\n"
20
+ b"1 0 obj<</Type /Catalog /Pages 2 0 R>>endobj\n"
21
+ b"2 0 obj<</Type /Pages /Kids [3 0 R] /Count 1>>endobj\n"
22
+ b"3 0 obj<</Type /Page /MediaBox [0 0 100 100]>>endobj\n"
23
+ b"xref\n0 4\n"
24
+ b"0000000000 65535 f \n"
25
+ b"trailer<</Size 4/Root 1 0 R>>\n"
26
+ b"startxref\n9\n%%EOF"
27
+ )
28
+
14
29
 
15
30
  @beartype
16
31
  @unique
@@ -45,6 +60,9 @@ class ResultCodes(Enum):
45
60
  PROJECT_INACTIVE = "ProjectInactive"
46
61
  INACTIVE_PROJECT = "InactiveProject"
47
62
  TOO_MANY_REQUESTS = "TooManyRequests"
63
+ INVALID_ACCEPT_HEADER = "InvalidAcceptHeader"
64
+ INVALID_INSTANCE_ID = "InvalidInstanceId"
65
+ BAD_REQUEST = "BadRequest"
48
66
 
49
67
 
50
68
  @beartype
@@ -1,24 +1,20 @@
1
- FROM python:3.13-slim AS base
1
+ FROM ghcr.io/astral-sh/uv:0.10.4-python3.13-trixie-slim AS base
2
2
  # We set this pretend version as we do not have Git in our path, and we do
3
3
  # not care enough about having the version correct inside the Docker container
4
4
  # to install it.
5
5
  ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
6
6
  # Avoid using root user.
7
- # This avoids having to use ``--root-user-action=ignore`` with pip.
8
7
  RUN useradd -ms /bin/bash myuser
9
8
  USER myuser
10
9
  COPY --chown=myuser:myuser . /app
11
10
 
12
11
  # See https://pythonspeed.com/articles/activate-virtualenv-dockerfile/
13
12
  # For why we use this method of activating the virtual environment.
14
- ENV VIRTUAL_ENV=/app/docker_venvs/.venv
15
13
  ENV UV_PROJECT_ENVIRONMENT=/app/docker_venvs/.venv
16
- ENV PATH="$VIRTUAL_ENV/bin:$PATH"
14
+ ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
17
15
 
18
16
  WORKDIR /app
19
- RUN python3 -m venv $VIRTUAL_ENV && \
20
- pip install --no-cache-dir uv==0.10.4 && \
21
- uv sync --no-cache
17
+ RUN uv sync --no-cache
22
18
  EXPOSE 5000
23
19
  ENTRYPOINT ["python"]
24
20
  HEALTHCHECK --interval=1s --timeout=10s --start-period=5s --retries=3 CMD ["python", "/app/src/mock_vws/_flask_server/healthcheck.py"]
@@ -18,12 +18,20 @@ from beartype import beartype
18
18
  from flask import Flask, Response, request
19
19
  from pydantic_settings import BaseSettings
20
20
 
21
- from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
21
+ from mock_vws._constants import (
22
+ VUMARK_PDF,
23
+ VUMARK_PNG,
24
+ VUMARK_SVG,
25
+ ResultCodes,
26
+ TargetStatuses,
27
+ )
22
28
  from mock_vws._database_matchers import get_database_matching_server_keys
23
29
  from mock_vws._mock_common import json_dump
24
30
  from mock_vws._services_validators import run_services_validators
25
31
  from mock_vws._services_validators.exceptions import (
26
32
  FailError,
33
+ InvalidAcceptHeaderError,
34
+ InvalidInstanceIdError,
27
35
  TargetStatusNotSuccessError,
28
36
  TargetStatusProcessingError,
29
37
  ValidatorError,
@@ -351,10 +359,27 @@ def generate_vumark_instance(target_id: str) -> Response:
351
359
  """
352
360
  # ``target_id`` is validated by request validators.
353
361
  del target_id
362
+
363
+ accept = request.headers.get(key="Accept", default="")
364
+ valid_accept_types: dict[str, bytes] = {
365
+ "image/png": VUMARK_PNG,
366
+ "image/svg+xml": VUMARK_SVG,
367
+ "application/pdf": VUMARK_PDF,
368
+ }
369
+ if accept not in valid_accept_types:
370
+ raise InvalidAcceptHeaderError
371
+
372
+ request_json = json.loads(s=request.data)
373
+ instance_id = request_json.get("instance_id", "")
374
+ if not instance_id:
375
+ raise InvalidInstanceIdError
376
+
377
+ response_body = valid_accept_types[accept]
378
+ content_type = accept
354
379
  date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
355
380
  headers = {
356
381
  "Connection": "keep-alive",
357
- "Content-Type": "image/png",
382
+ "Content-Type": content_type,
358
383
  "server": "envoy",
359
384
  "Date": date,
360
385
  "x-envoy-upstream-service-time": "5",
@@ -364,7 +389,7 @@ def generate_vumark_instance(target_id: str) -> Response:
364
389
  }
365
390
  return Response(
366
391
  status=HTTPStatus.OK,
367
- response=VUMARK_PNG,
392
+ response=response_body,
368
393
  headers=headers,
369
394
  )
370
395
 
@@ -7,6 +7,7 @@ from email.message import EmailMessage
7
7
 
8
8
  from beartype import beartype
9
9
  from PIL import Image
10
+ from werkzeug.datastructures import FileStorage, MultiDict
10
11
  from werkzeug.formparser import MultiPartParser
11
12
 
12
13
  from mock_vws._query_validators.exceptions import (
@@ -19,19 +20,19 @@ _LOGGER = logging.getLogger(name=__name__)
19
20
 
20
21
 
21
22
  @beartype
22
- def validate_image_field_given(
23
+ def _parse_multipart_files(
23
24
  *,
24
25
  request_headers: Mapping[str, str],
25
26
  request_body: bytes,
26
- ) -> None:
27
- """Validate that the image field is given.
27
+ ) -> MultiDict[str, FileStorage]:
28
+ """Parse the multipart body and return the files section.
28
29
 
29
30
  Args:
30
31
  request_headers: The headers sent with the request.
31
32
  request_body: The body of the request.
32
33
 
33
- Raises:
34
- ImageNotGivenError: The image field is not given.
34
+ Returns:
35
+ The files parsed from the multipart body.
35
36
  """
36
37
  email_message = EmailMessage()
37
38
  email_message["Content-Type"] = request_headers["Content-Type"]
@@ -42,6 +43,28 @@ def validate_image_field_given(
42
43
  boundary=boundary.encode(encoding="utf-8"),
43
44
  content_length=len(request_body),
44
45
  )
46
+ return files
47
+
48
+
49
+ @beartype
50
+ def validate_image_field_given(
51
+ *,
52
+ request_headers: Mapping[str, str],
53
+ request_body: bytes,
54
+ ) -> None:
55
+ """Validate that the image field is given.
56
+
57
+ Args:
58
+ request_headers: The headers sent with the request.
59
+ request_body: The body of the request.
60
+
61
+ Raises:
62
+ ImageNotGivenError: The image field is not given.
63
+ """
64
+ files = _parse_multipart_files(
65
+ request_headers=request_headers,
66
+ request_body=request_body,
67
+ )
45
68
  if files.get(key="image") is not None:
46
69
  return
47
70
 
@@ -64,14 +87,9 @@ def validate_image_file_size(
64
87
  Raises:
65
88
  RequestEntityTooLargeError: The image file size is too large.
66
89
  """
67
- email_message = EmailMessage()
68
- email_message["Content-Type"] = request_headers["Content-Type"]
69
- boundary = email_message.get_boundary(failobj="")
70
- parser = MultiPartParser()
71
- _, files = parser.parse(
72
- stream=io.BytesIO(initial_bytes=request_body),
73
- boundary=boundary.encode(encoding="utf-8"),
74
- content_length=len(request_body),
90
+ files = _parse_multipart_files(
91
+ request_headers=request_headers,
92
+ request_body=request_body,
75
93
  )
76
94
  image_part = files["image"]
77
95
  image_value = image_part.stream.read()
@@ -105,14 +123,9 @@ def validate_image_dimensions(
105
123
  BadImageError: The image is given and is not within the maximum width
106
124
  and height limits.
107
125
  """
108
- email_message = EmailMessage()
109
- email_message["Content-Type"] = request_headers["Content-Type"]
110
- boundary = email_message.get_boundary(failobj="")
111
- parser = MultiPartParser()
112
- _, files = parser.parse(
113
- stream=io.BytesIO(initial_bytes=request_body),
114
- boundary=boundary.encode(encoding="utf-8"),
115
- content_length=len(request_body),
126
+ files = _parse_multipart_files(
127
+ request_headers=request_headers,
128
+ request_body=request_body,
116
129
  )
117
130
  image_part = files["image"]
118
131
  image_value = image_part.stream.read()
@@ -142,14 +155,9 @@ def validate_image_format(
142
155
  Raises:
143
156
  BadImageError: The image is given and is not either a PNG or a JPEG.
144
157
  """
145
- email_message = EmailMessage()
146
- email_message["Content-Type"] = request_headers["Content-Type"]
147
- boundary = email_message.get_boundary(failobj="")
148
- parser = MultiPartParser()
149
- _, files = parser.parse(
150
- stream=io.BytesIO(initial_bytes=request_body),
151
- boundary=boundary.encode(encoding="utf-8"),
152
- content_length=len(request_body),
158
+ files = _parse_multipart_files(
159
+ request_headers=request_headers,
160
+ request_body=request_body,
153
161
  )
154
162
  image_part = files["image"]
155
163
  pil_image = Image.open(fp=image_part.stream)
@@ -175,17 +183,11 @@ def validate_image_is_image(
175
183
  Raises:
176
184
  BadImageError: Image data is given and it is not an image file.
177
185
  """
178
- email_message = EmailMessage()
179
- email_message["Content-Type"] = request_headers["Content-Type"]
180
- boundary = email_message.get_boundary(failobj="")
181
- parser = MultiPartParser()
182
- _, files = parser.parse(
183
- stream=io.BytesIO(initial_bytes=request_body),
184
- boundary=boundary.encode(encoding="utf-8"),
185
- content_length=len(request_body),
186
+ files = _parse_multipart_files(
187
+ request_headers=request_headers,
188
+ request_body=request_body,
186
189
  )
187
- image_part = files["image"]
188
- image_file = image_part.stream
190
+ image_file = files["image"].stream
189
191
 
190
192
  try:
191
193
  Image.open(fp=image_file)
@@ -18,12 +18,20 @@ from zoneinfo import ZoneInfo
18
18
  from beartype import BeartypeConf, beartype
19
19
  from requests.models import PreparedRequest
20
20
 
21
- from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
21
+ from mock_vws._constants import (
22
+ VUMARK_PDF,
23
+ VUMARK_PNG,
24
+ VUMARK_SVG,
25
+ ResultCodes,
26
+ TargetStatuses,
27
+ )
22
28
  from mock_vws._database_matchers import get_database_matching_server_keys
23
29
  from mock_vws._mock_common import Route, json_dump
24
30
  from mock_vws._services_validators import run_services_validators
25
31
  from mock_vws._services_validators.exceptions import (
26
32
  FailError,
33
+ InvalidAcceptHeaderError,
34
+ InvalidInstanceIdError,
27
35
  TargetStatusNotSuccessError,
28
36
  TargetStatusProcessingError,
29
37
  ValidatorError,
@@ -295,14 +303,33 @@ class MockVuforiaWebServicesAPI:
295
303
  self, request: PreparedRequest
296
304
  ) -> _ResponseType:
297
305
  """Generate a VuMark instance."""
298
- run_services_validators(
299
- request_headers=request.headers,
300
- request_body=_body_bytes(request=request),
301
- request_method=request.method or "",
302
- request_path=request.path_url,
303
- databases=self._target_manager.databases,
304
- )
306
+ valid_accept_types: dict[str, bytes] = {
307
+ "image/png": VUMARK_PNG,
308
+ "image/svg+xml": VUMARK_SVG,
309
+ "application/pdf": VUMARK_PDF,
310
+ }
311
+ try:
312
+ run_services_validators(
313
+ request_headers=request.headers,
314
+ request_body=_body_bytes(request=request),
315
+ request_method=request.method or "",
316
+ request_path=request.path_url,
317
+ databases=self._target_manager.databases,
318
+ )
319
+
320
+ accept = dict(request.headers).get("Accept", "")
321
+ if accept not in valid_accept_types:
322
+ raise InvalidAcceptHeaderError
323
+
324
+ request_json = json.loads(s=_body_bytes(request=request))
325
+ instance_id = request_json.get("instance_id", "")
326
+ if not instance_id:
327
+ raise InvalidInstanceIdError
328
+ except ValidatorError as exc:
329
+ return exc.status_code, exc.headers, exc.response_text
305
330
 
331
+ response_body = valid_accept_types[accept]
332
+ content_type = accept
306
333
  date = email.utils.formatdate(
307
334
  timeval=None,
308
335
  localtime=False,
@@ -310,7 +337,7 @@ class MockVuforiaWebServicesAPI:
310
337
  )
311
338
  headers = {
312
339
  "Connection": "keep-alive",
313
- "Content-Type": "image/png",
340
+ "Content-Type": content_type,
314
341
  "Date": date,
315
342
  "server": "envoy",
316
343
  "x-envoy-upstream-service-time": "5",
@@ -318,7 +345,7 @@ class MockVuforiaWebServicesAPI:
318
345
  "x-aws-region": "us-east-2, us-west-2",
319
346
  "x-content-type-options": "nosniff",
320
347
  }
321
- return HTTPStatus.OK, headers, VUMARK_PNG
348
+ return HTTPStatus.OK, headers, response_body
322
349
 
323
350
  @route(path_pattern="/summary", http_methods={HTTPMethod.GET})
324
351
  def database_summary(self, request: PreparedRequest) -> _ResponseType:
@@ -103,7 +103,7 @@ def run_services_validators(
103
103
  validate_date_format(request_headers=request_headers)
104
104
  validate_date_in_range(request_headers=request_headers)
105
105
 
106
- validate_json(request_body=request_body)
106
+ validate_json(request_body=request_body, request_path=request_path)
107
107
 
108
108
  validate_keys(
109
109
  request_body=request_body,
@@ -184,6 +184,46 @@ class FailError(ValidatorError):
184
184
  }
185
185
 
186
186
 
187
+ @beartype
188
+ class BadRequestError(ValidatorError):
189
+ """Exception raised when Vuforia returns a response with a result code
190
+ 'BadRequest'.
191
+ """
192
+
193
+ def __init__(self) -> None:
194
+ """
195
+ Attributes:
196
+ status_code: The status code to use in a response if this is
197
+ raised.
198
+ response_text: The response text to use in a response if this
199
+ is
200
+ raised.
201
+ """
202
+ super().__init__()
203
+ self.status_code = HTTPStatus.BAD_REQUEST
204
+ body = {
205
+ "transaction_id": uuid.uuid4().hex,
206
+ "result_code": ResultCodes.BAD_REQUEST.value,
207
+ }
208
+ self.response_text = json_dump(body=body)
209
+ date = email.utils.formatdate(
210
+ timeval=None,
211
+ localtime=False,
212
+ usegmt=True,
213
+ )
214
+ self.headers = {
215
+ "Connection": "keep-alive",
216
+ "Content-Type": "application/json",
217
+ "server": "envoy",
218
+ "Date": date,
219
+ "x-envoy-upstream-service-time": "5",
220
+ "Content-Length": str(object=len(self.response_text)),
221
+ "strict-transport-security": "max-age=31536000",
222
+ "x-aws-region": "us-east-2, us-west-2",
223
+ "x-content-type-options": "nosniff",
224
+ }
225
+
226
+
187
227
  @beartype
188
228
  class MetadataTooLargeError(ValidatorError):
189
229
  """Exception raised when Vuforia returns a response with a result code
@@ -530,6 +570,82 @@ class TargetStatusNotSuccessError(ValidatorError):
530
570
  }
531
571
 
532
572
 
573
+ @beartype
574
+ class InvalidAcceptHeaderError(ValidatorError):
575
+ """Exception raised when an unsupported Accept header is given."""
576
+
577
+ def __init__(self) -> None:
578
+ """
579
+ Attributes:
580
+ status_code: The status code to use in a response if this is
581
+ raised.
582
+ response_text: The response text to use in a response if this
583
+ is
584
+ raised.
585
+ """
586
+ super().__init__()
587
+ self.status_code = HTTPStatus.BAD_REQUEST
588
+ body = {
589
+ "transaction_id": uuid.uuid4().hex,
590
+ "result_code": ResultCodes.INVALID_ACCEPT_HEADER.value,
591
+ }
592
+ self.response_text = json_dump(body=body)
593
+ date = email.utils.formatdate(
594
+ timeval=None,
595
+ localtime=False,
596
+ usegmt=True,
597
+ )
598
+ self.headers = {
599
+ "Connection": "keep-alive",
600
+ "Content-Type": "application/json",
601
+ "server": "envoy",
602
+ "Date": date,
603
+ "x-envoy-upstream-service-time": "5",
604
+ "Content-Length": str(object=len(self.response_text)),
605
+ "strict-transport-security": "max-age=31536000",
606
+ "x-aws-region": "us-east-2, us-west-2",
607
+ "x-content-type-options": "nosniff",
608
+ }
609
+
610
+
611
+ @beartype
612
+ class InvalidInstanceIdError(ValidatorError):
613
+ """Exception raised when an invalid instance_id is given."""
614
+
615
+ def __init__(self) -> None:
616
+ """
617
+ Attributes:
618
+ status_code: The status code to use in a response if this is
619
+ raised.
620
+ response_text: The response text to use in a response if this
621
+ is
622
+ raised.
623
+ """
624
+ super().__init__()
625
+ self.status_code = HTTPStatus.UNPROCESSABLE_ENTITY
626
+ body = {
627
+ "transaction_id": uuid.uuid4().hex,
628
+ "result_code": ResultCodes.INVALID_INSTANCE_ID.value,
629
+ }
630
+ self.response_text = json_dump(body=body)
631
+ date = email.utils.formatdate(
632
+ timeval=None,
633
+ localtime=False,
634
+ usegmt=True,
635
+ )
636
+ self.headers = {
637
+ "Connection": "keep-alive",
638
+ "Content-Type": "application/json",
639
+ "server": "envoy",
640
+ "Date": date,
641
+ "x-envoy-upstream-service-time": "5",
642
+ "Content-Length": str(object=len(self.response_text)),
643
+ "strict-transport-security": "max-age=31536000",
644
+ "x-aws-region": "us-east-2, us-west-2",
645
+ "x-content-type-options": "nosniff",
646
+ }
647
+
648
+
533
649
  @beartype
534
650
  class TargetStatusProcessingError(ValidatorError):
535
651
  """Exception raised when trying to delete a target which is processing."""
@@ -8,6 +8,7 @@ from json.decoder import JSONDecodeError
8
8
  from beartype import beartype
9
9
 
10
10
  from mock_vws._services_validators.exceptions import (
11
+ BadRequestError,
11
12
  FailError,
12
13
  UnnecessaryRequestBodyError,
13
14
  )
@@ -43,14 +44,18 @@ def validate_body_given(*, request_body: bytes, request_method: str) -> None:
43
44
 
44
45
 
45
46
  @beartype
46
- def validate_json(*, request_body: bytes) -> None:
47
+ def validate_json(*, request_body: bytes, request_path: str) -> None:
47
48
  """Validate that any given body is valid JSON.
48
49
 
49
50
  Args:
50
51
  request_body: The body of the request.
52
+ request_path: The path of the request.
51
53
 
52
54
  Raises:
53
- FailError: The request body includes invalid JSON.
55
+ BadRequestError: The request body includes invalid JSON for the
56
+ VuMark instance generation endpoint.
57
+ FailError: The request body includes invalid JSON for other
58
+ endpoints.
54
59
  """
55
60
  if not request_body:
56
61
  return
@@ -59,4 +64,6 @@ def validate_json(*, request_body: bytes) -> None:
59
64
  json.loads(s=request_body.decode())
60
65
  except JSONDecodeError as exc:
61
66
  _LOGGER.warning(msg="The request body is not valid JSON.")
67
+ if request_path.endswith("/instances"):
68
+ raise BadRequestError from exc
62
69
  raise FailError(status_code=HTTPStatus.BAD_REQUEST) from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vws-python-mock
3
- Version: 2026.2.18
3
+ Version: 2026.2.18.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
@@ -85,6 +85,7 @@ def target_id(
85
85
  "target_summary",
86
86
  "update_target",
87
87
  "query",
88
+ "vumark_generate_instance",
88
89
  ],
89
90
  )
90
91
  def endpoint(request: pytest.FixtureRequest) -> Endpoint:
@@ -42,7 +42,7 @@ class _VuMarkVuforiaDatabaseSettings(BaseSettings):
42
42
  target_manager_database_name: str
43
43
  server_access_key: str
44
44
  server_secret_key: str
45
- target_id: str = "MockVuMarkTargetID00"
45
+ target_id: str
46
46
 
47
47
  model_config = SettingsConfigDict(
48
48
  env_prefix="VUMARK_VUFORIA_",