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,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)