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
|
@@ -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)
|