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
mock_vws/database.py ADDED
@@ -0,0 +1,257 @@
1
+ """Utilities for managing mock Vuforia databases."""
2
+
3
+ import uuid
4
+ from collections.abc import Iterable
5
+ from dataclasses import dataclass, field
6
+ from typing import NotRequired, Self, TypedDict
7
+
8
+ from beartype import beartype
9
+
10
+ from mock_vws._constants import TargetStatuses
11
+ from mock_vws.database_type import DatabaseType
12
+ from mock_vws.states import States
13
+ from mock_vws.target import (
14
+ ImageTarget,
15
+ ImageTargetDict,
16
+ VuMarkTarget,
17
+ VuMarkTargetDict,
18
+ )
19
+
20
+
21
+ @beartype
22
+ class CloudDatabaseDict(TypedDict):
23
+ """A dictionary type which represents a cloud database."""
24
+
25
+ database_name: str
26
+ server_access_key: str
27
+ server_secret_key: str
28
+ client_access_key: str
29
+ client_secret_key: str
30
+ state_name: str
31
+ database_type_name: str
32
+ targets: Iterable[ImageTargetDict]
33
+ request_quota: NotRequired[int]
34
+ target_quota: NotRequired[int]
35
+
36
+
37
+ @beartype
38
+ class VuMarkDatabaseDict(TypedDict):
39
+ """A dictionary type which represents a VuMark database."""
40
+
41
+ database_name: str
42
+ server_access_key: str
43
+ server_secret_key: str
44
+ vumark_targets: Iterable[VuMarkTargetDict]
45
+ state_name: str
46
+
47
+
48
+ @beartype
49
+ def _random_hex() -> str:
50
+ """Return a random hex value."""
51
+ return uuid.uuid4().hex
52
+
53
+
54
+ @beartype
55
+ @dataclass(eq=True, frozen=True, kw_only=True)
56
+ class CloudDatabase:
57
+ """Credentials for VWS APIs.
58
+
59
+ Args:
60
+ database_name: The name of a VWS target manager database name. Defaults
61
+ to a random string.
62
+ server_access_key: A VWS server access key. Defaults to a random
63
+ string.
64
+ server_secret_key: A VWS server secret key. Defaults to a random
65
+ string.
66
+ client_access_key: A VWS client access key. Defaults to a random
67
+ string.
68
+ client_secret_key: A VWS client secret key. Defaults to a random
69
+ string.
70
+ state: The state of the database.
71
+ request_quota: The request quota. Set this to ``0`` to make VWS
72
+ endpoints return ``RequestQuotaReached``.
73
+ target_quota: The target quota. When the database contains this many
74
+ targets, adding another returns ``TargetQuotaReached``.
75
+ """
76
+
77
+ # We hide a few things in the ``repr`` with ``repr=False`` so that they do
78
+ # not show up in CI logs.
79
+ database_name: str = field(default_factory=_random_hex, repr=False)
80
+ server_access_key: str = field(default_factory=_random_hex, repr=False)
81
+ server_secret_key: str = field(default_factory=_random_hex, repr=False)
82
+ client_access_key: str = field(default_factory=_random_hex, repr=False)
83
+ client_secret_key: str = field(default_factory=_random_hex, repr=False)
84
+ # We have ``targets`` as ``hash=False`` so that we can have the class as
85
+ # ``frozen=True`` while still being able to keep the interface we want.
86
+ # In particular, we might want to inspect the ``database`` object's targets
87
+ # as they change via API requests.
88
+ targets: set[ImageTarget] = field(
89
+ default_factory=set[ImageTarget],
90
+ hash=False,
91
+ )
92
+ state: States = States.WORKING
93
+ database_type: DatabaseType = DatabaseType.CLOUD_RECO
94
+
95
+ request_quota: int = 100000
96
+ reco_threshold: int = 1000
97
+ current_month_recos: int = 0
98
+ previous_month_recos: int = 0
99
+ total_recos: int = 0
100
+ target_quota: int = 1000
101
+
102
+ def to_dict(self) -> CloudDatabaseDict:
103
+ """Dump a target to a dictionary which can be loaded as JSON."""
104
+ targets: list[ImageTargetDict] = [
105
+ target.to_dict() for target in self.targets
106
+ ]
107
+ return {
108
+ "database_name": self.database_name,
109
+ "server_access_key": self.server_access_key,
110
+ "server_secret_key": self.server_secret_key,
111
+ "client_access_key": self.client_access_key,
112
+ "client_secret_key": self.client_secret_key,
113
+ "state_name": self.state.name,
114
+ "database_type_name": self.database_type.name,
115
+ "targets": targets,
116
+ "request_quota": self.request_quota,
117
+ "target_quota": self.target_quota,
118
+ }
119
+
120
+ def get_target(self, target_id: str) -> ImageTarget:
121
+ """Return a target from the database with the given ID."""
122
+ (target,) = (
123
+ target for target in self.targets if target.target_id == target_id
124
+ )
125
+ return target
126
+
127
+ @classmethod
128
+ def from_dict(cls, database_dict: CloudDatabaseDict) -> Self:
129
+ """Load a database from a dictionary."""
130
+ targets: set[ImageTarget] = {
131
+ ImageTarget.from_dict(target_dict=target_dict)
132
+ for target_dict in database_dict["targets"]
133
+ }
134
+
135
+ return cls(
136
+ database_name=database_dict["database_name"],
137
+ server_access_key=database_dict["server_access_key"],
138
+ server_secret_key=database_dict["server_secret_key"],
139
+ client_access_key=database_dict["client_access_key"],
140
+ client_secret_key=database_dict["client_secret_key"],
141
+ state=States[database_dict["state_name"]],
142
+ database_type=DatabaseType[database_dict["database_type_name"]],
143
+ targets=targets,
144
+ request_quota=database_dict.get("request_quota", 100000),
145
+ target_quota=database_dict.get("target_quota", 1000),
146
+ )
147
+
148
+ @property
149
+ def not_deleted_targets(self) -> set[ImageTarget]:
150
+ """All targets which have not been deleted."""
151
+ return {target for target in self.targets if not target.delete_date}
152
+
153
+ @property
154
+ def active_targets(self) -> set[ImageTarget]:
155
+ """All active targets."""
156
+ return {
157
+ target
158
+ for target in self.not_deleted_targets
159
+ if target.status == TargetStatuses.SUCCESS.value
160
+ and target.active_flag
161
+ }
162
+
163
+ @property
164
+ def inactive_targets(self) -> set[ImageTarget]:
165
+ """All inactive targets."""
166
+ return {
167
+ target
168
+ for target in self.not_deleted_targets
169
+ if target.status == TargetStatuses.SUCCESS.value
170
+ and not target.active_flag
171
+ }
172
+
173
+ @property
174
+ def failed_targets(self) -> set[ImageTarget]:
175
+ """All failed targets."""
176
+ return {
177
+ target
178
+ for target in self.not_deleted_targets
179
+ if target.status == TargetStatuses.FAILED.value
180
+ }
181
+
182
+ @property
183
+ def processing_targets(self) -> set[ImageTarget]:
184
+ """All processing targets."""
185
+ return {
186
+ target
187
+ for target in self.not_deleted_targets
188
+ if target.status == TargetStatuses.PROCESSING.value
189
+ }
190
+
191
+
192
+ @beartype
193
+ @dataclass(eq=True, frozen=True, kw_only=True)
194
+ class VuMarkDatabase:
195
+ """Credentials for the VuMark generation API.
196
+
197
+ Args:
198
+ database_name: The name of a VWS target manager database name. Defaults
199
+ to a random string.
200
+ server_access_key: A VWS server access key. Defaults to a random
201
+ string.
202
+ server_secret_key: A VWS server secret key. Defaults to a random
203
+ string.
204
+ """
205
+
206
+ database_name: str = field(default_factory=_random_hex, repr=False)
207
+ server_access_key: str = field(default_factory=_random_hex, repr=False)
208
+ server_secret_key: str = field(default_factory=_random_hex, repr=False)
209
+ # We have ``vumark_targets`` as ``hash=False`` so that we can have the
210
+ # class as ``frozen=True`` while still being able to keep the interface
211
+ # we want.
212
+ vumark_targets: set[VuMarkTarget] = field(
213
+ default_factory=set[VuMarkTarget],
214
+ hash=False,
215
+ )
216
+ state: States = States.WORKING
217
+
218
+ def get_vumark_target(self, target_id: str) -> VuMarkTarget:
219
+ """Return a VuMark target from the database with the given ID."""
220
+ (target,) = (
221
+ target
222
+ for target in self.vumark_targets
223
+ if target.target_id == target_id
224
+ )
225
+ return target
226
+
227
+ def to_dict(self) -> VuMarkDatabaseDict:
228
+ """Dump a VuMark database to a dictionary which can be loaded as
229
+ JSON.
230
+ """
231
+ vumark_targets = [target.to_dict() for target in self.vumark_targets]
232
+ return {
233
+ "database_name": self.database_name,
234
+ "server_access_key": self.server_access_key,
235
+ "server_secret_key": self.server_secret_key,
236
+ "vumark_targets": vumark_targets,
237
+ "state_name": self.state.name,
238
+ }
239
+
240
+ @classmethod
241
+ def from_dict(cls, database_dict: VuMarkDatabaseDict) -> Self:
242
+ """Load a VuMark database from a dictionary."""
243
+ return cls(
244
+ database_name=database_dict["database_name"],
245
+ server_access_key=database_dict["server_access_key"],
246
+ server_secret_key=database_dict["server_secret_key"],
247
+ vumark_targets={
248
+ VuMarkTarget.from_dict(target_dict=target_dict)
249
+ for target_dict in database_dict["vumark_targets"]
250
+ },
251
+ state=States[database_dict["state_name"]],
252
+ )
253
+
254
+ @property
255
+ def not_deleted_targets(self) -> set[VuMarkTarget]:
256
+ """All VuMark targets."""
257
+ return set(self.vumark_targets)
@@ -0,0 +1,13 @@
1
+ """Vuforia database types."""
2
+
3
+ from enum import StrEnum, auto, unique
4
+
5
+ from beartype import beartype
6
+
7
+
8
+ @beartype
9
+ @unique
10
+ class DatabaseType(StrEnum):
11
+ """Constants representing various database types."""
12
+
13
+ CLOUD_RECO = auto()
@@ -0,0 +1,124 @@
1
+ """Matchers for query and duplicate requests."""
2
+
3
+ import io
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ import numpy as np
7
+ import torch
8
+ from beartype import beartype
9
+ from PIL import Image
10
+ from torchmetrics.image import (
11
+ StructuralSimilarityIndexMeasure,
12
+ )
13
+
14
+
15
+ @runtime_checkable
16
+ class ImageMatcher(Protocol):
17
+ """Protocol for a matcher for query and duplicate requests."""
18
+
19
+ def __call__(
20
+ self,
21
+ first_image_content: bytes,
22
+ second_image_content: bytes,
23
+ ) -> bool:
24
+ """Whether one image's content matches another's closely enough.
25
+
26
+ Args:
27
+ first_image_content: One image's content.
28
+ second_image_content: Another image's content.
29
+ """
30
+ # We disable a pylint warning here because the ellipsis is required
31
+ # for pyright to recognize this as a protocol.
32
+ ... # pylint: disable=unnecessary-ellipsis
33
+
34
+
35
+ @beartype
36
+ class ExactMatcher:
37
+ """A matcher which returns whether two images are exactly equal."""
38
+
39
+ def __call__(
40
+ self,
41
+ first_image_content: bytes,
42
+ second_image_content: bytes,
43
+ ) -> bool:
44
+ """Whether one image's content matches another's exactly.
45
+
46
+ Args:
47
+ first_image_content: One image's content.
48
+ second_image_content: Another image's content.
49
+ """
50
+ return bool(first_image_content == second_image_content)
51
+
52
+
53
+ @beartype
54
+ class StructuralSimilarityMatcher:
55
+ """
56
+ A matcher which returns whether two images are similar using
57
+ SSIM.
58
+ """
59
+
60
+ def __call__(
61
+ self,
62
+ first_image_content: bytes,
63
+ second_image_content: bytes,
64
+ ) -> bool:
65
+ """Whether one image's content matches another's using a SSIM.
66
+
67
+ Args:
68
+ first_image_content: One image's content.
69
+ second_image_content: Another image's content.
70
+ """
71
+ first_image_file = io.BytesIO(initial_bytes=first_image_content)
72
+ second_image_file = io.BytesIO(initial_bytes=second_image_content)
73
+ with (
74
+ Image.open(fp=first_image_file) as first_image,
75
+ Image.open(fp=second_image_file) as second_image,
76
+ ):
77
+ # Images must be the same size, and they must be larger than the
78
+ # default SSIM window size of 11x11.
79
+ target_size = (256, 256)
80
+ first_image_resized = first_image.resize(size=target_size)
81
+ second_image_resized = second_image.resize(size=target_size)
82
+
83
+ first_image_np = np.array(object=first_image_resized, dtype=np.float32)
84
+ first_image_tensor = torch.tensor(data=first_image_np).float() / 255
85
+ first_image_tensor = first_image_tensor.view(
86
+ first_image_resized.size[1],
87
+ first_image_resized.size[0],
88
+ len(first_image_resized.getbands()),
89
+ )
90
+
91
+ second_image_np = np.array(
92
+ object=second_image_resized,
93
+ dtype=np.float32,
94
+ )
95
+ second_image_tensor = torch.tensor(data=second_image_np).float() / 255
96
+ second_image_tensor = second_image_tensor.view(
97
+ second_image_resized.size[1],
98
+ second_image_resized.size[0],
99
+ len(second_image_resized.getbands()),
100
+ )
101
+
102
+ first_image_tensor_batch_dimension = first_image_tensor.permute(
103
+ 2,
104
+ 0,
105
+ 1,
106
+ ).unsqueeze(dim=0)
107
+ second_image_tensor_batch_dimension = second_image_tensor.permute(
108
+ 2,
109
+ 0,
110
+ 1,
111
+ ).unsqueeze(dim=0)
112
+
113
+ ssim = StructuralSimilarityIndexMeasure(data_range=1.0)
114
+ ssim_value = ssim(
115
+ first_image_tensor_batch_dimension,
116
+ second_image_tensor_batch_dimension,
117
+ )
118
+ ssim_score = ssim_value.item()
119
+
120
+ # Normalize SSIM score from -1 to 1 scale to 0 to 10 scale.
121
+ # This maps -1 to 0 and 1 to 10.
122
+ normalized_score = (ssim_score + 1) * 5
123
+ minimum_acceptable_ssim_score = 7
124
+ return bool(normalized_score > minimum_acceptable_ssim_score)
@@ -0,0 +1,79 @@
1
+ """Model Target dataset objects."""
2
+
3
+ import datetime
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+ from typing import Any
8
+ from zoneinfo import ZoneInfo
9
+
10
+ from beartype import beartype
11
+
12
+
13
+ @beartype
14
+ class ModelTargetDatasetType(StrEnum):
15
+ """The kind of Model Target dataset."""
16
+
17
+ STANDARD = "standard"
18
+ ADVANCED = "advanced"
19
+
20
+
21
+ @beartype
22
+ def _now() -> datetime.datetime:
23
+ """Return the current time in UTC."""
24
+ return datetime.datetime.now(tz=ZoneInfo(key="UTC"))
25
+
26
+
27
+ @beartype
28
+ def _format_datetime(value: datetime.datetime) -> str:
29
+ """Format a timestamp like the Model Target Web API."""
30
+ return value.isoformat(timespec="milliseconds").replace("+00:00", "Z")
31
+
32
+
33
+ @beartype
34
+ @dataclass(frozen=True, kw_only=True)
35
+ class ModelTargetDataset:
36
+ """A Model Target dataset generation request.
37
+
38
+ Args:
39
+ request_body: The JSON request body used to start dataset creation.
40
+ dataset_type: Whether this is a standard or advanced dataset.
41
+ processing_time_seconds: The number of seconds before the generated
42
+ dataset becomes available.
43
+ uuid_: The dataset UUID.
44
+ created_at: When the dataset creation was requested.
45
+ """
46
+
47
+ request_body: dict[str, Any] = field(hash=False)
48
+ dataset_type: ModelTargetDatasetType
49
+ processing_time_seconds: float = field(hash=False)
50
+ uuid_: str = field(default_factory=lambda: uuid.uuid4().hex)
51
+ created_at: datetime.datetime = field(default_factory=_now)
52
+
53
+ @property
54
+ def completed_at(self) -> datetime.datetime:
55
+ """When the dataset completes processing."""
56
+ return self.created_at + datetime.timedelta(
57
+ seconds=self.processing_time_seconds,
58
+ )
59
+
60
+ @property
61
+ def status(self) -> str:
62
+ """The current dataset generation status."""
63
+ if _now() < self.completed_at:
64
+ return "processing"
65
+ return "done"
66
+
67
+ def status_body(self) -> dict[str, Any]:
68
+ """Return a status response body for this dataset."""
69
+ body: dict[str, Any] = {
70
+ "status": self.status,
71
+ "uuid": self.uuid_,
72
+ "createdAt": _format_datetime(value=self.created_at),
73
+ }
74
+ if self.status == "processing":
75
+ body["eta"] = _format_datetime(value=self.completed_at)
76
+ else:
77
+ body["completedAt"] = _format_datetime(value=self.completed_at)
78
+
79
+ return body
mock_vws/py.typed ADDED
File without changes
mock_vws/states.py ADDED
@@ -0,0 +1,20 @@
1
+ """Vuforia database states."""
2
+
3
+ from enum import StrEnum, auto, unique
4
+
5
+ from beartype import beartype
6
+
7
+
8
+ @beartype
9
+ @unique
10
+ class States(StrEnum):
11
+ """Constants representing various web service states."""
12
+
13
+ WORKING = auto()
14
+
15
+ PROJECT_SUSPENDED = auto()
16
+
17
+ # A project is inactive if the license key has been deleted.
18
+ PROJECT_INACTIVE = auto()
19
+
20
+ PROJECT_HAS_NO_API_ACCESS = auto()