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.
Files changed (62) hide show
  1. mock_vws/__init__.py +9 -0
  2. mock_vws/_base64_decoding.py +35 -0
  3. mock_vws/_constants.py +84 -0
  4. mock_vws/_database_matchers.py +107 -0
  5. mock_vws/_flask_server/Dockerfile +32 -0
  6. mock_vws/_flask_server/__init__.py +1 -0
  7. mock_vws/_flask_server/healthcheck.py +31 -0
  8. mock_vws/_flask_server/target_manager.py +447 -0
  9. mock_vws/_flask_server/vwq.py +173 -0
  10. mock_vws/_flask_server/vws.py +954 -0
  11. mock_vws/_mock_common.py +75 -0
  12. mock_vws/_model_target_web_api.py +486 -0
  13. mock_vws/_query_tools.py +136 -0
  14. mock_vws/_query_validators/__init__.py +128 -0
  15. mock_vws/_query_validators/accept_header_validators.py +31 -0
  16. mock_vws/_query_validators/auth_validators.py +143 -0
  17. mock_vws/_query_validators/content_length_validators.py +90 -0
  18. mock_vws/_query_validators/content_type_validators.py +65 -0
  19. mock_vws/_query_validators/date_validators.py +110 -0
  20. mock_vws/_query_validators/exceptions.py +769 -0
  21. mock_vws/_query_validators/fields_validators.py +47 -0
  22. mock_vws/_query_validators/image_validators.py +197 -0
  23. mock_vws/_query_validators/include_target_data_validators.py +51 -0
  24. mock_vws/_query_validators/num_results_validators.py +64 -0
  25. mock_vws/_query_validators/project_state_validators.py +49 -0
  26. mock_vws/_requests_mock_server/__init__.py +1 -0
  27. mock_vws/_requests_mock_server/decorators.py +275 -0
  28. mock_vws/_requests_mock_server/mock_web_query_api.py +139 -0
  29. mock_vws/_requests_mock_server/mock_web_services_api.py +954 -0
  30. mock_vws/_respx_mock_server/__init__.py +1 -0
  31. mock_vws/_respx_mock_server/decorators.py +186 -0
  32. mock_vws/_services_validators/__init__.py +186 -0
  33. mock_vws/_services_validators/active_flag_validators.py +43 -0
  34. mock_vws/_services_validators/auth_validators.py +124 -0
  35. mock_vws/_services_validators/content_length_validators.py +102 -0
  36. mock_vws/_services_validators/content_type_validators.py +42 -0
  37. mock_vws/_services_validators/date_validators.py +78 -0
  38. mock_vws/_services_validators/exceptions.py +858 -0
  39. mock_vws/_services_validators/image_validators.py +220 -0
  40. mock_vws/_services_validators/json_validators.py +69 -0
  41. mock_vws/_services_validators/key_validators.py +171 -0
  42. mock_vws/_services_validators/metadata_validators.py +106 -0
  43. mock_vws/_services_validators/name_validators.py +235 -0
  44. mock_vws/_services_validators/project_state_validators.py +76 -0
  45. mock_vws/_services_validators/request_quota_validators.py +42 -0
  46. mock_vws/_services_validators/target_quota_validators.py +41 -0
  47. mock_vws/_services_validators/target_validators.py +69 -0
  48. mock_vws/_services_validators/width_validators.py +38 -0
  49. mock_vws/database.py +257 -0
  50. mock_vws/database_type.py +13 -0
  51. mock_vws/image_matchers.py +124 -0
  52. mock_vws/model_target.py +79 -0
  53. mock_vws/py.typed +0 -0
  54. mock_vws/states.py +20 -0
  55. mock_vws/target.py +285 -0
  56. mock_vws/target_manager.py +178 -0
  57. mock_vws/target_raters.py +109 -0
  58. vws_python_mock-2026.8.4.dist-info/METADATA +177 -0
  59. vws_python_mock-2026.8.4.dist-info/RECORD +62 -0
  60. vws_python_mock-2026.8.4.dist-info/WHEEL +5 -0
  61. vws_python_mock-2026.8.4.dist-info/licenses/LICENSE +21 -0
  62. vws_python_mock-2026.8.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,128 @@
1
+ """Input validators to use in the mock query API."""
2
+
3
+ from collections.abc import Iterable, Mapping
4
+
5
+ from beartype import beartype
6
+
7
+ from mock_vws.database import CloudDatabase
8
+
9
+ from .accept_header_validators import validate_accept_header
10
+ from .auth_validators import (
11
+ validate_auth_header_exists,
12
+ validate_auth_header_has_signature,
13
+ validate_auth_header_number_of_parts,
14
+ validate_authorization,
15
+ validate_client_key_exists,
16
+ )
17
+ from .content_length_validators import (
18
+ validate_content_length_header_is_int,
19
+ validate_content_length_header_not_too_large,
20
+ validate_content_length_header_not_too_small,
21
+ )
22
+ from .content_type_validators import validate_content_type_header
23
+ from .date_validators import (
24
+ validate_date_format,
25
+ validate_date_header_given,
26
+ validate_date_in_range,
27
+ )
28
+ from .fields_validators import validate_extra_fields
29
+ from .image_validators import (
30
+ validate_image_dimensions,
31
+ validate_image_field_given,
32
+ validate_image_file_size,
33
+ validate_image_format,
34
+ validate_image_is_image,
35
+ )
36
+ from .include_target_data_validators import validate_include_target_data
37
+ from .num_results_validators import validate_max_num_results
38
+ from .project_state_validators import validate_project_state
39
+
40
+
41
+ @beartype
42
+ def run_query_validators(
43
+ *,
44
+ request_path: str,
45
+ request_headers: Mapping[str, str],
46
+ request_body: bytes,
47
+ request_method: str,
48
+ databases: Iterable[CloudDatabase],
49
+ ) -> None:
50
+ """Run all validators.
51
+
52
+ Args:
53
+ request_path: The path of the request.
54
+ request_headers: The headers sent with the request.
55
+ request_body: The body of the request.
56
+ request_method: The HTTP method of the request.
57
+ databases: All Vuforia databases.
58
+ """
59
+ validate_content_length_header_is_int(request_headers=request_headers)
60
+ validate_content_length_header_not_too_large(
61
+ request_headers=request_headers,
62
+ request_body=request_body,
63
+ )
64
+ validate_content_length_header_not_too_small(
65
+ request_headers=request_headers,
66
+ request_body=request_body,
67
+ )
68
+ validate_auth_header_exists(request_headers=request_headers)
69
+ validate_auth_header_number_of_parts(request_headers=request_headers)
70
+ validate_auth_header_has_signature(request_headers=request_headers)
71
+ validate_client_key_exists(
72
+ request_headers=request_headers,
73
+ databases=databases,
74
+ )
75
+ validate_authorization(
76
+ request_headers=request_headers,
77
+ request_body=request_body,
78
+ request_method=request_method,
79
+ request_path=request_path,
80
+ databases=databases,
81
+ )
82
+ validate_project_state(
83
+ request_headers=request_headers,
84
+ request_body=request_body,
85
+ request_method=request_method,
86
+ request_path=request_path,
87
+ databases=databases,
88
+ )
89
+ validate_accept_header(request_headers=request_headers)
90
+ validate_date_header_given(request_headers=request_headers)
91
+ validate_date_format(request_headers=request_headers)
92
+ validate_date_in_range(request_headers=request_headers)
93
+ validate_content_type_header(
94
+ request_headers=request_headers,
95
+ request_body=request_body,
96
+ )
97
+ validate_extra_fields(
98
+ request_headers=request_headers,
99
+ request_body=request_body,
100
+ )
101
+ validate_image_field_given(
102
+ request_headers=request_headers,
103
+ request_body=request_body,
104
+ )
105
+ validate_image_is_image(
106
+ request_headers=request_headers,
107
+ request_body=request_body,
108
+ )
109
+ validate_image_format(
110
+ request_headers=request_headers,
111
+ request_body=request_body,
112
+ )
113
+ validate_image_dimensions(
114
+ request_headers=request_headers,
115
+ request_body=request_body,
116
+ )
117
+ validate_image_file_size(
118
+ request_headers=request_headers,
119
+ request_body=request_body,
120
+ )
121
+ validate_max_num_results(
122
+ request_headers=request_headers,
123
+ request_body=request_body,
124
+ )
125
+ validate_include_target_data(
126
+ request_headers=request_headers,
127
+ request_body=request_body,
128
+ )
@@ -0,0 +1,31 @@
1
+ """Validators for the ``Accept`` header."""
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+
6
+ from beartype import beartype
7
+
8
+ from mock_vws._query_validators.exceptions import InvalidAcceptHeaderError
9
+
10
+ _LOGGER = logging.getLogger(name=__name__)
11
+
12
+
13
+ @beartype
14
+ def validate_accept_header(request_headers: Mapping[str, str]) -> None:
15
+ """Validate the accept header.
16
+
17
+ Args:
18
+ request_headers: The headers sent with the request.
19
+
20
+ Raises:
21
+ InvalidAcceptHeaderError: The Accept header is given and is not
22
+ 'application/json' or '*/*'.
23
+ """
24
+ accept = request_headers.get("Accept")
25
+ if accept in {"application/json", "*/*", None}:
26
+ return
27
+
28
+ _LOGGER.warning(
29
+ msg="The Accept header is not 'application/json' or '*/*'.",
30
+ )
31
+ raise InvalidAcceptHeaderError
@@ -0,0 +1,143 @@
1
+ """Authorization validators to use in the mock query API."""
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Mapping
5
+
6
+ from beartype import beartype
7
+
8
+ from mock_vws._database_matchers import get_database_matching_client_keys
9
+ from mock_vws._query_validators.exceptions import (
10
+ AuthenticationFailureError,
11
+ AuthHeaderMissingError,
12
+ MalformedAuthHeaderError,
13
+ )
14
+ from mock_vws.database import CloudDatabase
15
+
16
+ _LOGGER = logging.getLogger(name=__name__)
17
+
18
+
19
+ @beartype
20
+ def validate_auth_header_exists(*, request_headers: Mapping[str, str]) -> None:
21
+ """Validate that there is an authorization header given to the query
22
+ endpoint.
23
+
24
+ Args:
25
+ request_headers: The headers sent with the request.
26
+
27
+ Raises:
28
+ AuthHeaderMissingError: There is no "Authorization" header.
29
+ """
30
+ if "Authorization" in request_headers:
31
+ return
32
+
33
+ _LOGGER.warning(msg="There is no authorization header.")
34
+ raise AuthHeaderMissingError
35
+
36
+
37
+ @beartype
38
+ def validate_auth_header_number_of_parts(
39
+ *,
40
+ request_headers: Mapping[str, str],
41
+ ) -> None:
42
+ """Validate the authorization header includes text either side of a
43
+ space.
44
+
45
+ Args:
46
+ request_headers: The headers sent with the request.
47
+
48
+ Raises:
49
+ MalformedAuthHeaderError: The "Authorization" header is not as
50
+ expected.
51
+ """
52
+ header = request_headers["Authorization"]
53
+ parts = header.split(sep=" ")
54
+ expected_number_of_parts = 2
55
+ if len(parts) == expected_number_of_parts and parts[1]:
56
+ return
57
+
58
+ _LOGGER.warning(msg="The authorization header is malformed.")
59
+ raise MalformedAuthHeaderError
60
+
61
+
62
+ @beartype
63
+ def validate_client_key_exists(
64
+ *,
65
+ request_headers: Mapping[str, str],
66
+ databases: Iterable[CloudDatabase],
67
+ ) -> None:
68
+ """Validate the authorization header includes a client key for a
69
+ database.
70
+
71
+ Args:
72
+ request_headers: The headers sent with the request.
73
+ databases: All Vuforia databases.
74
+
75
+ Raises:
76
+ AuthenticationFailureError: The client key is unknown.
77
+ """
78
+ header = request_headers["Authorization"]
79
+ first_part, _ = header.split(sep=":")
80
+ _, access_key = first_part.split(sep=" ")
81
+ for database in databases:
82
+ if access_key == database.client_access_key:
83
+ return
84
+
85
+ _LOGGER.warning(msg="The client key is unknown.")
86
+ raise AuthenticationFailureError
87
+
88
+
89
+ @beartype
90
+ def validate_auth_header_has_signature(
91
+ request_headers: Mapping[str, str],
92
+ ) -> None:
93
+ """Validate the authorization header includes a signature.
94
+
95
+ Args:
96
+ request_headers: The headers sent with the request.
97
+
98
+ Raises:
99
+ MalformedAuthHeaderError: The "Authorization" header has no signature.
100
+ """
101
+ header = request_headers["Authorization"]
102
+ if header.count(":") == 1 and header.split(sep=":")[1]:
103
+ return
104
+
105
+ _LOGGER.warning(msg="The authorization header has no signature.")
106
+ raise MalformedAuthHeaderError
107
+
108
+
109
+ @beartype
110
+ def validate_authorization(
111
+ *,
112
+ request_path: str,
113
+ request_headers: Mapping[str, str],
114
+ request_body: bytes,
115
+ request_method: str,
116
+ databases: Iterable[CloudDatabase],
117
+ ) -> None:
118
+ """Validate the authorization header given to the query endpoint.
119
+
120
+ Args:
121
+ request_path: The path of the request.
122
+ request_headers: The headers sent with the request.
123
+ request_body: The body of the request.
124
+ request_method: The HTTP method of the request.
125
+ databases: All Vuforia databases.
126
+
127
+ Raises:
128
+ AuthenticationFailureError: The "Authorization" header is not as
129
+ expected.
130
+ """
131
+ try:
132
+ get_database_matching_client_keys(
133
+ request_headers=request_headers,
134
+ request_body=request_body,
135
+ request_method=request_method,
136
+ request_path=request_path,
137
+ databases=databases,
138
+ )
139
+ except ValueError as exc:
140
+ _LOGGER.warning(
141
+ msg="The authorization header does not match any databases.",
142
+ )
143
+ raise AuthenticationFailureError from exc
@@ -0,0 +1,90 @@
1
+ """Content-Length header validators to use in the mock."""
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+
6
+ from beartype import beartype
7
+
8
+ from mock_vws._query_validators.exceptions import (
9
+ AuthenticationFailureGoodFormattingError,
10
+ ContentLengthHeaderNotIntError,
11
+ ContentLengthHeaderTooLargeError,
12
+ )
13
+
14
+ _LOGGER = logging.getLogger(name=__name__)
15
+
16
+
17
+ @beartype
18
+ def validate_content_length_header_is_int(
19
+ *,
20
+ request_headers: Mapping[str, str],
21
+ ) -> None:
22
+ """Validate the ``Content-Length`` header is an integer.
23
+
24
+ Args:
25
+ request_headers: The headers sent with the request.
26
+
27
+ Raises:
28
+ ContentLengthHeaderNotIntError: ``Content-Length`` header is not an
29
+ integer.
30
+ """
31
+ given_content_length = request_headers["Content-Length"]
32
+
33
+ try:
34
+ int(given_content_length)
35
+ except ValueError as exc:
36
+ _LOGGER.warning(msg="The Content-Length header is not an integer.")
37
+ raise ContentLengthHeaderNotIntError from exc
38
+
39
+
40
+ @beartype
41
+ def validate_content_length_header_not_too_large(
42
+ *,
43
+ request_headers: Mapping[str, str],
44
+ request_body: bytes,
45
+ ) -> None:
46
+ """Validate the ``Content-Length`` header is not too large.
47
+
48
+ Args:
49
+ request_headers: The headers sent with the request.
50
+ request_body: The body of the request.
51
+
52
+ Raises:
53
+ ContentLengthHeaderTooLargeError: The given content length header says
54
+ that the content length is greater than the body length.
55
+ """
56
+ given_content_length = request_headers["Content-Length"]
57
+
58
+ body_length = len(request_body)
59
+ given_content_length_value = int(given_content_length)
60
+ # We skip coverage here as running a test to cover this is very slow.
61
+ if given_content_length_value > body_length: # pragma: no cover
62
+ _LOGGER.warning(msg="The Content-Length header is too large.")
63
+ raise ContentLengthHeaderTooLargeError
64
+
65
+
66
+ @beartype
67
+ def validate_content_length_header_not_too_small(
68
+ *,
69
+ request_headers: Mapping[str, str],
70
+ request_body: bytes,
71
+ ) -> None:
72
+ """Validate the ``Content-Length`` header is not too small.
73
+
74
+ Args:
75
+ request_headers: The headers sent with the request.
76
+ request_body: The body of the request.
77
+
78
+ Raises:
79
+ AuthenticationFailureGoodFormattingError: The given content length
80
+ header says that the content length is smaller than the body
81
+ length.
82
+ """
83
+ given_content_length = request_headers["Content-Length"]
84
+
85
+ body_length = len(request_body)
86
+ given_content_length_value = int(given_content_length)
87
+
88
+ if given_content_length_value < body_length:
89
+ _LOGGER.warning(msg="The Content-Length header is too small.")
90
+ raise AuthenticationFailureGoodFormattingError
@@ -0,0 +1,65 @@
1
+ """Validators for the ``Content-Type`` header."""
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+ from email.message import EmailMessage
6
+
7
+ from beartype import beartype
8
+
9
+ from mock_vws._query_validators.exceptions import (
10
+ ImageNotGivenError,
11
+ NoBoundaryFoundError,
12
+ NoContentTypeError,
13
+ UnsupportedMediaTypeError,
14
+ )
15
+
16
+ _LOGGER = logging.getLogger(name=__name__)
17
+
18
+
19
+ @beartype
20
+ def validate_content_type_header(
21
+ *,
22
+ request_headers: Mapping[str, str],
23
+ request_body: bytes,
24
+ ) -> None:
25
+ """Validate the ``Content-Type`` header.
26
+
27
+ Args:
28
+ request_headers: The headers sent with the request.
29
+ request_body: The body of the request.
30
+
31
+ Raises:
32
+ UnsupportedMediaTypeError: The ``Content-Type`` header main part is not
33
+ 'multipart/form-data'.
34
+ NoBoundaryFoundError: The ``Content-Type`` header does not contain a
35
+ boundary.
36
+ ImageNotGivenError: The boundary is not in the request body.
37
+ NoContentTypeError: The content type header is either empty or not
38
+ given.
39
+ """
40
+ request_headers_dict = dict(request_headers)
41
+ content_type_header = request_headers_dict.get("Content-Type", "")
42
+ if not content_type_header:
43
+ _LOGGER.warning(msg="The content type header is empty.")
44
+ raise NoContentTypeError
45
+
46
+ email_message = EmailMessage()
47
+ email_message["Content-Type"] = request_headers["Content-Type"]
48
+ if email_message.get_content_type() not in {"multipart/form-data", "*/*"}:
49
+ _LOGGER.warning(
50
+ msg=(
51
+ "The content type header main part is not multipart/form-data."
52
+ ),
53
+ )
54
+ raise UnsupportedMediaTypeError
55
+
56
+ boundary = email_message.get_boundary()
57
+ if boundary is None:
58
+ _LOGGER.warning(
59
+ msg="The content type header does not contain a boundary.",
60
+ )
61
+ raise NoBoundaryFoundError
62
+
63
+ if boundary.encode() not in request_body:
64
+ _LOGGER.warning(msg="The boundary is not in the request body.")
65
+ raise ImageNotGivenError
@@ -0,0 +1,110 @@
1
+ """Validators of the date header to use in the mock query API."""
2
+
3
+ import contextlib
4
+ import datetime
5
+ import logging
6
+ from collections.abc import Mapping
7
+ from zoneinfo import ZoneInfo
8
+
9
+ from beartype import beartype
10
+
11
+ from mock_vws._query_validators.exceptions import (
12
+ DateFormatNotValidError,
13
+ DateHeaderNotGivenError,
14
+ RequestTimeTooSkewedError,
15
+ )
16
+
17
+ _LOGGER = logging.getLogger(name=__name__)
18
+
19
+
20
+ @beartype
21
+ def validate_date_header_given(*, request_headers: Mapping[str, str]) -> None:
22
+ """Validate the date header is given to the query endpoint.
23
+
24
+ Args:
25
+ request_headers: The headers sent with the request.
26
+
27
+ Raises:
28
+ DateHeaderNotGivenError: The date is not given.
29
+ """
30
+ if "Date" in request_headers:
31
+ return
32
+
33
+ _LOGGER.warning(msg="The date header is not given.")
34
+ raise DateHeaderNotGivenError
35
+
36
+
37
+ @beartype
38
+ def _accepted_date_formats() -> set[str]:
39
+ """Return all known accepted date formats.
40
+
41
+ We expect that more formats than this will be accepted. These are
42
+ the accepted ones we know of at the time of writing.
43
+ """
44
+ known_accepted_formats = {
45
+ "%a, %b %d %H:%M:%S %Y",
46
+ "%a %b %d %H:%M:%S %Y",
47
+ "%a, %d %b %Y %H:%M:%S",
48
+ "%a %d %b %Y %H:%M:%S",
49
+ }
50
+
51
+ return known_accepted_formats.union(
52
+ {f"{date_format} GMT" for date_format in known_accepted_formats},
53
+ )
54
+
55
+
56
+ @beartype
57
+ def validate_date_format(*, request_headers: Mapping[str, str]) -> None:
58
+ """Validate the format of the date header given to the query endpoint.
59
+
60
+ Args:
61
+ request_headers: The headers sent with the request.
62
+
63
+ Raises:
64
+ DateFormatNotValidError: The date is in the wrong format.
65
+ """
66
+ date_header = request_headers["Date"]
67
+
68
+ for date_format in _accepted_date_formats():
69
+ with contextlib.suppress(ValueError):
70
+ datetime.datetime.strptime(date_header, date_format).astimezone()
71
+ return
72
+
73
+ _LOGGER.warning(msg="The date header is in the wrong format.")
74
+ raise DateFormatNotValidError
75
+
76
+
77
+ @beartype
78
+ def validate_date_in_range(*, request_headers: Mapping[str, str]) -> None:
79
+ """Validate date in the date header given to the query endpoint.
80
+
81
+ Args:
82
+ request_headers: The headers sent with the request.
83
+
84
+ Raises:
85
+ RequestTimeTooSkewedError: The date is out of range.
86
+ """
87
+ date_header = request_headers["Date"]
88
+ gmt = ZoneInfo(key="GMT")
89
+
90
+ dates: list[datetime.datetime] = []
91
+ for date_format in _accepted_date_formats():
92
+ with contextlib.suppress(ValueError):
93
+ date = datetime.datetime.strptime(
94
+ date_header,
95
+ date_format,
96
+ ).astimezone()
97
+ dates.append(date)
98
+
99
+ date = dates[0]
100
+ now = datetime.datetime.now(tz=gmt)
101
+ date_from_header = date.replace(tzinfo=gmt)
102
+ time_difference = now - date_from_header
103
+
104
+ maximum_time_difference = datetime.timedelta(minutes=65)
105
+
106
+ if abs(time_difference) < maximum_time_difference:
107
+ return
108
+
109
+ _LOGGER.warning(msg="The date header is out of range.")
110
+ raise RequestTimeTooSkewedError