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 @@
1
+ """A fake implementation of Vuforia Web Services for use with respx."""
@@ -0,0 +1,186 @@
1
+ """Helpers for mocking Vuforia with httpx via respx."""
2
+
3
+ import re
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Protocol
6
+ from urllib.parse import urlparse
7
+
8
+ import httpx
9
+ import respx
10
+
11
+ from mock_vws._mock_common import RequestData, Route
12
+
13
+ _ResponseType = tuple[int, Mapping[str, str], str | bytes]
14
+
15
+
16
+ class _APIHandler(Protocol):
17
+ """An API handler with mock routes."""
18
+
19
+ routes: set[Route]
20
+
21
+
22
+ def _to_request_data(
23
+ request: httpx.Request,
24
+ *,
25
+ base_path: str,
26
+ ) -> RequestData:
27
+ """Convert an httpx.Request to a RequestData.
28
+
29
+ Args:
30
+ request: The httpx request to convert.
31
+ base_path: The base path prefix to strip from the request path.
32
+
33
+ Returns:
34
+ A RequestData with method, path, headers, and body set.
35
+ """
36
+ path = request.url.raw_path.decode(encoding="ascii")
37
+ if base_path and path.startswith(base_path):
38
+ path = path[len(base_path) :]
39
+ return RequestData(
40
+ method=request.method,
41
+ path=path,
42
+ headers={k.title(): v for k, v in request.headers.items()},
43
+ body=request.content,
44
+ )
45
+
46
+
47
+ def _block_unmatched(request: httpx.Request) -> httpx.Response:
48
+ """Raise ConnectError for unmatched requests when real_http=False.
49
+
50
+ Args:
51
+ request: The unmatched httpx request.
52
+
53
+ Raises:
54
+ Exception: A connection error is always raised to block
55
+ unmatched requests.
56
+ """
57
+ raise httpx.ConnectError(
58
+ message="Connection refused by mock",
59
+ request=request,
60
+ )
61
+
62
+
63
+ def _make_respx_callback(
64
+ *,
65
+ handler: Callable[[RequestData], _ResponseType],
66
+ base_path: str,
67
+ delay_seconds: float,
68
+ sleep_fn: Callable[[float], None],
69
+ ) -> Callable[[httpx.Request], httpx.Response]:
70
+ """Create a respx-compatible callback from a handler.
71
+
72
+ Args:
73
+ handler: A handler that takes a RequestData and returns a
74
+ response tuple.
75
+ base_path: The base path prefix to strip from the request path.
76
+ delay_seconds: The number of seconds to delay the response by.
77
+ sleep_fn: The function to use for sleeping during delays.
78
+
79
+ Returns:
80
+ A callback that takes an httpx.Request and returns an
81
+ httpx.Response.
82
+ """
83
+
84
+ def callback(request: httpx.Request) -> httpx.Response:
85
+ """Handle an httpx request by converting it and calling the
86
+ handler.
87
+
88
+ Args:
89
+ request: The httpx request to handle.
90
+
91
+ Returns:
92
+ An httpx.Response built from the handler's return value.
93
+
94
+ Raises:
95
+ Exception: A timeout error is raised when the response
96
+ delay exceeds the read timeout.
97
+ """
98
+ request_data = _to_request_data(
99
+ request=request,
100
+ base_path=base_path,
101
+ )
102
+ timeout_info: dict[str, float | None] = request.extensions.get(
103
+ "timeout", {}
104
+ )
105
+ read_timeout = timeout_info.get("read")
106
+ if read_timeout is not None and delay_seconds > read_timeout:
107
+ sleep_fn(read_timeout)
108
+ raise httpx.ReadTimeout(
109
+ message="Response delay exceeded read timeout",
110
+ request=request,
111
+ )
112
+ status_code, headers, body = handler(request_data)
113
+ sleep_fn(delay_seconds)
114
+ if isinstance(body, str):
115
+ body = body.encode()
116
+ return httpx.Response(
117
+ status_code=status_code,
118
+ headers=headers,
119
+ content=body,
120
+ )
121
+
122
+ return callback
123
+
124
+
125
+ def start_respx_router(
126
+ *,
127
+ mock_vws_api: _APIHandler,
128
+ mock_vwq_api: _APIHandler,
129
+ base_vws_url: str,
130
+ base_vwq_url: str,
131
+ response_delay_seconds: float,
132
+ sleep_fn: Callable[[float], None],
133
+ real_http: bool,
134
+ ) -> respx.MockRouter:
135
+ """Configure and start a respx router with Vuforia routes.
136
+
137
+ Args:
138
+ mock_vws_api: The VWS API handler.
139
+ mock_vwq_api: The VWQ API handler.
140
+ base_vws_url: The base URL for the VWS API.
141
+ base_vwq_url: The base URL for the VWQ API.
142
+ response_delay_seconds: The number of seconds to delay responses.
143
+ sleep_fn: The function to use for sleeping during delays.
144
+ real_http: Whether to pass through unmatched requests.
145
+
146
+ Returns:
147
+ A started respx router.
148
+ """
149
+ router = respx.MockRouter(
150
+ assert_all_called=False,
151
+ assert_all_mocked=False,
152
+ )
153
+
154
+ for api, base_url in (
155
+ (mock_vws_api, base_vws_url),
156
+ (mock_vwq_api, base_vwq_url),
157
+ ):
158
+ base_path = urlparse(url=base_url).path.rstrip("/")
159
+ for route in api.routes:
160
+ url_pattern = base_url.rstrip("/") + route.path_pattern + "$"
161
+ compiled_url_pattern = re.compile(pattern=url_pattern)
162
+
163
+ for http_method in route.http_methods:
164
+ original_callback = getattr( # pylint: disable=bad-builtin
165
+ api,
166
+ route.route_name,
167
+ )
168
+ router.route(
169
+ method=http_method,
170
+ url=compiled_url_pattern,
171
+ ).mock(
172
+ side_effect=_make_respx_callback(
173
+ handler=original_callback,
174
+ base_path=base_path,
175
+ delay_seconds=response_delay_seconds,
176
+ sleep_fn=sleep_fn,
177
+ ),
178
+ )
179
+
180
+ if real_http:
181
+ router.route().pass_through()
182
+ else:
183
+ router.route().mock(side_effect=_block_unmatched)
184
+
185
+ router.start()
186
+ return router
@@ -0,0 +1,186 @@
1
+ """Input validators to use in the mock."""
2
+
3
+ from collections.abc import Iterable, Mapping
4
+
5
+ from beartype import beartype
6
+
7
+ from mock_vws._database_matchers import AnyDatabase
8
+
9
+ from .active_flag_validators import validate_active_flag
10
+ from .auth_validators import (
11
+ validate_access_key_exists,
12
+ validate_auth_header_exists,
13
+ validate_auth_header_has_signature,
14
+ validate_authorization,
15
+ )
16
+ from .content_length_validators import (
17
+ validate_content_length_header_is_int,
18
+ validate_content_length_header_not_too_large,
19
+ validate_content_length_header_not_too_small,
20
+ )
21
+ from .content_type_validators import validate_content_type_header_given
22
+ from .date_validators import (
23
+ validate_date_format,
24
+ validate_date_header_given,
25
+ validate_date_in_range,
26
+ )
27
+ from .image_validators import (
28
+ validate_image_color_space,
29
+ validate_image_data_type,
30
+ validate_image_encoding,
31
+ validate_image_format,
32
+ validate_image_integrity,
33
+ validate_image_is_image,
34
+ validate_image_size,
35
+ )
36
+ from .json_validators import validate_body_given, validate_json
37
+ from .key_validators import validate_keys
38
+ from .metadata_validators import (
39
+ validate_metadata_encoding,
40
+ validate_metadata_size,
41
+ validate_metadata_type,
42
+ )
43
+ from .name_validators import (
44
+ validate_name_characters_in_range,
45
+ validate_name_does_not_exist_existing_target,
46
+ validate_name_does_not_exist_new_target,
47
+ validate_name_length,
48
+ validate_name_type,
49
+ )
50
+ from .project_state_validators import validate_project_state
51
+ from .request_quota_validators import validate_request_quota
52
+ from .target_quota_validators import validate_target_quota
53
+ from .target_validators import validate_target_id_exists
54
+ from .width_validators import validate_width
55
+
56
+
57
+ @beartype
58
+ def run_services_validators(
59
+ *,
60
+ request_path: str,
61
+ request_headers: Mapping[str, str],
62
+ request_body: bytes,
63
+ request_method: str,
64
+ databases: Iterable[AnyDatabase],
65
+ ) -> None:
66
+ """Run all validators.
67
+
68
+ Args:
69
+ request_path: The path of the request.
70
+ request_headers: The headers sent with the request.
71
+ request_body: The body of the request.
72
+ request_method: The HTTP method of the request.
73
+ databases: All Vuforia databases.
74
+ """
75
+ validate_auth_header_exists(request_headers=request_headers)
76
+ validate_auth_header_has_signature(request_headers=request_headers)
77
+ validate_access_key_exists(
78
+ request_headers=request_headers,
79
+ databases=databases,
80
+ )
81
+ validate_authorization(
82
+ request_headers=request_headers,
83
+ request_body=request_body,
84
+ request_method=request_method,
85
+ request_path=request_path,
86
+ databases=databases,
87
+ )
88
+ validate_request_quota(
89
+ request_headers=request_headers,
90
+ request_body=request_body,
91
+ request_method=request_method,
92
+ request_path=request_path,
93
+ databases=databases,
94
+ )
95
+ validate_project_state(
96
+ request_headers=request_headers,
97
+ request_body=request_body,
98
+ request_method=request_method,
99
+ request_path=request_path,
100
+ databases=databases,
101
+ )
102
+ validate_target_quota(
103
+ request_headers=request_headers,
104
+ request_body=request_body,
105
+ request_method=request_method,
106
+ request_path=request_path,
107
+ databases=databases,
108
+ )
109
+ validate_target_id_exists(
110
+ request_headers=request_headers,
111
+ request_body=request_body,
112
+ request_method=request_method,
113
+ request_path=request_path,
114
+ databases=databases,
115
+ )
116
+
117
+ validate_body_given(
118
+ request_body=request_body,
119
+ request_method=request_method,
120
+ )
121
+
122
+ validate_date_header_given(request_headers=request_headers)
123
+ validate_date_format(request_headers=request_headers)
124
+ validate_date_in_range(request_headers=request_headers)
125
+
126
+ validate_json(request_body=request_body, request_path=request_path)
127
+
128
+ validate_keys(
129
+ request_body=request_body,
130
+ request_path=request_path,
131
+ request_method=request_method,
132
+ )
133
+ validate_metadata_type(request_body=request_body)
134
+ validate_metadata_encoding(request_body=request_body)
135
+ validate_metadata_size(request_body=request_body)
136
+ validate_active_flag(request_body=request_body)
137
+
138
+ validate_image_data_type(request_body=request_body)
139
+ validate_image_encoding(request_body=request_body)
140
+ validate_image_is_image(request_body=request_body)
141
+ validate_image_format(request_body=request_body)
142
+ validate_image_color_space(request_body=request_body)
143
+ validate_image_size(request_body=request_body)
144
+ validate_image_integrity(request_body=request_body)
145
+
146
+ validate_name_type(request_body=request_body)
147
+ validate_name_length(request_body=request_body)
148
+ validate_name_characters_in_range(
149
+ request_body=request_body,
150
+ request_method=request_method,
151
+ request_path=request_path,
152
+ )
153
+ validate_name_does_not_exist_new_target(
154
+ request_headers=request_headers,
155
+ request_body=request_body,
156
+ request_method=request_method,
157
+ request_path=request_path,
158
+ databases=databases,
159
+ )
160
+ validate_name_does_not_exist_existing_target(
161
+ request_headers=request_headers,
162
+ request_body=request_body,
163
+ request_method=request_method,
164
+ request_path=request_path,
165
+ databases=databases,
166
+ )
167
+
168
+ validate_width(request_body=request_body)
169
+ validate_content_type_header_given(
170
+ request_headers=request_headers,
171
+ request_method=request_method,
172
+ )
173
+
174
+ validate_content_length_header_is_int(
175
+ request_headers=request_headers,
176
+ request_body=request_body,
177
+ )
178
+ validate_content_length_header_not_too_large(
179
+ request_headers=request_headers,
180
+ request_body=request_body,
181
+ )
182
+
183
+ validate_content_length_header_not_too_small(
184
+ request_headers=request_headers,
185
+ request_body=request_body,
186
+ )
@@ -0,0 +1,43 @@
1
+ """Validators for the active flag."""
2
+
3
+ import json
4
+ import logging
5
+ from http import HTTPStatus
6
+
7
+ from beartype import beartype
8
+
9
+ from mock_vws._services_validators.exceptions import FailError
10
+
11
+ _LOGGER = logging.getLogger(name=__name__)
12
+
13
+
14
+ @beartype
15
+ def validate_active_flag(*, request_body: bytes) -> None:
16
+ """Validate the active flag data given to the endpoint.
17
+
18
+ Args:
19
+ request_body: The body of the request.
20
+
21
+ Raises:
22
+ FailError: There is active flag data given to the endpoint which is not
23
+ either a Boolean or NULL.
24
+ """
25
+ if not request_body:
26
+ return
27
+
28
+ request_text = request_body.decode()
29
+ if "active_flag" not in json.loads(s=request_text):
30
+ return
31
+
32
+ active_flag = json.loads(s=request_text).get("active_flag")
33
+
34
+ if active_flag in {True, False, None}:
35
+ return
36
+
37
+ _LOGGER.warning(
38
+ msg=(
39
+ 'The value of "active_flag" is not a Boolean or NULL. '
40
+ "This is not allowed."
41
+ ),
42
+ )
43
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
@@ -0,0 +1,124 @@
1
+ """Authorization header validators to use in the mock."""
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Mapping
5
+ from http import HTTPStatus
6
+
7
+ from beartype import beartype
8
+
9
+ from mock_vws._database_matchers import (
10
+ AnyDatabase,
11
+ get_database_matching_server_keys,
12
+ )
13
+ from mock_vws._services_validators.exceptions import (
14
+ AuthenticationFailureError,
15
+ FailError,
16
+ )
17
+
18
+ _LOGGER = logging.getLogger(name=__name__)
19
+
20
+
21
+ @beartype
22
+ def validate_auth_header_exists(*, request_headers: Mapping[str, str]) -> None:
23
+ """Validate that there is an authorization header given to a VWS
24
+ endpoint.
25
+
26
+ Args:
27
+ request_headers: The headers sent with the request.
28
+
29
+ Raises:
30
+ AuthenticationFailureError: There is no "Authorization" header.
31
+ """
32
+ if "Authorization" not in request_headers:
33
+ _LOGGER.warning(msg="There is no authorization header.")
34
+ raise AuthenticationFailureError
35
+
36
+
37
+ @beartype
38
+ def validate_access_key_exists(
39
+ *,
40
+ request_headers: Mapping[str, str],
41
+ databases: Iterable[AnyDatabase],
42
+ ) -> None:
43
+ """Validate the authorization header includes an access key for a
44
+ database.
45
+
46
+ Args:
47
+ request_headers: The headers sent with the request.
48
+ databases: All Vuforia databases.
49
+
50
+ Raises:
51
+ FailError: The access key does not match a given database.
52
+ """
53
+ header = request_headers["Authorization"]
54
+ first_part, _ = header.split(sep=":")
55
+ _, access_key = first_part.split(sep=" ")
56
+ for database in databases:
57
+ if access_key == database.server_access_key:
58
+ return
59
+
60
+ _LOGGER.warning(
61
+ 'The access key "%s" does not match a known database.',
62
+ access_key,
63
+ )
64
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
65
+
66
+
67
+ @beartype
68
+ def validate_auth_header_has_signature(
69
+ *,
70
+ request_headers: Mapping[str, str],
71
+ ) -> None:
72
+ """Validate the authorization header includes a signature.
73
+
74
+ Args:
75
+ request_headers: The headers sent with the request.
76
+
77
+ Raises:
78
+ FailError: The "Authorization" header does not include a signature.
79
+ """
80
+ header = request_headers["Authorization"]
81
+ if header.count(":") == 1 and header.split(sep=":")[1]:
82
+ return
83
+
84
+ _LOGGER.warning(
85
+ msg="The authorization header does not include a signature.",
86
+ )
87
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
88
+
89
+
90
+ @beartype
91
+ def validate_authorization(
92
+ *,
93
+ request_path: str,
94
+ request_headers: Mapping[str, str],
95
+ request_body: bytes,
96
+ request_method: str,
97
+ databases: Iterable[AnyDatabase],
98
+ ) -> None:
99
+ """Validate the authorization header given to a VWS endpoint.
100
+
101
+ Args:
102
+ request_path: The path of the request.
103
+ request_headers: The headers sent with the request.
104
+ request_body: The body of the request.
105
+ request_method: The HTTP method of the request.
106
+ databases: All Vuforia databases.
107
+
108
+ Raises:
109
+ AuthenticationFailureError: No database matches the given authorization
110
+ header.
111
+ """
112
+ try:
113
+ get_database_matching_server_keys(
114
+ request_headers=request_headers,
115
+ request_body=request_body,
116
+ request_method=request_method,
117
+ request_path=request_path,
118
+ databases=databases,
119
+ )
120
+ except ValueError as exc:
121
+ _LOGGER.warning(
122
+ msg="No database matches the given authorization header.",
123
+ )
124
+ raise AuthenticationFailureError from exc
@@ -0,0 +1,102 @@
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._services_validators.exceptions import (
9
+ AuthenticationFailureError,
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
+ request_body: bytes,
22
+ ) -> None:
23
+ """Validate the ``Content-Length`` header is an integer.
24
+
25
+ Args:
26
+ request_headers: The headers sent with the request.
27
+ request_body: The body of the request.
28
+
29
+ Raises:
30
+ ContentLengthHeaderNotIntError: The content length header is not an
31
+ integer
32
+ """
33
+ body_length = len(request_body)
34
+ request_headers_dict = dict(request_headers)
35
+ given_content_length = request_headers_dict.get(
36
+ "Content-Length",
37
+ body_length,
38
+ )
39
+
40
+ try:
41
+ int(given_content_length)
42
+ except ValueError as exc:
43
+ _LOGGER.warning(msg="The Content-Length header is not an integer.")
44
+ raise ContentLengthHeaderNotIntError from exc
45
+
46
+
47
+ @beartype
48
+ def validate_content_length_header_not_too_large(
49
+ *,
50
+ request_headers: Mapping[str, str],
51
+ request_body: bytes,
52
+ ) -> None:
53
+ """Validate the ``Content-Length`` header is not too large.
54
+
55
+ Args:
56
+ request_headers: The headers sent with the request.
57
+ request_body: The body of the request.
58
+
59
+ Raises:
60
+ ContentLengthHeaderTooLargeError: The given content length header says
61
+ that the content length is greater than the body length.
62
+ """
63
+ body_length = len(request_body)
64
+ request_headers_dict = dict(request_headers)
65
+ given_content_length = request_headers_dict.get(
66
+ "Content-Length",
67
+ body_length,
68
+ )
69
+ given_content_length_value = int(given_content_length)
70
+ # We skip coverage here as running a test to cover this is very slow.
71
+ if given_content_length_value > body_length: # pragma: no cover
72
+ _LOGGER.warning(msg="The Content-Length header is too large.")
73
+ raise ContentLengthHeaderTooLargeError
74
+
75
+
76
+ @beartype
77
+ def validate_content_length_header_not_too_small(
78
+ *,
79
+ request_headers: Mapping[str, str],
80
+ request_body: bytes,
81
+ ) -> None:
82
+ """Validate the ``Content-Length`` header is not too small.
83
+
84
+ Args:
85
+ request_headers: The headers sent with the request.
86
+ request_body: The body of the request.
87
+
88
+ Raises:
89
+ AuthenticationFailureError: The given content length header says that
90
+ the content length is smaller than the body length.
91
+ """
92
+ body_length = len(request_body)
93
+ request_headers_dict = dict(request_headers)
94
+ given_content_length = request_headers_dict.get(
95
+ "Content-Length",
96
+ body_length,
97
+ )
98
+ given_content_length_value = int(given_content_length)
99
+
100
+ if given_content_length_value < body_length:
101
+ _LOGGER.warning(msg="The Content-Length header is too small.")
102
+ raise AuthenticationFailureError
@@ -0,0 +1,42 @@
1
+ """Content-Type header validators to use in the mock."""
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+ from http import HTTPMethod
6
+
7
+ from beartype import beartype
8
+
9
+ from mock_vws._services_validators.exceptions import AuthenticationFailureError
10
+
11
+ _LOGGER = logging.getLogger(name=__name__)
12
+
13
+
14
+ @beartype
15
+ def validate_content_type_header_given(
16
+ *,
17
+ request_headers: Mapping[str, str],
18
+ request_method: str,
19
+ ) -> None:
20
+ """Validate that there is a non-empty content type header given if
21
+ required.
22
+
23
+ Args:
24
+ request_headers: The headers sent with the request.
25
+ request_method: The HTTP method of the request.
26
+
27
+ Raises:
28
+ AuthenticationFailureError: No ``Content-Type`` header is given and the
29
+ request requires one.
30
+ """
31
+ request_headers_dict = dict(request_headers)
32
+ request_needs_content_type = bool(
33
+ request_method in {HTTPMethod.POST, HTTPMethod.PUT},
34
+ )
35
+ if (
36
+ request_headers_dict.get("Content-Type")
37
+ or not request_needs_content_type
38
+ ):
39
+ return
40
+
41
+ _LOGGER.warning(msg="No Content-Type header is given.")
42
+ raise AuthenticationFailureError