vws-python-mock 2026.8.4__py3-none-any.whl
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.
- mock_vws/__init__.py +9 -0
- mock_vws/_base64_decoding.py +35 -0
- mock_vws/_constants.py +84 -0
- mock_vws/_database_matchers.py +107 -0
- mock_vws/_flask_server/Dockerfile +32 -0
- mock_vws/_flask_server/__init__.py +1 -0
- mock_vws/_flask_server/healthcheck.py +31 -0
- mock_vws/_flask_server/target_manager.py +447 -0
- mock_vws/_flask_server/vwq.py +173 -0
- mock_vws/_flask_server/vws.py +954 -0
- mock_vws/_mock_common.py +75 -0
- mock_vws/_model_target_web_api.py +486 -0
- mock_vws/_query_tools.py +136 -0
- mock_vws/_query_validators/__init__.py +128 -0
- mock_vws/_query_validators/accept_header_validators.py +31 -0
- mock_vws/_query_validators/auth_validators.py +143 -0
- mock_vws/_query_validators/content_length_validators.py +90 -0
- mock_vws/_query_validators/content_type_validators.py +65 -0
- mock_vws/_query_validators/date_validators.py +110 -0
- mock_vws/_query_validators/exceptions.py +769 -0
- mock_vws/_query_validators/fields_validators.py +47 -0
- mock_vws/_query_validators/image_validators.py +197 -0
- mock_vws/_query_validators/include_target_data_validators.py +51 -0
- mock_vws/_query_validators/num_results_validators.py +64 -0
- mock_vws/_query_validators/project_state_validators.py +49 -0
- mock_vws/_requests_mock_server/__init__.py +1 -0
- mock_vws/_requests_mock_server/decorators.py +275 -0
- mock_vws/_requests_mock_server/mock_web_query_api.py +139 -0
- mock_vws/_requests_mock_server/mock_web_services_api.py +954 -0
- mock_vws/_respx_mock_server/__init__.py +1 -0
- mock_vws/_respx_mock_server/decorators.py +186 -0
- mock_vws/_services_validators/__init__.py +186 -0
- mock_vws/_services_validators/active_flag_validators.py +43 -0
- mock_vws/_services_validators/auth_validators.py +124 -0
- mock_vws/_services_validators/content_length_validators.py +102 -0
- mock_vws/_services_validators/content_type_validators.py +42 -0
- mock_vws/_services_validators/date_validators.py +78 -0
- mock_vws/_services_validators/exceptions.py +858 -0
- mock_vws/_services_validators/image_validators.py +220 -0
- mock_vws/_services_validators/json_validators.py +69 -0
- mock_vws/_services_validators/key_validators.py +171 -0
- mock_vws/_services_validators/metadata_validators.py +106 -0
- mock_vws/_services_validators/name_validators.py +235 -0
- mock_vws/_services_validators/project_state_validators.py +76 -0
- mock_vws/_services_validators/request_quota_validators.py +42 -0
- mock_vws/_services_validators/target_quota_validators.py +41 -0
- mock_vws/_services_validators/target_validators.py +69 -0
- mock_vws/_services_validators/width_validators.py +38 -0
- mock_vws/database.py +257 -0
- mock_vws/database_type.py +13 -0
- mock_vws/image_matchers.py +124 -0
- mock_vws/model_target.py +79 -0
- mock_vws/py.typed +0 -0
- mock_vws/states.py +20 -0
- mock_vws/target.py +285 -0
- mock_vws/target_manager.py +178 -0
- mock_vws/target_raters.py +109 -0
- vws_python_mock-2026.8.4.dist-info/METADATA +177 -0
- vws_python_mock-2026.8.4.dist-info/RECORD +62 -0
- vws_python_mock-2026.8.4.dist-info/WHEEL +5 -0
- vws_python_mock-2026.8.4.dist-info/licenses/LICENSE +21 -0
- vws_python_mock-2026.8.4.dist-info/top_level.txt +1 -0
mock_vws/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Helpers for handling Base64 like Vuforia does."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import string
|
|
6
|
+
|
|
7
|
+
from beartype import beartype
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@beartype
|
|
11
|
+
def decode_base64(encoded_data: str) -> bytes:
|
|
12
|
+
"""Decode base64 somewhat like Vuforia does.
|
|
13
|
+
|
|
14
|
+
Raises:
|
|
15
|
+
binascii.Error: Vuforia would consider this encoded data as an
|
|
16
|
+
"UNPROCESSABLE_ENTITY".
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The given data, decoded as base64.
|
|
20
|
+
"""
|
|
21
|
+
acceptable_characters = string.ascii_letters + string.digits + "+/="
|
|
22
|
+
for character in encoded_data:
|
|
23
|
+
if character not in acceptable_characters:
|
|
24
|
+
raise binascii.Error
|
|
25
|
+
|
|
26
|
+
mod_four_result_to_modified_encoded_data = {
|
|
27
|
+
0: encoded_data,
|
|
28
|
+
1: encoded_data[:-1],
|
|
29
|
+
2: f"{encoded_data}==",
|
|
30
|
+
3: f"{encoded_data}=",
|
|
31
|
+
}
|
|
32
|
+
modified_encoded_data = mod_four_result_to_modified_encoded_data[
|
|
33
|
+
len(encoded_data) % 4
|
|
34
|
+
]
|
|
35
|
+
return base64.b64decode(s=modified_encoded_data)
|
mock_vws/_constants.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Constants used to make the VWS mock."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum, unique
|
|
4
|
+
|
|
5
|
+
from beartype import beartype
|
|
6
|
+
|
|
7
|
+
VUMARK_PNG = (
|
|
8
|
+
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00"
|
|
9
|
+
b"\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx\xdac"
|
|
10
|
+
b"\xfc\xff\x1f\x00\x03\x03\x02\x00\xee\xd9\x97\xa9\x00\x00\x00\x00IEND"
|
|
11
|
+
b"\xaeB`\x82"
|
|
12
|
+
)
|
|
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
|
+
|
|
29
|
+
|
|
30
|
+
@beartype
|
|
31
|
+
@unique
|
|
32
|
+
class ResultCodes(Enum):
|
|
33
|
+
"""Constants representing various VWS result codes.
|
|
34
|
+
|
|
35
|
+
See
|
|
36
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#result-codes.
|
|
37
|
+
|
|
38
|
+
Some codes here are not documented in the above link.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
SUCCESS = "Success"
|
|
42
|
+
TARGET_CREATED = "TargetCreated"
|
|
43
|
+
AUTHENTICATION_FAILURE = "AuthenticationFailure"
|
|
44
|
+
REQUEST_TIME_TOO_SKEWED = "RequestTimeTooSkewed"
|
|
45
|
+
TARGET_NAME_EXIST = "TargetNameExist"
|
|
46
|
+
UNKNOWN_TARGET = "UnknownTarget"
|
|
47
|
+
BAD_IMAGE = "BadImage"
|
|
48
|
+
IMAGE_TOO_LARGE = "ImageTooLarge"
|
|
49
|
+
METADATA_TOO_LARGE = "MetadataTooLarge"
|
|
50
|
+
# The documentation says "Start date is after the end date" but, at the
|
|
51
|
+
# time of writing, I do not know how to trigger that, therefore this is not
|
|
52
|
+
# tested.
|
|
53
|
+
DATE_RANGE_ERROR = "DateRangeError"
|
|
54
|
+
FAIL = "Fail"
|
|
55
|
+
TARGET_STATUS_PROCESSING = "TargetStatusProcessing"
|
|
56
|
+
# This is tested only against the mock. We do not deliberately exhaust the
|
|
57
|
+
# real test database's quota because that would stop the verified-fake test
|
|
58
|
+
# suite from using it.
|
|
59
|
+
REQUEST_QUOTA_REACHED = "RequestQuotaReached"
|
|
60
|
+
TARGET_STATUS_NOT_SUCCESS = "TargetStatusNotSuccess"
|
|
61
|
+
TARGET_QUOTA_REACHED = "TargetQuotaReached"
|
|
62
|
+
PROJECT_SUSPENDED = "ProjectSuspended"
|
|
63
|
+
PROJECT_INACTIVE = "ProjectInactive"
|
|
64
|
+
PROJECT_HAS_NO_API_ACCESS = "ProjectHasNoAPIAccess"
|
|
65
|
+
INACTIVE_PROJECT = "InactiveProject"
|
|
66
|
+
TOO_MANY_REQUESTS = "TooManyRequests"
|
|
67
|
+
INVALID_ACCEPT_HEADER = "InvalidAcceptHeader"
|
|
68
|
+
INVALID_INSTANCE_ID = "InvalidInstanceId"
|
|
69
|
+
BAD_REQUEST = "BadRequest"
|
|
70
|
+
INVALID_TARGET_TYPE = "InvalidTargetType"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@beartype
|
|
74
|
+
@unique
|
|
75
|
+
class TargetStatuses(Enum):
|
|
76
|
+
"""Constants representing VWS target statuses.
|
|
77
|
+
|
|
78
|
+
See the 'status' field in
|
|
79
|
+
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
PROCESSING = "processing"
|
|
83
|
+
SUCCESS = "success"
|
|
84
|
+
FAILED = "failed"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Helpers for getting databases which match keys given in requests."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
|
|
5
|
+
from beartype import beartype
|
|
6
|
+
from vws_auth_tools import authorization_header
|
|
7
|
+
|
|
8
|
+
from mock_vws.database import CloudDatabase, VuMarkDatabase
|
|
9
|
+
|
|
10
|
+
AnyDatabase = CloudDatabase | VuMarkDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@beartype
|
|
14
|
+
def get_database_matching_client_keys(
|
|
15
|
+
*,
|
|
16
|
+
request_headers: Mapping[str, str],
|
|
17
|
+
request_body: bytes | None,
|
|
18
|
+
request_method: str,
|
|
19
|
+
request_path: str,
|
|
20
|
+
databases: Iterable[CloudDatabase],
|
|
21
|
+
) -> CloudDatabase:
|
|
22
|
+
"""Return the first of the given databases which is being accessed by
|
|
23
|
+
the
|
|
24
|
+
given client request.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
request_headers: The headers sent with the request.
|
|
28
|
+
request_body: The request body.
|
|
29
|
+
request_method: The HTTP method of the request.
|
|
30
|
+
request_path: The path of the request.
|
|
31
|
+
databases: The databases to check for matches.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The database which is being accessed by the given client request.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: No database matches the given request.
|
|
38
|
+
"""
|
|
39
|
+
request_headers_dict = dict(request_headers)
|
|
40
|
+
content_type = request_headers_dict.get("Content-Type", "").split(sep=";")[
|
|
41
|
+
0
|
|
42
|
+
]
|
|
43
|
+
auth_header = request_headers_dict.get("Authorization")
|
|
44
|
+
date = request_headers_dict.get("Date", "")
|
|
45
|
+
|
|
46
|
+
for database in databases:
|
|
47
|
+
expected_authorization_header = authorization_header(
|
|
48
|
+
access_key=database.client_access_key,
|
|
49
|
+
secret_key=database.client_secret_key,
|
|
50
|
+
method=request_method,
|
|
51
|
+
content=request_body,
|
|
52
|
+
content_type=content_type,
|
|
53
|
+
date=date,
|
|
54
|
+
request_path=request_path,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if auth_header == expected_authorization_header:
|
|
58
|
+
return database
|
|
59
|
+
raise ValueError
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@beartype
|
|
63
|
+
def get_database_matching_server_keys[DatabaseT: AnyDatabase](
|
|
64
|
+
*,
|
|
65
|
+
request_headers: Mapping[str, str],
|
|
66
|
+
request_body: bytes | None,
|
|
67
|
+
request_method: str,
|
|
68
|
+
request_path: str,
|
|
69
|
+
databases: Iterable[DatabaseT],
|
|
70
|
+
) -> DatabaseT:
|
|
71
|
+
"""Return the first of the given databases which is being accessed by
|
|
72
|
+
the
|
|
73
|
+
given server request.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
request_headers: The headers sent with the request.
|
|
77
|
+
request_body: The request body.
|
|
78
|
+
request_method: The HTTP method of the request.
|
|
79
|
+
request_path: The path of the request.
|
|
80
|
+
databases: The databases to check for matches.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The database being accessed by the given server request.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: No database matches the given request.
|
|
87
|
+
"""
|
|
88
|
+
request_headers_dict = dict(request_headers)
|
|
89
|
+
content_type_header = request_headers_dict.get("Content-Type", "")
|
|
90
|
+
content_type = content_type_header.split(sep=";")[0]
|
|
91
|
+
auth_header = request_headers_dict.get("Authorization")
|
|
92
|
+
date = request_headers_dict.get("Date", "")
|
|
93
|
+
|
|
94
|
+
for database in databases:
|
|
95
|
+
expected_authorization_header = authorization_header(
|
|
96
|
+
access_key=database.server_access_key,
|
|
97
|
+
secret_key=database.server_secret_key,
|
|
98
|
+
method=request_method,
|
|
99
|
+
content=request_body,
|
|
100
|
+
content_type=content_type,
|
|
101
|
+
date=date,
|
|
102
|
+
request_path=request_path,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if auth_header == expected_authorization_header:
|
|
106
|
+
return database
|
|
107
|
+
raise ValueError
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
FROM ghcr.io/astral-sh/uv:0.11.7-python3.14-trixie-slim AS base
|
|
2
|
+
# We set this pretend version as we do not have Git in our path, and we do
|
|
3
|
+
# not care enough about having the version correct inside the Docker container
|
|
4
|
+
# to install it.
|
|
5
|
+
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
|
|
6
|
+
# Avoid using root user.
|
|
7
|
+
RUN useradd -ms /bin/bash myuser
|
|
8
|
+
USER myuser
|
|
9
|
+
COPY --chown=myuser:myuser . /app
|
|
10
|
+
|
|
11
|
+
# See https://pythonspeed.com/articles/activate-virtualenv-dockerfile/
|
|
12
|
+
# For why we use this method of activating the virtual environment.
|
|
13
|
+
ENV UV_PROJECT_ENVIRONMENT=/app/docker_venvs/.venv
|
|
14
|
+
ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
|
|
15
|
+
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
RUN uv sync --no-cache
|
|
18
|
+
EXPOSE 5000
|
|
19
|
+
ENTRYPOINT ["python"]
|
|
20
|
+
HEALTHCHECK --interval=1s --timeout=10s --start-period=5s --retries=3 CMD ["python", "/app/src/mock_vws/_flask_server/healthcheck.py"]
|
|
21
|
+
|
|
22
|
+
FROM base AS vws
|
|
23
|
+
ENV VWS_HOST=0.0.0.0
|
|
24
|
+
CMD ["src/mock_vws/_flask_server/vws.py"]
|
|
25
|
+
|
|
26
|
+
FROM base AS vwq
|
|
27
|
+
ENV VWQ_HOST=0.0.0.0
|
|
28
|
+
CMD ["src/mock_vws/_flask_server/vwq.py"]
|
|
29
|
+
|
|
30
|
+
FROM base AS target-manager
|
|
31
|
+
ENV TARGET_MANAGER_HOST=0.0.0.0
|
|
32
|
+
CMD ["src/mock_vws/_flask_server/target_manager.py"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Flask server for the mock Vuforia web service."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Health check for the Flask server."""
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import socket
|
|
5
|
+
import sys
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
|
|
8
|
+
from beartype import beartype
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@beartype
|
|
12
|
+
def flask_app_healthy(port: int) -> bool:
|
|
13
|
+
"""Check if the Flask app is healthy."""
|
|
14
|
+
conn = http.client.HTTPConnection(host="localhost", port=port)
|
|
15
|
+
try:
|
|
16
|
+
conn.request(method="GET", url="/some-random-endpoint")
|
|
17
|
+
response = conn.getresponse()
|
|
18
|
+
except TimeoutError, http.client.HTTPException, socket.gaierror:
|
|
19
|
+
return False
|
|
20
|
+
finally:
|
|
21
|
+
conn.close()
|
|
22
|
+
|
|
23
|
+
return response.status in {
|
|
24
|
+
HTTPStatus.NOT_FOUND,
|
|
25
|
+
HTTPStatus.UNAUTHORIZED,
|
|
26
|
+
HTTPStatus.FORBIDDEN,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
sys.exit(int(not flask_app_healthy(port=5000)))
|