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,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
|