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
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,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)
|
mock_vws/model_target.py
ADDED
|
@@ -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()
|