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,447 @@
|
|
|
1
|
+
"""Storage layer for the mock Vuforia Flask application."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import copy
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
from enum import StrEnum, auto
|
|
8
|
+
from http import HTTPMethod, HTTPStatus
|
|
9
|
+
from typing import assert_never
|
|
10
|
+
from zoneinfo import ZoneInfo
|
|
11
|
+
|
|
12
|
+
from beartype import beartype
|
|
13
|
+
from flask import Flask, Response, request
|
|
14
|
+
from pydantic_settings import BaseSettings
|
|
15
|
+
|
|
16
|
+
from mock_vws.database import CloudDatabase, VuMarkDatabase
|
|
17
|
+
from mock_vws.database_type import DatabaseType
|
|
18
|
+
from mock_vws.states import States
|
|
19
|
+
from mock_vws.target import ImageTarget, VuMarkTarget
|
|
20
|
+
from mock_vws.target_manager import TargetManager
|
|
21
|
+
from mock_vws.target_raters import (
|
|
22
|
+
BrisqueTargetTrackingRater,
|
|
23
|
+
HardcodedTargetTrackingRater,
|
|
24
|
+
RandomTargetTrackingRater,
|
|
25
|
+
TargetTrackingRater,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
TARGET_MANAGER_FLASK_APP = Flask(import_name=__name__, static_folder=None)
|
|
29
|
+
|
|
30
|
+
TARGET_MANAGER = TargetManager()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@beartype
|
|
34
|
+
class _TargetRaterChoice(StrEnum):
|
|
35
|
+
"""Target rater choices."""
|
|
36
|
+
|
|
37
|
+
BRISQUE = auto()
|
|
38
|
+
PERFECT = auto()
|
|
39
|
+
RANDOM = auto()
|
|
40
|
+
|
|
41
|
+
def to_target_rater(
|
|
42
|
+
self: _TargetRaterChoice,
|
|
43
|
+
) -> TargetTrackingRater:
|
|
44
|
+
"""Get the target rater."""
|
|
45
|
+
match self:
|
|
46
|
+
case _TargetRaterChoice.BRISQUE:
|
|
47
|
+
return BrisqueTargetTrackingRater()
|
|
48
|
+
case _TargetRaterChoice.PERFECT:
|
|
49
|
+
return HardcodedTargetTrackingRater(rating=5)
|
|
50
|
+
case _TargetRaterChoice.RANDOM:
|
|
51
|
+
return RandomTargetTrackingRater()
|
|
52
|
+
case _ as unreachable:
|
|
53
|
+
assert_never(unreachable)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@beartype
|
|
57
|
+
class TargetManagerSettings(BaseSettings):
|
|
58
|
+
"""Settings for the Target Manager Flask app."""
|
|
59
|
+
|
|
60
|
+
target_manager_host: str = ""
|
|
61
|
+
target_rater: _TargetRaterChoice = _TargetRaterChoice.BRISQUE
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
65
|
+
rule="/cloud_databases/<string:database_name>",
|
|
66
|
+
methods=[HTTPMethod.DELETE],
|
|
67
|
+
)
|
|
68
|
+
@beartype
|
|
69
|
+
def delete_cloud_database(database_name: str) -> Response:
|
|
70
|
+
"""Delete a cloud database.
|
|
71
|
+
|
|
72
|
+
:status 200: The cloud database has been deleted.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
(matching_database,) = {
|
|
76
|
+
database
|
|
77
|
+
for database in TARGET_MANAGER.cloud_databases
|
|
78
|
+
if database_name == database.database_name
|
|
79
|
+
}
|
|
80
|
+
except ValueError:
|
|
81
|
+
return Response(response="", status=HTTPStatus.NOT_FOUND)
|
|
82
|
+
|
|
83
|
+
TARGET_MANAGER.remove_cloud_database(cloud_database=matching_database)
|
|
84
|
+
return Response(response="", status=HTTPStatus.OK)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
88
|
+
rule="/vumark_databases/<string:database_name>",
|
|
89
|
+
methods=[HTTPMethod.DELETE],
|
|
90
|
+
)
|
|
91
|
+
@beartype
|
|
92
|
+
def delete_vumark_database(database_name: str) -> Response:
|
|
93
|
+
"""Delete a VuMark database.
|
|
94
|
+
|
|
95
|
+
:status 200: The VuMark database has been deleted.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
(matching_database,) = {
|
|
99
|
+
database
|
|
100
|
+
for database in TARGET_MANAGER.vumark_databases
|
|
101
|
+
if database_name == database.database_name
|
|
102
|
+
}
|
|
103
|
+
except ValueError:
|
|
104
|
+
return Response(response="", status=HTTPStatus.NOT_FOUND)
|
|
105
|
+
|
|
106
|
+
TARGET_MANAGER.remove_vumark_database(vumark_database=matching_database)
|
|
107
|
+
return Response(response="", status=HTTPStatus.OK)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
111
|
+
rule="/cloud_databases", methods=[HTTPMethod.GET]
|
|
112
|
+
)
|
|
113
|
+
@beartype
|
|
114
|
+
def get_cloud_databases() -> Response:
|
|
115
|
+
"""Return a list of all cloud databases."""
|
|
116
|
+
databases = [
|
|
117
|
+
database.to_dict() for database in TARGET_MANAGER.cloud_databases
|
|
118
|
+
]
|
|
119
|
+
return Response(
|
|
120
|
+
response=json.dumps(obj=databases),
|
|
121
|
+
status=HTTPStatus.OK,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
126
|
+
rule="/vumark_databases",
|
|
127
|
+
methods=[HTTPMethod.GET],
|
|
128
|
+
)
|
|
129
|
+
@beartype
|
|
130
|
+
def get_vumark_databases() -> Response:
|
|
131
|
+
"""Return a list of all VuMark databases."""
|
|
132
|
+
databases = [
|
|
133
|
+
database.to_dict() for database in TARGET_MANAGER.vumark_databases
|
|
134
|
+
]
|
|
135
|
+
return Response(
|
|
136
|
+
response=json.dumps(obj=databases),
|
|
137
|
+
status=HTTPStatus.OK,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
142
|
+
rule="/cloud_databases", methods=[HTTPMethod.POST]
|
|
143
|
+
)
|
|
144
|
+
@beartype
|
|
145
|
+
def create_cloud_database() -> Response:
|
|
146
|
+
"""Create a new cloud database.
|
|
147
|
+
|
|
148
|
+
:reqheader Content-Type: application/json
|
|
149
|
+
:resheader Content-Type: application/json
|
|
150
|
+
|
|
151
|
+
:reqjson string client_access_key: (Optional) The client access key for the
|
|
152
|
+
cloud database.
|
|
153
|
+
|
|
154
|
+
:reqjson string client_secret_key: (Optional) The client secret key for the
|
|
155
|
+
cloud database.
|
|
156
|
+
|
|
157
|
+
:reqjson string database_name: (Optional) The name of the cloud database.
|
|
158
|
+
|
|
159
|
+
:reqjson int request_quota: (Optional) The request quota. Set this to zero
|
|
160
|
+
to make VWS endpoints return ``RequestQuotaReached``.
|
|
161
|
+
|
|
162
|
+
:reqjson int target_quota: (Optional) The target quota. Once this many
|
|
163
|
+
targets exist, adding another returns ``TargetQuotaReached``.
|
|
164
|
+
|
|
165
|
+
:reqjson string server_access_key: (Optional) The server access key for the
|
|
166
|
+
cloud database.
|
|
167
|
+
|
|
168
|
+
:reqjson string server_secret_key: (Optional) The server secret key for the
|
|
169
|
+
cloud database.
|
|
170
|
+
|
|
171
|
+
:reqjson string state_name: (Optional) The state of the cloud database.
|
|
172
|
+
This can be "WORKING", "PROJECT_INACTIVE", "PROJECT_SUSPENDED", or
|
|
173
|
+
"PROJECT_HAS_NO_API_ACCESS". This defaults to "WORKING".
|
|
174
|
+
|
|
175
|
+
:resjson string client_access_key: The client access key for the cloud
|
|
176
|
+
database.
|
|
177
|
+
|
|
178
|
+
:resjson string client_secret_key: The client secret key for the cloud
|
|
179
|
+
database.
|
|
180
|
+
|
|
181
|
+
:resjson string database_name: The cloud database name.
|
|
182
|
+
|
|
183
|
+
:resjson int request_quota: The request quota.
|
|
184
|
+
|
|
185
|
+
:resjson int target_quota: The target quota.
|
|
186
|
+
|
|
187
|
+
:resjson string server_access_key: The server access key for the cloud
|
|
188
|
+
database.
|
|
189
|
+
|
|
190
|
+
:resjson string server_secret_key: The server secret key for the cloud
|
|
191
|
+
database.
|
|
192
|
+
|
|
193
|
+
:resjson string state_name: The cloud database state.
|
|
194
|
+
|
|
195
|
+
:reqjsonarr targets: The targets in the cloud database.
|
|
196
|
+
|
|
197
|
+
:status 201: The cloud database has been successfully created.
|
|
198
|
+
"""
|
|
199
|
+
random_database = CloudDatabase()
|
|
200
|
+
request_json = json.loads(s=request.data)
|
|
201
|
+
server_access_key = request_json.get(
|
|
202
|
+
"server_access_key",
|
|
203
|
+
random_database.server_access_key,
|
|
204
|
+
)
|
|
205
|
+
server_secret_key = request_json.get(
|
|
206
|
+
"server_secret_key",
|
|
207
|
+
random_database.server_secret_key,
|
|
208
|
+
)
|
|
209
|
+
client_access_key = request_json.get(
|
|
210
|
+
"client_access_key",
|
|
211
|
+
random_database.client_access_key,
|
|
212
|
+
)
|
|
213
|
+
client_secret_key = request_json.get(
|
|
214
|
+
"client_secret_key",
|
|
215
|
+
random_database.client_secret_key,
|
|
216
|
+
)
|
|
217
|
+
database_name = request_json.get(
|
|
218
|
+
"database_name",
|
|
219
|
+
random_database.database_name,
|
|
220
|
+
)
|
|
221
|
+
state_name = request_json.get(
|
|
222
|
+
"state_name",
|
|
223
|
+
random_database.state.name,
|
|
224
|
+
)
|
|
225
|
+
database_type_name = request_json.get(
|
|
226
|
+
"database_type_name",
|
|
227
|
+
random_database.database_type.name,
|
|
228
|
+
)
|
|
229
|
+
request_quota = request_json.get(
|
|
230
|
+
"request_quota",
|
|
231
|
+
random_database.request_quota,
|
|
232
|
+
)
|
|
233
|
+
target_quota = request_json.get(
|
|
234
|
+
"target_quota",
|
|
235
|
+
random_database.target_quota,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
state = States[state_name]
|
|
239
|
+
database_type = DatabaseType[database_type_name]
|
|
240
|
+
|
|
241
|
+
database = CloudDatabase(
|
|
242
|
+
server_access_key=server_access_key,
|
|
243
|
+
server_secret_key=server_secret_key,
|
|
244
|
+
client_access_key=client_access_key,
|
|
245
|
+
client_secret_key=client_secret_key,
|
|
246
|
+
database_name=database_name,
|
|
247
|
+
state=state,
|
|
248
|
+
database_type=database_type,
|
|
249
|
+
request_quota=request_quota,
|
|
250
|
+
target_quota=target_quota,
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
TARGET_MANAGER.add_cloud_database(cloud_database=database)
|
|
254
|
+
except ValueError as exc:
|
|
255
|
+
return Response(
|
|
256
|
+
response=str(object=exc),
|
|
257
|
+
status=HTTPStatus.CONFLICT,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return Response(
|
|
261
|
+
response=json.dumps(obj=database.to_dict()),
|
|
262
|
+
status=HTTPStatus.CREATED,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
267
|
+
rule="/vumark_databases",
|
|
268
|
+
methods=[HTTPMethod.POST],
|
|
269
|
+
)
|
|
270
|
+
@beartype
|
|
271
|
+
def create_vumark_database() -> Response:
|
|
272
|
+
"""Create a new VuMark database.
|
|
273
|
+
|
|
274
|
+
:status 201: The database has been successfully created.
|
|
275
|
+
"""
|
|
276
|
+
request_json = json.loads(s=request.data)
|
|
277
|
+
random_vumark_database = VuMarkDatabase()
|
|
278
|
+
state_name = request_json.get(
|
|
279
|
+
"state_name",
|
|
280
|
+
random_vumark_database.state.name,
|
|
281
|
+
)
|
|
282
|
+
database = VuMarkDatabase(
|
|
283
|
+
server_access_key=request_json.get(
|
|
284
|
+
"server_access_key",
|
|
285
|
+
random_vumark_database.server_access_key,
|
|
286
|
+
),
|
|
287
|
+
server_secret_key=request_json.get(
|
|
288
|
+
"server_secret_key",
|
|
289
|
+
random_vumark_database.server_secret_key,
|
|
290
|
+
),
|
|
291
|
+
database_name=request_json.get(
|
|
292
|
+
"database_name",
|
|
293
|
+
random_vumark_database.database_name,
|
|
294
|
+
),
|
|
295
|
+
state=States[state_name],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
TARGET_MANAGER.add_vumark_database(vumark_database=database)
|
|
300
|
+
except ValueError as exc:
|
|
301
|
+
return Response(
|
|
302
|
+
response=str(object=exc),
|
|
303
|
+
status=HTTPStatus.CONFLICT,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return Response(
|
|
307
|
+
response=json.dumps(obj=database.to_dict()),
|
|
308
|
+
status=HTTPStatus.CREATED,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
313
|
+
rule="/cloud_databases/<string:database_name>/targets",
|
|
314
|
+
methods=[HTTPMethod.POST],
|
|
315
|
+
)
|
|
316
|
+
@beartype
|
|
317
|
+
def create_target(database_name: str) -> Response:
|
|
318
|
+
"""Create a new target in a given cloud database."""
|
|
319
|
+
(database,) = (
|
|
320
|
+
database
|
|
321
|
+
for database in TARGET_MANAGER.cloud_databases
|
|
322
|
+
if database.database_name == database_name
|
|
323
|
+
)
|
|
324
|
+
request_json = json.loads(s=request.data)
|
|
325
|
+
settings = TargetManagerSettings.model_validate(obj={})
|
|
326
|
+
|
|
327
|
+
image_bytes = base64.b64decode(s=request_json["image_base64"])
|
|
328
|
+
target_tracking_rater = settings.target_rater.to_target_rater()
|
|
329
|
+
target = ImageTarget(
|
|
330
|
+
name=request_json["name"],
|
|
331
|
+
width=request_json["width"],
|
|
332
|
+
image_value=image_bytes,
|
|
333
|
+
active_flag=request_json["active_flag"],
|
|
334
|
+
processing_time_seconds=request_json["processing_time_seconds"],
|
|
335
|
+
application_metadata=request_json["application_metadata"],
|
|
336
|
+
target_id=request_json["target_id"],
|
|
337
|
+
target_tracking_rater=target_tracking_rater,
|
|
338
|
+
)
|
|
339
|
+
database.targets.add(target)
|
|
340
|
+
|
|
341
|
+
return Response(
|
|
342
|
+
response=json.dumps(obj=target.to_dict()),
|
|
343
|
+
status=HTTPStatus.CREATED,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
348
|
+
rule="/vumark_databases/<string:database_name>/vumark_targets",
|
|
349
|
+
methods=[HTTPMethod.POST],
|
|
350
|
+
)
|
|
351
|
+
@beartype
|
|
352
|
+
def create_vumark_target(database_name: str) -> Response:
|
|
353
|
+
"""Create a new VuMark target in a given database."""
|
|
354
|
+
(database,) = (
|
|
355
|
+
database
|
|
356
|
+
for database in TARGET_MANAGER.vumark_databases
|
|
357
|
+
if database.database_name == database_name
|
|
358
|
+
)
|
|
359
|
+
request_json = json.loads(s=request.data)
|
|
360
|
+
target = VuMarkTarget.from_dict(target_dict=request_json)
|
|
361
|
+
database.vumark_targets.add(target)
|
|
362
|
+
|
|
363
|
+
return Response(
|
|
364
|
+
response=json.dumps(obj=target.to_dict()),
|
|
365
|
+
status=HTTPStatus.CREATED,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
370
|
+
rule="/cloud_databases/<string:database_name>/targets/<string:target_id>",
|
|
371
|
+
methods={HTTPMethod.DELETE},
|
|
372
|
+
)
|
|
373
|
+
@beartype
|
|
374
|
+
def delete_target(database_name: str, target_id: str) -> Response:
|
|
375
|
+
"""Delete a target."""
|
|
376
|
+
(database,) = (
|
|
377
|
+
database
|
|
378
|
+
for database in TARGET_MANAGER.cloud_databases
|
|
379
|
+
if database.database_name == database_name
|
|
380
|
+
)
|
|
381
|
+
target = database.get_target(target_id=target_id)
|
|
382
|
+
now = datetime.datetime.now(tz=target.upload_date.tzinfo)
|
|
383
|
+
# See https://github.com/facebook/pyrefly/issues/1897
|
|
384
|
+
new_target: ImageTarget = copy.replace(
|
|
385
|
+
target, # pyrefly: ignore[bad-argument-type]
|
|
386
|
+
delete_date=now,
|
|
387
|
+
)
|
|
388
|
+
database.targets.remove(target)
|
|
389
|
+
database.targets.add(new_target)
|
|
390
|
+
return Response(
|
|
391
|
+
response=json.dumps(obj=new_target.to_dict()),
|
|
392
|
+
status=HTTPStatus.OK,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@TARGET_MANAGER_FLASK_APP.route(
|
|
397
|
+
rule="/cloud_databases/<string:database_name>/targets/<string:target_id>",
|
|
398
|
+
methods=[HTTPMethod.PUT],
|
|
399
|
+
)
|
|
400
|
+
@beartype
|
|
401
|
+
def update_target(database_name: str, target_id: str) -> Response:
|
|
402
|
+
"""Update a target."""
|
|
403
|
+
(database,) = (
|
|
404
|
+
database
|
|
405
|
+
for database in TARGET_MANAGER.cloud_databases
|
|
406
|
+
if database.database_name == database_name
|
|
407
|
+
)
|
|
408
|
+
target = database.get_target(target_id=target_id)
|
|
409
|
+
|
|
410
|
+
request_json = json.loads(s=request.data)
|
|
411
|
+
name = request_json.get("name", target.name)
|
|
412
|
+
active_flag = request_json.get("active_flag", target.active_flag)
|
|
413
|
+
|
|
414
|
+
gmt = ZoneInfo(key="GMT")
|
|
415
|
+
last_modified_date = datetime.datetime.now(tz=gmt)
|
|
416
|
+
|
|
417
|
+
width = request_json.get("width", target.width)
|
|
418
|
+
application_metadata = request_json.get(
|
|
419
|
+
"application_metadata",
|
|
420
|
+
target.application_metadata,
|
|
421
|
+
)
|
|
422
|
+
image_value = target.image_value
|
|
423
|
+
if "image" in request_json:
|
|
424
|
+
image_value = base64.b64decode(s=request_json["image"])
|
|
425
|
+
# See https://github.com/facebook/pyrefly/issues/1897
|
|
426
|
+
new_target: ImageTarget = copy.replace(
|
|
427
|
+
target, # pyrefly: ignore[bad-argument-type]
|
|
428
|
+
name=name,
|
|
429
|
+
width=width,
|
|
430
|
+
active_flag=active_flag,
|
|
431
|
+
application_metadata=application_metadata,
|
|
432
|
+
image_value=image_value,
|
|
433
|
+
last_modified_date=last_modified_date,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
database.targets.remove(target)
|
|
437
|
+
database.targets.add(new_target)
|
|
438
|
+
|
|
439
|
+
return Response(
|
|
440
|
+
response=json.dumps(obj=new_target.to_dict()),
|
|
441
|
+
status=HTTPStatus.OK,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
if __name__ == "__main__": # pragma: no cover
|
|
446
|
+
SETTINGS = TargetManagerSettings.model_validate(obj={})
|
|
447
|
+
TARGET_MANAGER_FLASK_APP.run(host=SETTINGS.target_manager_host)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""A fake implementation of the Vuforia Web Query API using Flask.
|
|
2
|
+
|
|
3
|
+
See
|
|
4
|
+
https://developer.vuforia.com/library/web-api/vuforia-query-web-api
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import email.utils
|
|
8
|
+
import time
|
|
9
|
+
from enum import StrEnum, auto
|
|
10
|
+
from http import HTTPMethod, HTTPStatus
|
|
11
|
+
from typing import assert_never
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from beartype import beartype
|
|
15
|
+
from flask import Flask, Response, request
|
|
16
|
+
from pydantic_settings import BaseSettings
|
|
17
|
+
|
|
18
|
+
from mock_vws._query_tools import (
|
|
19
|
+
get_query_match_response_text,
|
|
20
|
+
)
|
|
21
|
+
from mock_vws._query_validators import run_query_validators
|
|
22
|
+
from mock_vws._query_validators.exceptions import (
|
|
23
|
+
ValidatorError,
|
|
24
|
+
)
|
|
25
|
+
from mock_vws.database import CloudDatabase
|
|
26
|
+
from mock_vws.image_matchers import (
|
|
27
|
+
ExactMatcher,
|
|
28
|
+
ImageMatcher,
|
|
29
|
+
StructuralSimilarityMatcher,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
CLOUDRECO_FLASK_APP = Flask(import_name=__name__, static_folder=None)
|
|
33
|
+
CLOUDRECO_FLASK_APP.config["PROPAGATE_EXCEPTIONS"] = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@beartype
|
|
37
|
+
class _ImageMatcherChoice(StrEnum):
|
|
38
|
+
"""Image matcher choices."""
|
|
39
|
+
|
|
40
|
+
EXACT = auto()
|
|
41
|
+
STRUCTURAL_SIMILARITY = auto()
|
|
42
|
+
|
|
43
|
+
def to_image_matcher(self: _ImageMatcherChoice) -> ImageMatcher:
|
|
44
|
+
"""Get the image matcher."""
|
|
45
|
+
match self:
|
|
46
|
+
case _ImageMatcherChoice.EXACT:
|
|
47
|
+
return ExactMatcher()
|
|
48
|
+
case _ImageMatcherChoice.STRUCTURAL_SIMILARITY:
|
|
49
|
+
return StructuralSimilarityMatcher()
|
|
50
|
+
case _ as unreachable:
|
|
51
|
+
assert_never(unreachable)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@beartype
|
|
55
|
+
class VWQSettings(BaseSettings):
|
|
56
|
+
"""Settings for the VWQ Flask app."""
|
|
57
|
+
|
|
58
|
+
vwq_host: str = ""
|
|
59
|
+
target_manager_base_url: str
|
|
60
|
+
query_image_matcher: _ImageMatcherChoice = (
|
|
61
|
+
_ImageMatcherChoice.STRUCTURAL_SIMILARITY
|
|
62
|
+
)
|
|
63
|
+
response_delay_seconds: float = 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@beartype
|
|
67
|
+
def get_all_cloud_databases() -> set[CloudDatabase]:
|
|
68
|
+
"""Get all database objects from the target manager back-end."""
|
|
69
|
+
settings = VWQSettings.model_validate(obj={})
|
|
70
|
+
response = requests.get(
|
|
71
|
+
url=f"{settings.target_manager_base_url}/cloud_databases",
|
|
72
|
+
timeout=30,
|
|
73
|
+
)
|
|
74
|
+
return {
|
|
75
|
+
CloudDatabase.from_dict(database_dict=database_dict)
|
|
76
|
+
for database_dict in response.json()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@CLOUDRECO_FLASK_APP.before_request
|
|
81
|
+
@beartype
|
|
82
|
+
def set_terminate_wsgi_input() -> None:
|
|
83
|
+
"""We set ``wsgi.input_terminated`` to ``True`` when going through
|
|
84
|
+
``requests`` in our tests, so that requests have the given ``Content-
|
|
85
|
+
Length`` headers and the given data in ``request.headers`` and
|
|
86
|
+
``request.data``.
|
|
87
|
+
|
|
88
|
+
We do not set this at all when running an application as standalone.
|
|
89
|
+
This is because when running the Flask application, if this is set,
|
|
90
|
+
reading ``request.data`` hangs.
|
|
91
|
+
|
|
92
|
+
Therefore, when running the real Flask application, the behavior is not the
|
|
93
|
+
same as the real Vuforia.
|
|
94
|
+
This is documented as a difference in the documentation for this package.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
set_terminate_wsgi_input_true = (
|
|
98
|
+
CLOUDRECO_FLASK_APP.config["VWS_MOCK_TERMINATE_WSGI_INPUT"] is True
|
|
99
|
+
)
|
|
100
|
+
except KeyError:
|
|
101
|
+
set_terminate_wsgi_input_true = False
|
|
102
|
+
|
|
103
|
+
if set_terminate_wsgi_input_true:
|
|
104
|
+
request.environ["wsgi.input_terminated"] = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@CLOUDRECO_FLASK_APP.after_request
|
|
108
|
+
@beartype
|
|
109
|
+
def add_response_delay(response: Response) -> Response:
|
|
110
|
+
"""Add a delay to each response."""
|
|
111
|
+
settings = VWQSettings.model_validate(obj={})
|
|
112
|
+
time.sleep(settings.response_delay_seconds)
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@CLOUDRECO_FLASK_APP.errorhandler(code_or_exception=ValidatorError)
|
|
117
|
+
@beartype
|
|
118
|
+
def handle_exceptions(exc: ValidatorError) -> Response:
|
|
119
|
+
"""Return the error response associated with the given exception."""
|
|
120
|
+
response = Response(
|
|
121
|
+
status=exc.status_code.value,
|
|
122
|
+
response=exc.response_text,
|
|
123
|
+
headers=exc.headers,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
response.headers.clear()
|
|
127
|
+
response.headers.extend(exc.headers)
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@CLOUDRECO_FLASK_APP.route(rule="/v1/query", methods=[HTTPMethod.POST])
|
|
132
|
+
@beartype
|
|
133
|
+
def query() -> Response:
|
|
134
|
+
"""Perform an image recognition query."""
|
|
135
|
+
settings = VWQSettings.model_validate(obj={})
|
|
136
|
+
query_match_checker = settings.query_image_matcher.to_image_matcher()
|
|
137
|
+
|
|
138
|
+
databases = get_all_cloud_databases()
|
|
139
|
+
request_body = request.stream.read()
|
|
140
|
+
run_query_validators(
|
|
141
|
+
request_headers=dict(request.headers),
|
|
142
|
+
request_body=request_body,
|
|
143
|
+
request_method=request.method,
|
|
144
|
+
request_path=request.path,
|
|
145
|
+
databases=databases,
|
|
146
|
+
)
|
|
147
|
+
date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
|
|
148
|
+
|
|
149
|
+
response_text = get_query_match_response_text(
|
|
150
|
+
request_headers=dict(request.headers),
|
|
151
|
+
request_body=request_body,
|
|
152
|
+
request_method=request.method,
|
|
153
|
+
request_path=request.path,
|
|
154
|
+
databases=databases,
|
|
155
|
+
query_match_checker=query_match_checker,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
headers = {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
"Date": date,
|
|
161
|
+
"Connection": "keep-alive",
|
|
162
|
+
"Server": "nginx",
|
|
163
|
+
}
|
|
164
|
+
return Response(
|
|
165
|
+
status=HTTPStatus.OK,
|
|
166
|
+
response=response_text,
|
|
167
|
+
headers=headers,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__": # pragma: no cover
|
|
172
|
+
SETTINGS = VWQSettings.model_validate(obj={})
|
|
173
|
+
CLOUDRECO_FLASK_APP.run(host=SETTINGS.vwq_host)
|