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,220 @@
1
+ """Image validators to use in the mock."""
2
+
3
+ import binascii
4
+ import io
5
+ import json
6
+ import logging
7
+ from http import HTTPStatus
8
+
9
+ from beartype import beartype
10
+ from PIL import Image
11
+
12
+ from mock_vws._base64_decoding import decode_base64
13
+ from mock_vws._services_validators.exceptions import (
14
+ BadImageError,
15
+ FailError,
16
+ ImageTooLargeError,
17
+ )
18
+
19
+ _LOGGER = logging.getLogger(name=__name__)
20
+
21
+
22
+ @beartype
23
+ def validate_image_integrity(*, request_body: bytes) -> None:
24
+ """Validate the integrity of the image given to a VWS endpoint.
25
+
26
+ Args:
27
+ request_body: The body of the request.
28
+
29
+ Raises:
30
+ BadImageError: The image is given and is not a valid image file.
31
+ """
32
+ if not request_body:
33
+ return
34
+
35
+ request_text = request_body.decode()
36
+ image = json.loads(s=request_text).get("image")
37
+ if image is None:
38
+ return
39
+
40
+ decoded = decode_base64(encoded_data=image)
41
+
42
+ image_file = io.BytesIO(initial_bytes=decoded)
43
+ with Image.open(fp=image_file) as pil_image:
44
+ try:
45
+ pil_image.verify()
46
+ except SyntaxError as exc:
47
+ _LOGGER.warning(msg="The image is not a valid image file.")
48
+ raise BadImageError from exc
49
+
50
+
51
+ @beartype
52
+ def validate_image_format(*, request_body: bytes) -> None:
53
+ """Validate the format of the image given to a VWS endpoint.
54
+
55
+ Args:
56
+ request_body: The body of the request.
57
+
58
+ Raises:
59
+ BadImageError: The image is given and is not either a PNG or a JPEG.
60
+ """
61
+ if not request_body:
62
+ return
63
+
64
+ request_text = request_body.decode()
65
+ image = json.loads(s=request_text).get("image")
66
+
67
+ if image is None:
68
+ return
69
+
70
+ decoded = decode_base64(encoded_data=image)
71
+ image_file = io.BytesIO(initial_bytes=decoded)
72
+ with Image.open(fp=image_file) as pil_image:
73
+ if pil_image.format in {"PNG", "JPEG"}:
74
+ return
75
+
76
+ _LOGGER.warning(msg="The image is not a PNG or JPEG.")
77
+ raise BadImageError
78
+
79
+
80
+ @beartype
81
+ def validate_image_color_space(*, request_body: bytes) -> None:
82
+ """Validate the color space of the image given to a VWS endpoint.
83
+
84
+ Args:
85
+ request_body: The body of the request.
86
+
87
+ Raises:
88
+ BadImageError: The image is given and is not in either the RGB or
89
+ greyscale color space.
90
+ """
91
+ if not request_body:
92
+ return
93
+
94
+ request_text = request_body.decode()
95
+ image = json.loads(s=request_text).get("image")
96
+
97
+ if image is None:
98
+ return
99
+
100
+ decoded = decode_base64(encoded_data=image)
101
+ image_file = io.BytesIO(initial_bytes=decoded)
102
+ with Image.open(fp=image_file) as pil_image:
103
+ if pil_image.mode in {"L", "RGB"}:
104
+ return
105
+
106
+ _LOGGER.warning(
107
+ msg="The image is not in the RGB or greyscale color space.",
108
+ )
109
+ raise BadImageError
110
+
111
+
112
+ @beartype
113
+ def validate_image_size(*, request_body: bytes) -> None:
114
+ """Validate the file size of the image given to a VWS endpoint.
115
+
116
+ Args:
117
+ request_body: The body of the request.
118
+
119
+ Raises:
120
+ ImageTooLargeError: The image is given and is not under a certain file
121
+ size threshold.
122
+ """
123
+ if not request_body:
124
+ return
125
+
126
+ request_text = request_body.decode()
127
+ image = json.loads(s=request_text).get("image")
128
+
129
+ if image is None:
130
+ return
131
+
132
+ decoded = decode_base64(encoded_data=image)
133
+
134
+ max_allowed_size = 2_359_293
135
+ if len(decoded) <= max_allowed_size:
136
+ return
137
+
138
+ _LOGGER.warning(msg="The image is too large.")
139
+ raise ImageTooLargeError
140
+
141
+
142
+ @beartype
143
+ def validate_image_is_image(*, request_body: bytes) -> None:
144
+ """Validate that the given image data is actually an image file.
145
+
146
+ Args:
147
+ request_body: The body of the request.
148
+
149
+ Raises:
150
+ BadImageError: Image data is given and it is not an image file.
151
+ """
152
+ if not request_body:
153
+ return
154
+
155
+ request_text = request_body.decode()
156
+ image = json.loads(s=request_text).get("image")
157
+
158
+ if image is None:
159
+ return
160
+
161
+ decoded = decode_base64(encoded_data=image)
162
+ image_file = io.BytesIO(initial_bytes=decoded)
163
+
164
+ try:
165
+ with Image.open(fp=image_file) as _:
166
+ pass
167
+ except OSError as exc:
168
+ raise BadImageError from exc
169
+
170
+
171
+ @beartype
172
+ def validate_image_encoding(*, request_body: bytes) -> None:
173
+ """Validate that the given image data can be base64 decoded.
174
+
175
+ Args:
176
+ request_body: The body of the request.
177
+
178
+ Raises:
179
+ FailError: Image data is given and it cannot be base64 decoded.
180
+ """
181
+ if not request_body:
182
+ return
183
+
184
+ request_text = request_body.decode()
185
+ if "image" not in json.loads(s=request_text):
186
+ return
187
+
188
+ image = json.loads(s=request_text).get("image")
189
+
190
+ try:
191
+ decode_base64(encoded_data=image)
192
+ except binascii.Error as exc:
193
+ _LOGGER.warning('Image data cannot be base64 decoded: "%s"', exc)
194
+ raise FailError(status_code=HTTPStatus.UNPROCESSABLE_ENTITY) from exc
195
+
196
+
197
+ @beartype
198
+ def validate_image_data_type(*, request_body: bytes) -> None:
199
+ """Validate that the given image data is a string.
200
+
201
+ Args:
202
+ request_body: The body of the request.
203
+
204
+ Raises:
205
+ FailError: Image data is given and it is not a string.
206
+ """
207
+ if not request_body:
208
+ return
209
+
210
+ request_text = request_body.decode()
211
+ if "image" not in json.loads(s=request_text):
212
+ return
213
+
214
+ image = json.loads(s=request_text).get("image")
215
+
216
+ if isinstance(image, str):
217
+ return
218
+
219
+ _LOGGER.warning('Image data is not a string: "%s"', image)
220
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
@@ -0,0 +1,69 @@
1
+ """Validators for given JSON."""
2
+
3
+ import json
4
+ import logging
5
+ from http import HTTPMethod, HTTPStatus
6
+ from json.decoder import JSONDecodeError
7
+
8
+ from beartype import beartype
9
+
10
+ from mock_vws._services_validators.exceptions import (
11
+ BadRequestError,
12
+ FailError,
13
+ UnnecessaryRequestBodyError,
14
+ )
15
+
16
+ _LOGGER = logging.getLogger(name=__name__)
17
+
18
+
19
+ @beartype
20
+ def validate_body_given(*, request_body: bytes, request_method: str) -> None:
21
+ """Validate that no JSON is given for requests other than ``POST`` and
22
+ ``PUT`` requests.
23
+
24
+ Args:
25
+ request_body: The body of the request.
26
+ request_method: The HTTP method of the request.
27
+
28
+ Raises:
29
+ UnnecessaryRequestBodyError: A request body was given for an endpoint
30
+ which does not require one.
31
+ FailError: The request body includes invalid JSON.
32
+ """
33
+ if not request_body:
34
+ return
35
+
36
+ if request_method not in {HTTPMethod.POST, HTTPMethod.PUT}:
37
+ _LOGGER.warning(
38
+ msg=(
39
+ "A request body was given for an endpoint which does not "
40
+ "require one."
41
+ ),
42
+ )
43
+ raise UnnecessaryRequestBodyError
44
+
45
+
46
+ @beartype
47
+ def validate_json(*, request_body: bytes, request_path: str) -> None:
48
+ """Validate that any given body is valid JSON.
49
+
50
+ Args:
51
+ request_body: The body of the request.
52
+ request_path: The path of the request.
53
+
54
+ Raises:
55
+ BadRequestError: The request body includes invalid JSON for the
56
+ VuMark instance generation endpoint.
57
+ FailError: The request body includes invalid JSON for other
58
+ endpoints.
59
+ """
60
+ if not request_body:
61
+ return
62
+
63
+ try:
64
+ json.loads(s=request_body.decode())
65
+ except JSONDecodeError as exc:
66
+ _LOGGER.warning(msg="The request body is not valid JSON.")
67
+ if request_path.endswith("/instances"):
68
+ raise BadRequestError from exc
69
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST) from exc
@@ -0,0 +1,171 @@
1
+ """Validators for JSON keys."""
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+ from http import HTTPMethod, HTTPStatus
9
+
10
+ from beartype import beartype
11
+
12
+ from .exceptions import FailError
13
+
14
+ _LOGGER = logging.getLogger(name=__name__)
15
+
16
+
17
+ @beartype
18
+ @dataclass(frozen=True, kw_only=True)
19
+ class _Route:
20
+ """A representation of a VWS route.
21
+
22
+ Args:
23
+ path_pattern: The end part of a URL pattern. E.g. `/targets` or
24
+ `/targets/.+`.
25
+ http_methods: HTTP methods that map to the route function.
26
+ mandatory_keys: Keys required by the endpoint.
27
+ optional_keys: Keys which are not required by the endpoint but which
28
+ are allowed.
29
+ """
30
+
31
+ path_pattern: str
32
+ http_methods: Iterable[HTTPMethod]
33
+ mandatory_keys: Iterable[str]
34
+ optional_keys: Iterable[str]
35
+
36
+
37
+ @beartype
38
+ def validate_keys(
39
+ *,
40
+ request_body: bytes,
41
+ request_path: str,
42
+ request_method: str,
43
+ ) -> None:
44
+ """Validate the request keys given to a VWS endpoint.
45
+
46
+ Args:
47
+ request_body: The body of the request.
48
+ request_path: The path of the request.
49
+ request_method: The HTTP method of the request.
50
+
51
+ Raises:
52
+ FailError: Any given keys are not allowed, or if any required keys are
53
+ missing.
54
+ """
55
+ target_id_pattern = "[A-Za-z0-9]+"
56
+ add_target = _Route(
57
+ path_pattern="/targets",
58
+ http_methods={HTTPMethod.POST},
59
+ mandatory_keys={"image", "width", "name"},
60
+ optional_keys={"active_flag", "application_metadata"},
61
+ )
62
+
63
+ delete_target = _Route(
64
+ path_pattern=f"/targets/{target_id_pattern}",
65
+ http_methods={HTTPMethod.DELETE},
66
+ mandatory_keys=set(),
67
+ optional_keys=set(),
68
+ )
69
+
70
+ database_summary = _Route(
71
+ path_pattern="/summary",
72
+ http_methods={HTTPMethod.GET},
73
+ mandatory_keys=set(),
74
+ optional_keys=set(),
75
+ )
76
+
77
+ target_list = _Route(
78
+ path_pattern="/targets",
79
+ http_methods={HTTPMethod.GET},
80
+ mandatory_keys=set(),
81
+ optional_keys=set(),
82
+ )
83
+
84
+ get_target = _Route(
85
+ path_pattern=f"/targets/{target_id_pattern}",
86
+ http_methods={HTTPMethod.GET},
87
+ mandatory_keys=set(),
88
+ optional_keys=set(),
89
+ )
90
+
91
+ target_summary = _Route(
92
+ path_pattern=f"/summary/{target_id_pattern}",
93
+ http_methods={HTTPMethod.GET},
94
+ mandatory_keys=set(),
95
+ optional_keys=set(),
96
+ )
97
+
98
+ get_duplicates = _Route(
99
+ path_pattern=f"/duplicates/{target_id_pattern}",
100
+ http_methods={HTTPMethod.GET},
101
+ mandatory_keys=set(),
102
+ optional_keys=set(),
103
+ )
104
+
105
+ update_target = _Route(
106
+ path_pattern=f"/targets/{target_id_pattern}",
107
+ http_methods={HTTPMethod.PUT},
108
+ mandatory_keys=set(),
109
+ optional_keys={
110
+ "active_flag",
111
+ "application_metadata",
112
+ "image",
113
+ "name",
114
+ "width",
115
+ },
116
+ )
117
+
118
+ generate_instance = _Route(
119
+ path_pattern=f"/targets/{target_id_pattern}/instances",
120
+ http_methods={HTTPMethod.POST},
121
+ mandatory_keys={"instance_id"},
122
+ optional_keys=set(),
123
+ )
124
+
125
+ target_summary = _Route(
126
+ path_pattern=f"/summary/{target_id_pattern}",
127
+ http_methods={HTTPMethod.GET},
128
+ mandatory_keys=set(),
129
+ optional_keys=set(),
130
+ )
131
+
132
+ routes = (
133
+ add_target,
134
+ delete_target,
135
+ database_summary,
136
+ target_list,
137
+ get_target,
138
+ get_duplicates,
139
+ update_target,
140
+ generate_instance,
141
+ target_summary,
142
+ )
143
+
144
+ (matching_route,) = (
145
+ route
146
+ for route in routes
147
+ if re.match(
148
+ pattern=re.compile(pattern=f"{route.path_pattern}$"),
149
+ string=request_path,
150
+ )
151
+ and request_method in set(route.http_methods)
152
+ )
153
+
154
+ mandatory_keys = matching_route.mandatory_keys
155
+ optional_keys = matching_route.optional_keys
156
+ allowed_keys = {*mandatory_keys, *optional_keys}
157
+
158
+ if not request_body and not allowed_keys:
159
+ return
160
+
161
+ request_text = request_body.decode()
162
+ request_json = json.loads(s=request_text)
163
+ given_keys = set(request_json.keys())
164
+ all_given_keys_allowed = given_keys.issubset(allowed_keys)
165
+ all_mandatory_keys_given = set(mandatory_keys).issubset(set(given_keys))
166
+
167
+ if all_given_keys_allowed and all_mandatory_keys_given:
168
+ return
169
+
170
+ _LOGGER.warning(msg="Invalid keys given to endpoint.")
171
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
@@ -0,0 +1,106 @@
1
+ """Validators for application metadata."""
2
+
3
+ import binascii
4
+ import json
5
+ import logging
6
+ from http import HTTPStatus
7
+
8
+ from beartype import beartype
9
+
10
+ from mock_vws._base64_decoding import decode_base64
11
+ from mock_vws._services_validators.exceptions import (
12
+ FailError,
13
+ MetadataTooLargeError,
14
+ )
15
+
16
+ _LOGGER = logging.getLogger(name=__name__)
17
+
18
+
19
+ @beartype
20
+ def validate_metadata_size(*, request_body: bytes) -> None:
21
+ """Validate that the given application metadata is a string or 1024 *
22
+ 1024
23
+ bytes or fewer.
24
+
25
+ Args:
26
+ request_body: The body of the request.
27
+
28
+ Raises:
29
+ MetadataTooLargeError: Application metadata is given and it is too
30
+ large.
31
+ """
32
+ if not request_body:
33
+ return
34
+
35
+ request_text = request_body.decode()
36
+ request_json = json.loads(s=request_text)
37
+ application_metadata = request_json.get("application_metadata")
38
+ if application_metadata is None:
39
+ return
40
+ decoded = decode_base64(encoded_data=application_metadata)
41
+
42
+ max_metadata_bytes = 1024 * 1024 - 1
43
+ if len(decoded) <= max_metadata_bytes:
44
+ return
45
+
46
+ _LOGGER.warning(msg="The application metadata is too large.")
47
+ raise MetadataTooLargeError
48
+
49
+
50
+ @beartype
51
+ def validate_metadata_encoding(*, request_body: bytes) -> None:
52
+ """Validate that the given application metadata can be base64 decoded.
53
+
54
+ Args:
55
+ request_body: The body of the request.
56
+
57
+ Raises:
58
+ FailError: Application metadata is given and it cannot be base64
59
+ decoded.
60
+ """
61
+ if not request_body:
62
+ return
63
+
64
+ request_text = request_body.decode()
65
+ request_json = json.loads(s=request_text)
66
+ if "application_metadata" not in request_json:
67
+ return
68
+
69
+ application_metadata = request_json.get("application_metadata")
70
+
71
+ if application_metadata is None:
72
+ return
73
+
74
+ try:
75
+ decode_base64(encoded_data=application_metadata)
76
+ except binascii.Error as exc:
77
+ _LOGGER.warning(msg="The application metadata is not base64 encoded.")
78
+ raise FailError(status_code=HTTPStatus.UNPROCESSABLE_ENTITY) from exc
79
+
80
+
81
+ @beartype
82
+ def validate_metadata_type(*, request_body: bytes) -> None:
83
+ """Validate that the given application metadata is a string or NULL.
84
+
85
+ Args:
86
+ request_body: The body of the request.
87
+
88
+ Raises:
89
+ FailError: Application metadata is given and it is not a string or
90
+ NULL.
91
+ """
92
+ if not request_body:
93
+ return
94
+
95
+ request_text = request_body.decode()
96
+ request_json = json.loads(s=request_text)
97
+ if "application_metadata" not in request_json:
98
+ return
99
+
100
+ application_metadata = request_json.get("application_metadata")
101
+
102
+ if application_metadata is None or isinstance(application_metadata, str):
103
+ return
104
+
105
+ _LOGGER.warning(msg="The application metadata is not a string or NULL.")
106
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)