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,235 @@
1
+ """Validators for target names."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Iterable, Mapping
6
+ from http import HTTPMethod, HTTPStatus
7
+
8
+ from beartype import beartype
9
+
10
+ from mock_vws._database_matchers import (
11
+ AnyDatabase,
12
+ get_database_matching_server_keys,
13
+ )
14
+ from mock_vws._services_validators.exceptions import (
15
+ FailError,
16
+ TargetNameExistError,
17
+ )
18
+
19
+ _LOGGER = logging.getLogger(name=__name__)
20
+
21
+
22
+ @beartype
23
+ def validate_name_characters_in_range(
24
+ *,
25
+ request_body: bytes,
26
+ request_method: str,
27
+ request_path: str,
28
+ ) -> None:
29
+ """Validate the characters in the name argument given to a VWS
30
+ endpoint.
31
+
32
+ Args:
33
+ request_body: The body of the request.
34
+ request_method: The HTTP method the request is using.
35
+ request_path: The path to the endpoint.
36
+
37
+ Raises:
38
+ FailError: Characters are out of range and the request is trying to
39
+ make a new target.
40
+ TargetNameExistError: Characters are out of range and the request is
41
+ for another endpoint.
42
+ """
43
+ if not request_body:
44
+ return
45
+
46
+ request_text = request_body.decode()
47
+ if "name" not in json.loads(s=request_text):
48
+ return
49
+
50
+ name = json.loads(s=request_text)["name"]
51
+
52
+ max_character_ord = 65535
53
+ if all(ord(character) <= max_character_ord for character in name):
54
+ return
55
+
56
+ if (request_method, request_path) == (HTTPMethod.POST, "/targets"):
57
+ _LOGGER.warning(msg="Characters are out of range.")
58
+ raise FailError(status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
59
+
60
+ _LOGGER.warning(msg="Characters are out of range.")
61
+ raise TargetNameExistError
62
+
63
+
64
+ @beartype
65
+ def validate_name_type(*, request_body: bytes) -> None:
66
+ """Validate the type of the name argument given to a VWS endpoint.
67
+
68
+ Args:
69
+ request_body: The body of the request.
70
+
71
+ Raises:
72
+ FailError: A name is given and it is not a string.
73
+ """
74
+ if not request_body:
75
+ return
76
+
77
+ request_text = request_body.decode()
78
+ if "name" not in json.loads(s=request_text):
79
+ return
80
+
81
+ name = json.loads(s=request_text)["name"]
82
+
83
+ if isinstance(name, str):
84
+ return
85
+
86
+ _LOGGER.warning(msg="Name is not a string.")
87
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
88
+
89
+
90
+ @beartype
91
+ def validate_name_length(*, request_body: bytes) -> None:
92
+ """Validate the length of the name argument given to a VWS endpoint.
93
+
94
+ Args:
95
+ request_body: The body of the request.
96
+
97
+ Raises:
98
+ FailError: A name is given and it is not a between 1 and 64 characters
99
+ in length.
100
+ """
101
+ if not request_body:
102
+ return
103
+
104
+ request_text = request_body.decode()
105
+ if "name" not in json.loads(s=request_text):
106
+ return
107
+
108
+ name = json.loads(s=request_text)["name"]
109
+
110
+ max_length = 64
111
+ if name and len(name) <= max_length:
112
+ return
113
+
114
+ _LOGGER.warning(msg="Name is not between 1 and 64 characters in length.")
115
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)
116
+
117
+
118
+ @beartype
119
+ def validate_name_does_not_exist_new_target(
120
+ *,
121
+ databases: Iterable[AnyDatabase],
122
+ request_body: bytes,
123
+ request_headers: Mapping[str, str],
124
+ request_method: str,
125
+ request_path: str,
126
+ ) -> None:
127
+ """Validate that the name does not exist for any existing target.
128
+
129
+ Args:
130
+ databases: All Vuforia databases.
131
+ request_body: The body of the request.
132
+ request_headers: The headers sent with the request.
133
+ request_method: The HTTP method the request is using.
134
+ request_path: The path to the endpoint.
135
+
136
+ Raises:
137
+ TargetNameExistError: The target name already exists.
138
+ """
139
+ if not request_body:
140
+ return
141
+
142
+ request_text = request_body.decode()
143
+ if "name" not in json.loads(s=request_text):
144
+ return
145
+
146
+ split_path = request_path.split(sep="/")
147
+
148
+ split_path_no_target_id_length = 2
149
+ if len(split_path) != split_path_no_target_id_length:
150
+ return
151
+
152
+ name = json.loads(s=request_text)["name"]
153
+ database = get_database_matching_server_keys(
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
+
161
+ matching_name_targets = [
162
+ target
163
+ for target in database.not_deleted_targets
164
+ if target.name == name
165
+ ]
166
+
167
+ if not matching_name_targets:
168
+ return
169
+
170
+ _LOGGER.warning(msg="Target name already exists.")
171
+ raise TargetNameExistError
172
+
173
+
174
+ @beartype
175
+ def validate_name_does_not_exist_existing_target(
176
+ *,
177
+ request_headers: Mapping[str, str],
178
+ request_body: bytes,
179
+ request_method: str,
180
+ request_path: str,
181
+ databases: Iterable[AnyDatabase],
182
+ ) -> None:
183
+ """Validate that the name does not exist for any existing target apart
184
+ from
185
+ the one being updated.
186
+
187
+ Args:
188
+ databases: All Vuforia databases.
189
+ request_body: The body of the request.
190
+ request_headers: The headers sent with the request.
191
+ request_method: The HTTP method the request is using.
192
+ request_path: The path to the endpoint.
193
+
194
+ Raises:
195
+ TargetNameExistError: The target name is not the same as the name of
196
+ the target being updated but it is the same as another target.
197
+ """
198
+ if not request_body:
199
+ return
200
+
201
+ request_text = request_body.decode()
202
+ if "name" not in json.loads(s=request_text):
203
+ return
204
+
205
+ split_path = request_path.split(sep="/")
206
+ split_path_no_target_id_length = 2
207
+ if len(split_path) == split_path_no_target_id_length:
208
+ return
209
+
210
+ target_id = split_path[-1]
211
+
212
+ name = json.loads(s=request_text)["name"]
213
+ database = get_database_matching_server_keys(
214
+ request_headers=request_headers,
215
+ request_body=request_body,
216
+ request_method=request_method,
217
+ request_path=request_path,
218
+ databases=databases,
219
+ )
220
+
221
+ matching_name_targets = [
222
+ target
223
+ for target in database.not_deleted_targets
224
+ if target.name == name
225
+ ]
226
+
227
+ if not matching_name_targets:
228
+ return
229
+
230
+ (matching_name_target,) = matching_name_targets
231
+ if matching_name_target.target_id == target_id:
232
+ return
233
+
234
+ _LOGGER.warning(msg="Name already exists for another target.")
235
+ raise TargetNameExistError
@@ -0,0 +1,76 @@
1
+ """Validators for the project state."""
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Mapping
5
+ from http import HTTPMethod
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
+ ProjectHasNoAPIAccessError,
15
+ ProjectInactiveError,
16
+ ProjectSuspendedError,
17
+ ValidatorError,
18
+ )
19
+ from mock_vws.database import CloudDatabase, VuMarkDatabase
20
+ from mock_vws.states import States
21
+
22
+ _LOGGER = logging.getLogger(name=__name__)
23
+
24
+
25
+ @beartype
26
+ def validate_project_state(
27
+ *,
28
+ request_path: str,
29
+ request_headers: Mapping[str, str],
30
+ request_body: bytes,
31
+ request_method: str,
32
+ databases: Iterable[AnyDatabase],
33
+ ) -> None:
34
+ """Validate the state of the project.
35
+
36
+ Args:
37
+ request_path: The path of the request.
38
+ request_headers: The headers sent with the request.
39
+ request_body: The body of the request.
40
+ request_method: The HTTP method of the request.
41
+ databases: All Vuforia databases.
42
+
43
+ Raises:
44
+ ProjectInactiveError: The project is inactive and this endpoint does
45
+ not work with inactive projects.
46
+ """
47
+ database = get_database_matching_server_keys(
48
+ request_headers=request_headers,
49
+ request_body=request_body,
50
+ request_method=request_method,
51
+ request_path=request_path,
52
+ databases=databases,
53
+ )
54
+
55
+ state_errors: dict[States, type[ValidatorError]] = {
56
+ States.PROJECT_HAS_NO_API_ACCESS: ProjectHasNoAPIAccessError,
57
+ States.PROJECT_SUSPENDED: ProjectSuspendedError,
58
+ }
59
+ if error := state_errors.get(database.state):
60
+ raise error
61
+
62
+ if database.state != States.PROJECT_INACTIVE:
63
+ return
64
+
65
+ if (
66
+ isinstance(database, CloudDatabase)
67
+ and request_method == HTTPMethod.GET
68
+ and "duplicates" not in request_path
69
+ ):
70
+ return
71
+
72
+ if isinstance(database, VuMarkDatabase):
73
+ return
74
+
75
+ _LOGGER.warning(msg="The project is inactive.")
76
+ raise ProjectInactiveError
@@ -0,0 +1,42 @@
1
+ """Validators for the VWS request quota.
2
+
3
+ This behavior cannot be verified against the real Vuforia Web Services
4
+ without deliberately exhausting a database's request quota. It implements the
5
+ publicly documented behavior so that users can exercise their application's
6
+ quota-error handling with the mock.
7
+ """
8
+
9
+ from collections.abc import Iterable, Mapping
10
+
11
+ from beartype import beartype
12
+
13
+ from mock_vws._database_matchers import (
14
+ AnyDatabase,
15
+ get_database_matching_server_keys,
16
+ )
17
+ from mock_vws.database import CloudDatabase
18
+
19
+ from .exceptions import RequestQuotaReachedError
20
+
21
+
22
+ @beartype
23
+ def validate_request_quota(
24
+ *,
25
+ request_headers: Mapping[str, str],
26
+ request_body: bytes,
27
+ request_method: str,
28
+ request_path: str,
29
+ databases: Iterable[AnyDatabase],
30
+ ) -> None:
31
+ """Raise an error if the matching cloud database has no request
32
+ quota.
33
+ """
34
+ database = get_database_matching_server_keys(
35
+ request_headers=request_headers,
36
+ request_body=request_body,
37
+ request_method=request_method,
38
+ request_path=request_path,
39
+ databases=databases,
40
+ )
41
+ if isinstance(database, CloudDatabase) and database.request_quota == 0:
42
+ raise RequestQuotaReachedError
@@ -0,0 +1,41 @@
1
+ """Validators for the VWS target quota."""
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from http import HTTPMethod
5
+
6
+ from beartype import beartype
7
+
8
+ from mock_vws._database_matchers import (
9
+ AnyDatabase,
10
+ get_database_matching_server_keys,
11
+ )
12
+ from mock_vws.database import CloudDatabase
13
+
14
+ from .exceptions import TargetQuotaReachedError
15
+
16
+
17
+ @beartype
18
+ def validate_target_quota(
19
+ *,
20
+ request_headers: Mapping[str, str],
21
+ request_body: bytes,
22
+ request_method: str,
23
+ request_path: str,
24
+ databases: Iterable[AnyDatabase],
25
+ ) -> None:
26
+ """Raise an error when adding a target would exceed the quota."""
27
+ if request_method != HTTPMethod.POST or request_path != "/targets":
28
+ return
29
+
30
+ database = get_database_matching_server_keys(
31
+ request_headers=request_headers,
32
+ request_body=request_body,
33
+ request_method=request_method,
34
+ request_path=request_path,
35
+ databases=databases,
36
+ )
37
+ if (
38
+ isinstance(database, CloudDatabase)
39
+ and len(database.not_deleted_targets) >= database.target_quota
40
+ ):
41
+ raise TargetQuotaReachedError
@@ -0,0 +1,69 @@
1
+ """Validators for given target IDs."""
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 (
9
+ AnyDatabase,
10
+ get_database_matching_server_keys,
11
+ )
12
+ from mock_vws._services_validators.exceptions import UnknownTargetError
13
+
14
+ _LOGGER = logging.getLogger(name=__name__)
15
+ _TARGETS_WITH_INSTANCE_PATH_LENGTH = 4
16
+
17
+
18
+ @beartype
19
+ def validate_target_id_exists(
20
+ *,
21
+ request_path: str,
22
+ request_headers: Mapping[str, str],
23
+ request_body: bytes,
24
+ request_method: str,
25
+ databases: Iterable[AnyDatabase],
26
+ ) -> None:
27
+ """Validate that if a target ID is given, it exists in the database
28
+ matching the request.
29
+
30
+ Args:
31
+ request_path: The path of the request.
32
+ request_headers: The headers sent with the request.
33
+ request_body: The body of the request.
34
+ request_method: The HTTP method of the request.
35
+ databases: All Vuforia databases.
36
+
37
+ Raises:
38
+ UnknownTargetError: There are no matching targets for a given target
39
+ ID.
40
+ """
41
+ split_path = request_path.split(sep="/")
42
+
43
+ request_path_no_target_id_length = 2
44
+ if len(split_path) == request_path_no_target_id_length:
45
+ return
46
+
47
+ target_id = split_path[-1]
48
+ if (
49
+ len(split_path) == _TARGETS_WITH_INSTANCE_PATH_LENGTH
50
+ and split_path[-3] == "targets"
51
+ and split_path[-1] == "instances"
52
+ ):
53
+ target_id = split_path[-2]
54
+ database = get_database_matching_server_keys(
55
+ request_headers=request_headers,
56
+ request_body=request_body,
57
+ request_method=request_method,
58
+ request_path=request_path,
59
+ databases=databases,
60
+ )
61
+
62
+ matching_targets = [
63
+ target
64
+ for target in database.not_deleted_targets
65
+ if target.target_id == target_id
66
+ ]
67
+ if not matching_targets:
68
+ _LOGGER.warning('The target ID "%s" does not exist.', target_id)
69
+ raise UnknownTargetError
@@ -0,0 +1,38 @@
1
+ """Validators for the width field."""
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_width(*, request_body: bytes) -> None:
16
+ """Validate the width argument given to a VWS endpoint.
17
+
18
+ Args:
19
+ request_body: The body of the request.
20
+
21
+ Raises:
22
+ FailError: Width is given and is not a positive number.
23
+ """
24
+ if not request_body:
25
+ return
26
+
27
+ request_text = request_body.decode()
28
+ if "width" not in json.loads(s=request_text):
29
+ return
30
+
31
+ width = json.loads(s=request_text).get("width")
32
+
33
+ width_is_number = isinstance(width, int | float)
34
+ width_positive = width_is_number and width > 0
35
+
36
+ if not width_positive:
37
+ _LOGGER.warning(msg="Width is not a positive number.")
38
+ raise FailError(status_code=HTTPStatus.BAD_REQUEST)