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.
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/CHANGELOG.rst +4 -0
- {vws_python_mock-2026.2.18/src/vws_python_mock.egg-info → vws_python_mock-2026.2.18.1}/PKG-INFO +1 -1
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/differences-to-vws.rst +7 -0
- vws_python_mock-2026.2.18.1/docs/source/installation.rst +34 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_constants.py +17 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/Dockerfile +3 -7
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/vws.py +28 -3
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/image_validators.py +41 -39
- {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
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/exceptions.py +76 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1/src/vws_python_mock.egg-info}/PKG-INFO +1 -1
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/credentials.py +1 -1
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_query.py +8 -1
- vws_python_mock-2026.2.18.1/tests/mock_vws/test_vumark_generation_api.py +137 -0
- vws_python_mock-2026.2.18.1/vuforia_secrets.env.example +21 -0
- vws_python_mock-2026.2.18/docs/source/installation.rst +0 -8
- vws_python_mock-2026.2.18/tests/mock_vws/test_vumark_generation_api.py +0 -55
- vws_python_mock-2026.2.18/vuforia_secrets.env.example +0 -21
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.git_archival.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.gitattributes +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/dependabot.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/dependabot-merge.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/docker-build.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/lint.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/publish-site.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/release.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.github/workflows/test.yml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.gitignore +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.pre-commit-config.yaml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.prettierrc +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.vscode/extensions.json +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/.vscode/settings.json +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/CODE_OF_CONDUCT.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/LICENSE +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/MANIFEST.in +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/README.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/create_secrets_files.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/admin/vumark_template.svg +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/decrypt_secret.sh +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/ci/test_custom_linters.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/conftest.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docker-bake.hcl +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/basic-example.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/changelog.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/ci-setup.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/conf.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/contributing.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/docker.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/getting-started.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/index.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/mock-api-reference.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/release-process.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/versioning-and-api-stability.rst +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/pyproject.toml +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/secrets.tar.gpg +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/setup.cfg +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/spelling_private_dict.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_base64_decoding.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_database_matchers.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/healthcheck.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/target_manager.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/vwq.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_mock_common.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_tools.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/accept_header_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/auth_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/content_length_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/content_type_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/date_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/exceptions.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/fields_validators.py +0 -0
- {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
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/num_results_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_query_validators/project_state_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_requests_mock_server/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_requests_mock_server/decorators.py +0 -0
- {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
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/active_flag_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/auth_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/content_length_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/content_type_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/date_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/image_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/json_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/key_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/metadata_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/name_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/project_state_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/target_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_services_validators/width_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/database.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/image_matchers.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/py.typed +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/states.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target_manager.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/target_raters.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/SOURCES.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/dependency_links.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/not-zip-safe +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/requires.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/vws_python_mock.egg-info/top_level.txt +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/conftest.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/prepared_requests.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/vuforia_backends.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_add_target.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_authorization_header.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_content_length.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_database_summary.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_date_header.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_delete_target.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_docker.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_flask_app_usage.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_get_duplicates.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_get_target.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_invalid_given_id.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_invalid_json.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_requests_mock_usage.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_list.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_raters.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_summary.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_target_validators.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_unexpected_json.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/test_update_target.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/__init__.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/assertions.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/retries.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/too_many_requests.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/utils/usage_test_helpers.py +0 -0
- {vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/zizmor.yml +0 -0
{vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/docs/source/differences-to-vws.rst
RENAMED
|
@@ -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
|
{vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/src/mock_vws/_flask_server/Dockerfile
RENAMED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
FROM
|
|
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="$
|
|
14
|
+
ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
|
|
17
15
|
|
|
18
16
|
WORKDIR /app
|
|
19
|
-
RUN
|
|
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
|
|
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":
|
|
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=
|
|
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
|
|
23
|
+
def _parse_multipart_files(
|
|
23
24
|
*,
|
|
24
25
|
request_headers: Mapping[str, str],
|
|
25
26
|
request_body: bytes,
|
|
26
|
-
) ->
|
|
27
|
-
"""
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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":
|
|
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,
|
|
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."""
|
{vws_python_mock-2026.2.18 → vws_python_mock-2026.2.18.1}/tests/mock_vws/fixtures/credentials.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
)
|