vws-python-mock 2026.2.18__tar.gz → 2026.2.18.1__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.1}/CHANGELOG.rst +4 -0
  2. {vws_python_mock-2026.2.18/src/vws_python_mock.egg-info → vws_python_mock-2026.2.18.1}/PKG-INFO +1 -1
  3. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/differences-to-vws.rst +7 -0
  4. vws_python_mock-2026.2.18.1/docs/source/installation.rst +34 -0
  5. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_constants.py +17 -0
  6. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/Dockerfile +3 -7
  7. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/vws.py +28 -3
  8. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/image_validators.py +41 -39
  9. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/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.1}/src/mock_vws/_services_validators/exceptions.py +76 -0
  11. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1/src/vws_python_mock.egg-info}/PKG-INFO +1 -1
  12. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/credentials.py +1 -1
  13. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_query.py +8 -1
  14. vws_python_mock-2026.2.18.1/tests/mock_vws/test_vumark_generation_api.py +137 -0
  15. vws_python_mock-2026.2.18.1/vuforia_secrets.env.example +21 -0
  16. vws_python_mock-2026.2.18/docs/source/installation.rst +0 -8
  17. vws_python_mock-2026.2.18/tests/mock_vws/test_vumark_generation_api.py +0 -55
  18. vws_python_mock-2026.2.18/vuforia_secrets.env.example +0 -21
  19. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.git_archival.txt +0 -0
  20. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.gitattributes +0 -0
  21. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/dependabot.yml +0 -0
  22. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/dependabot-merge.yml +0 -0
  23. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/docker-build.yml +0 -0
  24. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/lint.yml +0 -0
  25. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/publish-site.yml +0 -0
  26. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/release.yml +0 -0
  27. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/test.yml +0 -0
  28. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.gitignore +0 -0
  29. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.pre-commit-config.yaml +0 -0
  30. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.prettierrc +0 -0
  31. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.vscode/extensions.json +0 -0
  32. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.vscode/settings.json +0 -0
  33. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/CODE_OF_CONDUCT.rst +0 -0
  34. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/LICENSE +0 -0
  35. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/MANIFEST.in +0 -0
  36. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/README.rst +0 -0
  37. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/__init__.py +0 -0
  38. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/create_secrets_files.py +0 -0
  39. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/vumark_template.svg +0 -0
  40. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/__init__.py +0 -0
  41. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/decrypt_secret.sh +0 -0
  42. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/test_custom_linters.py +0 -0
  43. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/conftest.py +0 -0
  44. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docker-bake.hcl +0 -0
  45. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/__init__.py +0 -0
  46. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/basic-example.rst +0 -0
  47. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/changelog.rst +0 -0
  48. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/ci-setup.rst +0 -0
  49. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/conf.py +0 -0
  50. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/contributing.rst +0 -0
  51. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/docker.rst +0 -0
  52. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/getting-started.rst +0 -0
  53. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/index.rst +0 -0
  54. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/mock-api-reference.rst +0 -0
  55. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/release-process.rst +0 -0
  56. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/versioning-and-api-stability.rst +0 -0
  57. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/pyproject.toml +0 -0
  58. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/secrets.tar.gpg +0 -0
  59. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/setup.cfg +0 -0
  60. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/spelling_private_dict.txt +0 -0
  61. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/__init__.py +0 -0
  62. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_base64_decoding.py +0 -0
  63. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_database_matchers.py +0 -0
  64. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/__init__.py +0 -0
  65. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/healthcheck.py +0 -0
  66. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/target_manager.py +0 -0
  67. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/vwq.py +0 -0
  68. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_mock_common.py +0 -0
  69. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_tools.py +0 -0
  70. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/__init__.py +0 -0
  71. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
  72. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/auth_validators.py +0 -0
  73. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
  74. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
  75. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/date_validators.py +0 -0
  76. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/exceptions.py +0 -0
  77. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/fields_validators.py +0 -0
  78. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/include_target_data_validators.py +0 -0
  79. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
  80. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
  81. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
  82. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_requests_mock_server/decorators.py +0 -0
  83. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_requests_mock_server/mock_web_query_api.py +0 -0
  84. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/__init__.py +0 -0
  85. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
  86. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/auth_validators.py +0 -0
  87. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
  88. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
  89. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/date_validators.py +0 -0
  90. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/image_validators.py +0 -0
  91. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/json_validators.py +0 -0
  92. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/key_validators.py +0 -0
  93. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
  94. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/name_validators.py +0 -0
  95. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
  96. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/target_validators.py +0 -0
  97. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/width_validators.py +0 -0
  98. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/database.py +0 -0
  99. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/image_matchers.py +0 -0
  100. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/py.typed +0 -0
  101. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/states.py +0 -0
  102. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target.py +0 -0
  103. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target_manager.py +0 -0
  104. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target_raters.py +0 -0
  105. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/SOURCES.txt +0 -0
  106. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
  107. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
  108. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/requires.txt +0 -0
  109. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/top_level.txt +0 -0
  110. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/__init__.py +0 -0
  111. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/conftest.py +0 -0
  112. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/__init__.py +0 -0
  113. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/__init__.py +0 -0
  114. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/prepared_requests.py +0 -0
  115. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
  116. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_add_target.py +0 -0
  117. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_authorization_header.py +0 -0
  118. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_content_length.py +0 -0
  119. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_database_summary.py +0 -0
  120. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_date_header.py +0 -0
  121. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_delete_target.py +0 -0
  122. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_docker.py +0 -0
  123. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_flask_app_usage.py +0 -0
  124. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_get_duplicates.py +0 -0
  125. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_get_target.py +0 -0
  126. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_invalid_given_id.py +0 -0
  127. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_invalid_json.py +0 -0
  128. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_requests_mock_usage.py +0 -0
  129. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_list.py +0 -0
  130. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_raters.py +0 -0
  131. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_summary.py +0 -0
  132. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_validators.py +0 -0
  133. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_unexpected_json.py +0 -0
  134. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_update_target.py +0 -0
  135. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/__init__.py +0 -0
  136. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/assertions.py +0 -0
  137. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/retries.py +0 -0
  138. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/too_many_requests.py +0 -0
  139. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
  140. {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/zizmor.yml +0 -0
@@ -4,6 +4,10 @@ Changelog
4
4
  Next
5
5
  ----
6
6
 
7
+ 2026.02.18.1
8
+ ------------
9
+
10
+
7
11
  2026.02.18
8
12
  ----------
9
13
 
@@ -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.1
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,8 @@ 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"
48
65
 
49
66
 
50
67
  @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:
@@ -530,6 +530,82 @@ class TargetStatusNotSuccessError(ValidatorError):
530
530
  }
531
531
 
532
532
 
533
+ @beartype
534
+ class InvalidAcceptHeaderError(ValidatorError):
535
+ """Exception raised when an unsupported Accept header is given."""
536
+
537
+ def __init__(self) -> None:
538
+ """
539
+ Attributes:
540
+ status_code: The status code to use in a response if this is
541
+ raised.
542
+ response_text: The response text to use in a response if this
543
+ is
544
+ raised.
545
+ """
546
+ super().__init__()
547
+ self.status_code = HTTPStatus.BAD_REQUEST
548
+ body = {
549
+ "transaction_id": uuid.uuid4().hex,
550
+ "result_code": ResultCodes.INVALID_ACCEPT_HEADER.value,
551
+ }
552
+ self.response_text = json_dump(body=body)
553
+ date = email.utils.formatdate(
554
+ timeval=None,
555
+ localtime=False,
556
+ usegmt=True,
557
+ )
558
+ self.headers = {
559
+ "Connection": "keep-alive",
560
+ "Content-Type": "application/json",
561
+ "server": "envoy",
562
+ "Date": date,
563
+ "x-envoy-upstream-service-time": "5",
564
+ "Content-Length": str(object=len(self.response_text)),
565
+ "strict-transport-security": "max-age=31536000",
566
+ "x-aws-region": "us-east-2, us-west-2",
567
+ "x-content-type-options": "nosniff",
568
+ }
569
+
570
+
571
+ @beartype
572
+ class InvalidInstanceIdError(ValidatorError):
573
+ """Exception raised when an invalid instance_id is given."""
574
+
575
+ def __init__(self) -> None:
576
+ """
577
+ Attributes:
578
+ status_code: The status code to use in a response if this is
579
+ raised.
580
+ response_text: The response text to use in a response if this
581
+ is
582
+ raised.
583
+ """
584
+ super().__init__()
585
+ self.status_code = HTTPStatus.UNPROCESSABLE_ENTITY
586
+ body = {
587
+ "transaction_id": uuid.uuid4().hex,
588
+ "result_code": ResultCodes.INVALID_INSTANCE_ID.value,
589
+ }
590
+ self.response_text = json_dump(body=body)
591
+ date = email.utils.formatdate(
592
+ timeval=None,
593
+ localtime=False,
594
+ usegmt=True,
595
+ )
596
+ self.headers = {
597
+ "Connection": "keep-alive",
598
+ "Content-Type": "application/json",
599
+ "server": "envoy",
600
+ "Date": date,
601
+ "x-envoy-upstream-service-time": "5",
602
+ "Content-Length": str(object=len(self.response_text)),
603
+ "strict-transport-security": "max-age=31536000",
604
+ "x-aws-region": "us-east-2, us-west-2",
605
+ "x-content-type-options": "nosniff",
606
+ }
607
+
608
+
533
609
  @beartype
534
610
  class TargetStatusProcessingError(ValidatorError):
535
611
  """Exception raised when trying to delete a target which is processing."""
@@ -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.1
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
@@ -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_",
@@ -9,6 +9,7 @@ import copy
9
9
  import datetime
10
10
  import io
11
11
  import json
12
+ import re
12
13
  import textwrap
13
14
  import time
14
15
  import uuid
@@ -72,6 +73,8 @@ _JETTY_CONTENT_TYPE_ERROR = textwrap.dedent(
72
73
  """,
73
74
  )
74
75
 
76
+ _JETTY_VERSION_RE = re.compile(pattern=r"Powered by Jetty:// [\d.]+")
77
+
75
78
  _NGINX_REQUEST_ENTITY_TOO_LARGE_ERROR = textwrap.dedent(
76
79
  text="""\
77
80
  <html>\r
@@ -252,7 +255,11 @@ class TestContentType:
252
255
  if resp_status_code != HTTPStatus.INTERNAL_SERVER_ERROR:
253
256
  handle_server_errors(response=vws_response)
254
257
 
255
- assert requests_response.text == resp_text
258
+ repl = "Powered by Jetty://"
259
+ sub = _JETTY_VERSION_RE.sub
260
+ actual = sub(repl=repl, string=requests_response.text)
261
+ expected = sub(repl=repl, string=resp_text)
262
+ assert actual == expected
256
263
  assert_vwq_failure(
257
264
  response=vws_response,
258
265
  status_code=resp_status_code,
@@ -0,0 +1,137 @@
1
+ """Tests for the VuMark generation web API."""
2
+
3
+ import json
4
+ from http import HTTPMethod, HTTPStatus
5
+ from uuid import uuid4
6
+
7
+ import pytest
8
+ import requests
9
+ from vws_auth_tools import authorization_header, rfc_1123_date
10
+
11
+ from mock_vws._constants import ResultCodes
12
+ from tests.mock_vws.fixtures.credentials import VuMarkVuforiaDatabase
13
+
14
+ _VWS_HOST = "https://vws.vuforia.com"
15
+ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
16
+ _PDF_SIGNATURE = b"%PDF"
17
+ _SVG_START = b"<"
18
+
19
+
20
+ def _make_vumark_request(
21
+ *,
22
+ vumark_vuforia_database: VuMarkVuforiaDatabase,
23
+ instance_id: str,
24
+ accept: str,
25
+ ) -> requests.Response:
26
+ """Send a VuMark instance generation request and return the
27
+ response.
28
+ """
29
+ request_path = f"/targets/{vumark_vuforia_database.target_id}/instances"
30
+ content_type = "application/json"
31
+ content = json.dumps(obj={"instance_id": instance_id}).encode(
32
+ encoding="utf-8"
33
+ )
34
+ date = rfc_1123_date()
35
+ authorization_string = authorization_header(
36
+ access_key=vumark_vuforia_database.server_access_key,
37
+ secret_key=vumark_vuforia_database.server_secret_key,
38
+ method=HTTPMethod.POST,
39
+ content=content,
40
+ content_type=content_type,
41
+ date=date,
42
+ request_path=request_path,
43
+ )
44
+
45
+ return requests.post(
46
+ url=_VWS_HOST + request_path,
47
+ headers={
48
+ "Accept": accept,
49
+ "Authorization": authorization_string,
50
+ "Content-Length": str(object=len(content)),
51
+ "Content-Type": content_type,
52
+ "Date": date,
53
+ },
54
+ data=content,
55
+ timeout=30,
56
+ )
57
+
58
+
59
+ @pytest.mark.usefixtures("verify_mock_vuforia")
60
+ class TestGenerateInstance:
61
+ """Tests for the VuMark instance generation endpoint."""
62
+
63
+ @pytest.mark.parametrize(
64
+ argnames=("accept", "expected_content_type", "expected_signature"),
65
+ argvalues=[
66
+ pytest.param("image/png", "image/png", _PNG_SIGNATURE, id="png"),
67
+ pytest.param(
68
+ "image/svg+xml",
69
+ "image/svg+xml",
70
+ _SVG_START,
71
+ id="svg",
72
+ ),
73
+ pytest.param(
74
+ "application/pdf",
75
+ "application/pdf",
76
+ _PDF_SIGNATURE,
77
+ id="pdf",
78
+ ),
79
+ ],
80
+ )
81
+ @staticmethod
82
+ def test_generate_instance_format(
83
+ accept: str,
84
+ expected_content_type: str,
85
+ expected_signature: bytes,
86
+ vumark_vuforia_database: VuMarkVuforiaDatabase,
87
+ ) -> None:
88
+ """A VuMark instance can be generated in the requested format."""
89
+ response = _make_vumark_request(
90
+ vumark_vuforia_database=vumark_vuforia_database,
91
+ instance_id=uuid4().hex,
92
+ accept=accept,
93
+ )
94
+
95
+ assert response.status_code == HTTPStatus.OK
96
+ assert (
97
+ response.headers["Content-Type"].split(sep=";")[0]
98
+ == expected_content_type
99
+ )
100
+ assert response.content.strip().startswith(expected_signature)
101
+ assert len(response.content) > len(expected_signature)
102
+
103
+ @staticmethod
104
+ def test_invalid_accept_header(
105
+ vumark_vuforia_database: VuMarkVuforiaDatabase,
106
+ ) -> None:
107
+ """An unsupported Accept header returns an error."""
108
+ response = _make_vumark_request(
109
+ vumark_vuforia_database=vumark_vuforia_database,
110
+ instance_id=uuid4().hex,
111
+ accept="text/plain",
112
+ )
113
+
114
+ assert response.status_code == HTTPStatus.BAD_REQUEST
115
+ response_json = response.json()
116
+ assert (
117
+ response_json["result_code"]
118
+ == ResultCodes.INVALID_ACCEPT_HEADER.value
119
+ )
120
+
121
+ @staticmethod
122
+ def test_empty_instance_id(
123
+ vumark_vuforia_database: VuMarkVuforiaDatabase,
124
+ ) -> None:
125
+ """An empty instance_id returns InvalidInstanceId."""
126
+ response = _make_vumark_request(
127
+ vumark_vuforia_database=vumark_vuforia_database,
128
+ instance_id="",
129
+ accept="image/png",
130
+ )
131
+
132
+ assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
133
+ response_json = response.json()
134
+ assert (
135
+ response_json["result_code"]
136
+ == ResultCodes.INVALID_INSTANCE_ID.value
137
+ )