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,75 @@
1
+ """Common utilities for creating mock routes."""
2
+
3
+ import json
4
+ from collections.abc import Iterable, Mapping
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from beartype import beartype
9
+
10
+
11
+ @beartype
12
+ class MissingSchemeError(Exception):
13
+ """Raised when a URL is missing a schema."""
14
+
15
+ def __init__(self, url: str) -> None:
16
+ """
17
+ Args:
18
+ url: The URL which is missing a scheme.
19
+ """
20
+ super().__init__()
21
+ self.url = url
22
+
23
+ def __str__(self) -> str:
24
+ """
25
+ Give a string representation of this error with a
26
+ suggestion.
27
+ """
28
+ return (
29
+ f'Invalid URL "{self.url}": No scheme supplied. '
30
+ f'Perhaps you meant "https://{self.url}".'
31
+ )
32
+
33
+
34
+ @beartype
35
+ @dataclass(frozen=True, kw_only=True)
36
+ class RequestData:
37
+ """A library-agnostic representation of an HTTP request.
38
+
39
+ Args:
40
+ method: The HTTP method of the request.
41
+ path: The path of the request.
42
+ headers: The headers sent with the request.
43
+ body: The body of the request.
44
+ """
45
+
46
+ method: str
47
+ path: str
48
+ headers: Mapping[str, str]
49
+ body: bytes
50
+
51
+
52
+ @beartype
53
+ @dataclass(frozen=True, kw_only=True)
54
+ class Route:
55
+ """A representation of a VWS route.
56
+
57
+ Args:
58
+ route_name: The name of the method.
59
+ path_pattern: The end part of a URL pattern. E.g. `/targets` or
60
+ `/targets/.+`.
61
+ http_methods: HTTP methods that map to the route function.
62
+ """
63
+
64
+ route_name: str
65
+ path_pattern: str
66
+ http_methods: Iterable[str]
67
+
68
+
69
+ @beartype
70
+ def json_dump(*, body: dict[str, Any]) -> str:
71
+ """
72
+ Returns:
73
+ JSON dump of data in the same way that Vuforia dumps data.
74
+ """
75
+ return json.dumps(obj=body, separators=(",", ":"))
@@ -0,0 +1,486 @@
1
+ """A fake implementation of the Model Target Web API."""
2
+
3
+ import base64
4
+ import io
5
+ import json
6
+ import uuid
7
+ import zipfile
8
+ from http import HTTPStatus
9
+ from typing import Any
10
+ from urllib.parse import parse_qs
11
+
12
+ from beartype import beartype
13
+
14
+ from mock_vws._mock_common import RequestData, json_dump
15
+ from mock_vws.model_target import ModelTargetDataset, ModelTargetDatasetType
16
+ from mock_vws.target_manager import TargetManager
17
+
18
+ _ResponseType = tuple[int, dict[str, str], str | bytes]
19
+ _MAX_ADVANCED_MODEL_COUNT = 20
20
+ _JWT_DOT_COUNT = 2
21
+ _ZIP_EPOCH = (1980, 1, 1, 0, 0, 0)
22
+ _MOCK_MODEL_TARGET_CLIENT_ID = "client-id"
23
+ _MOCK_MODEL_TARGET_CLIENT_SECRET = "client-secret" # noqa: S105
24
+ # A stable mock value standing in for the user-id segment that real
25
+ # Vuforia embeds in some Model Target error targets such as
26
+ # ``userId:7635391``. The numeric portion is per-account in real Vuforia;
27
+ # the mock uses a fixed placeholder.
28
+ _MOCK_USER_TARGET = "userId:mock"
29
+
30
+
31
+ @beartype
32
+ def _json_response(
33
+ *,
34
+ status_code: HTTPStatus,
35
+ body: dict[str, Any],
36
+ ) -> _ResponseType:
37
+ """Return a JSON response."""
38
+ body_json = json_dump(body=body)
39
+ return (
40
+ status_code,
41
+ {
42
+ "Content-Length": str(object=len(body_json)),
43
+ "Content-Type": "application/json",
44
+ },
45
+ body_json,
46
+ )
47
+
48
+
49
+ @beartype
50
+ def _error_response(
51
+ *,
52
+ status_code: HTTPStatus,
53
+ code: str,
54
+ message: str,
55
+ target: str | None,
56
+ details: list[dict[str, str]] | None,
57
+ ) -> _ResponseType:
58
+ """Return an error response shaped like the Model Target Web API."""
59
+ error: dict[str, Any] = {"code": code, "message": message}
60
+ if target is not None:
61
+ error["target"] = target
62
+ if details is not None:
63
+ error["details"] = details
64
+ return _json_response(status_code=status_code, body={"error": error})
65
+
66
+
67
+ @beartype
68
+ def _validation_error_response(
69
+ *,
70
+ details: list[dict[str, str]],
71
+ ) -> _ResponseType:
72
+ """Return a Vuforia-style validation error.
73
+
74
+ Real Vuforia tags each validation error with a per-request UUID that
75
+ appears in both ``message`` and ``target``. The mock generates a fresh
76
+ UUID so the shape matches.
77
+ """
78
+ request_uuid = uuid.uuid4().hex
79
+ return _error_response(
80
+ status_code=HTTPStatus.BAD_REQUEST,
81
+ code="BAD_REQUEST",
82
+ message=f"Validation error for request {request_uuid}",
83
+ target=request_uuid,
84
+ details=details,
85
+ )
86
+
87
+
88
+ @beartype
89
+ def _oauth2_error_response(
90
+ *,
91
+ status_code: HTTPStatus,
92
+ body: dict[str, str],
93
+ ) -> _ResponseType:
94
+ """Return an OAuth2 error response."""
95
+ return _json_response(status_code=status_code, body=body)
96
+
97
+
98
+ @beartype
99
+ def _get_header(request: RequestData, name: str) -> str | None:
100
+ """Return a request header, case-insensitively."""
101
+ lower_name = name.casefold()
102
+ for key, value in request.headers.items():
103
+ if key.casefold() == lower_name:
104
+ return value
105
+ return None
106
+
107
+
108
+ @beartype
109
+ def _basic_auth_credentials(auth_header: str | None) -> tuple[str, str] | None:
110
+ """Return HTTP Basic credentials from an authorization header."""
111
+ if auth_header is None or not auth_header.startswith("Basic "):
112
+ return None
113
+
114
+ encoded_credentials = auth_header.removeprefix("Basic ").strip()
115
+ try:
116
+ decoded_credentials = base64.b64decode(
117
+ s=encoded_credentials,
118
+ validate=True,
119
+ ).decode(encoding="utf-8")
120
+ except ValueError:
121
+ return None
122
+
123
+ client_id, separator, client_secret = decoded_credentials.partition(":")
124
+ if not separator:
125
+ return None
126
+
127
+ return client_id, client_secret
128
+
129
+
130
+ @beartype
131
+ def _require_bearer_token(request: RequestData) -> _ResponseType | None:
132
+ """Return an error response if the request has no bearer token."""
133
+ auth_header = _get_header(request=request, name="Authorization")
134
+ if auth_header is None or not auth_header.startswith("Bearer "):
135
+ return _error_response(
136
+ status_code=HTTPStatus.UNAUTHORIZED,
137
+ code="401",
138
+ message="no Bearer token",
139
+ target="jwt",
140
+ details=None,
141
+ )
142
+ bearer_token = auth_header.removeprefix("Bearer ").strip()
143
+ if not bearer_token:
144
+ return _error_response(
145
+ status_code=HTTPStatus.UNAUTHORIZED,
146
+ code="401",
147
+ message="no Bearer token",
148
+ target="jwt",
149
+ details=None,
150
+ )
151
+ if bearer_token.count(".") != _JWT_DOT_COUNT:
152
+ return _error_response(
153
+ status_code=HTTPStatus.UNAUTHORIZED,
154
+ code="401",
155
+ message="Invalid JWT serialization: Missing dot delimiter(s)",
156
+ target="jwt",
157
+ details=None,
158
+ )
159
+ return None
160
+
161
+
162
+ @beartype
163
+ def _fake_jwt(*, token_source: bytes) -> str:
164
+ """Return a deterministic bearer token for the mock."""
165
+
166
+ def encode_part(value: dict[str, Any]) -> str:
167
+ """Return a base64url-encoded token part."""
168
+ raw_part = json.dumps(
169
+ obj=value,
170
+ sort_keys=True,
171
+ separators=(",", ":"),
172
+ ).encode(encoding="utf-8")
173
+ return (
174
+ base64.urlsafe_b64encode(s=raw_part)
175
+ .decode(
176
+ encoding="ascii",
177
+ )
178
+ .rstrip("=")
179
+ )
180
+
181
+ header = encode_part(value={"alg": "mock", "typ": "JWT"})
182
+ payload = encode_part(
183
+ value={
184
+ "aud": "vuforia-model-target",
185
+ "src": base64.urlsafe_b64encode(s=token_source)
186
+ .decode(
187
+ encoding="ascii",
188
+ )
189
+ .rstrip("="),
190
+ },
191
+ )
192
+ return f"{header}.{payload}.mock-signature"
193
+
194
+
195
+ @beartype
196
+ def oauth2_token(request: RequestData) -> _ResponseType:
197
+ """Return a fake OAuth2 access token."""
198
+ auth_header = _get_header(request=request, name="Authorization")
199
+ form = parse_qs(qs=request.body.decode(encoding="utf-8"))
200
+ grant_type = form.get("grant_type", ["client_credentials"])[0]
201
+ if grant_type != "client_credentials":
202
+ return _oauth2_error_response(
203
+ status_code=HTTPStatus.BAD_REQUEST,
204
+ body={"error": "unsupported_grant_type"},
205
+ )
206
+
207
+ basic_credentials = _basic_auth_credentials(auth_header=auth_header)
208
+ if basic_credentials is None:
209
+ return _oauth2_error_response(
210
+ status_code=HTTPStatus.UNAUTHORIZED,
211
+ body={
212
+ "error": "invalid_request",
213
+ "error_description": (
214
+ "Missing or invalid authorization header"
215
+ ),
216
+ },
217
+ )
218
+
219
+ if basic_credentials != (
220
+ _MOCK_MODEL_TARGET_CLIENT_ID,
221
+ _MOCK_MODEL_TARGET_CLIENT_SECRET,
222
+ ):
223
+ return _oauth2_error_response(
224
+ status_code=HTTPStatus.UNAUTHORIZED,
225
+ body={"error": "invalid_client"},
226
+ )
227
+
228
+ token_source = request.body or (auth_header or "").encode()
229
+ return _json_response(
230
+ status_code=HTTPStatus.OK,
231
+ body={
232
+ "access_token": _fake_jwt(token_source=token_source),
233
+ "token_type": "bearer",
234
+ "expires_in": 3600,
235
+ },
236
+ )
237
+
238
+
239
+ @beartype
240
+ def _load_request_json(request: RequestData) -> dict[str, Any] | _ResponseType:
241
+ """Load a Model Target dataset creation request body."""
242
+ content_type = _get_header(request=request, name="Content-Type") or ""
243
+ if "application/json" not in content_type:
244
+ return _error_response(
245
+ status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
246
+ code="ERROR",
247
+ message="Expecting text/json or application/json body",
248
+ target=None,
249
+ details=None,
250
+ )
251
+ try:
252
+ request_json: dict[str, Any] = json.loads(s=request.body)
253
+ except json.JSONDecodeError as exc:
254
+ return _error_response(
255
+ status_code=HTTPStatus.BAD_REQUEST,
256
+ code="ERROR",
257
+ message=f"Invalid Json: {exc}",
258
+ target=None,
259
+ details=None,
260
+ )
261
+ return request_json
262
+
263
+
264
+ @beartype
265
+ def _validate_dataset_request(
266
+ *,
267
+ request_json: dict[str, Any],
268
+ dataset_type: ModelTargetDatasetType,
269
+ ) -> _ResponseType | None:
270
+ """Validate the dataset request enough for useful mock feedback."""
271
+ missing_details = [
272
+ {
273
+ "code": "VALIDATION_ERROR",
274
+ "message": f"/{field}: element is required",
275
+ }
276
+ for field in ("models", "name", "targetSdk")
277
+ if field not in request_json
278
+ ]
279
+ if missing_details:
280
+ return _validation_error_response(details=missing_details)
281
+
282
+ models_value = request_json["models"]
283
+ if not isinstance(models_value, list):
284
+ return _validation_error_response(
285
+ details=[
286
+ {
287
+ "code": "VALIDATION_ERROR",
288
+ "message": "/models: error.expected.jsarray",
289
+ },
290
+ ],
291
+ )
292
+
293
+ models: list[Any] = [*models_value]
294
+ model_count = len(models)
295
+
296
+ if dataset_type == ModelTargetDatasetType.STANDARD and model_count != 1:
297
+ return _validation_error_response(
298
+ details=[
299
+ {
300
+ "code": "VALIDATION_ERROR",
301
+ "message": "exactly one model should be provided",
302
+ },
303
+ ],
304
+ )
305
+
306
+ if (
307
+ dataset_type == ModelTargetDatasetType.ADVANCED
308
+ and not 1 <= model_count <= _MAX_ADVANCED_MODEL_COUNT
309
+ ):
310
+ return _validation_error_response(
311
+ details=[
312
+ {
313
+ "code": "VALIDATION_ERROR",
314
+ "message": (
315
+ "models must contain between 1 and "
316
+ f"{_MAX_ADVANCED_MODEL_COUNT} entries"
317
+ ),
318
+ },
319
+ ],
320
+ )
321
+
322
+ return None
323
+
324
+
325
+ @beartype
326
+ def create_model_target_dataset(
327
+ *,
328
+ request: RequestData,
329
+ target_manager: TargetManager,
330
+ processing_time_seconds: float,
331
+ dataset_type: ModelTargetDatasetType,
332
+ ) -> _ResponseType:
333
+ """Create a standard or advanced Model Target dataset."""
334
+ auth_error = _require_bearer_token(request=request)
335
+ if auth_error is not None:
336
+ return auth_error
337
+
338
+ request_json_or_error = _load_request_json(request=request)
339
+ if not isinstance(request_json_or_error, dict):
340
+ return request_json_or_error
341
+
342
+ validation_error = _validate_dataset_request(
343
+ request_json=request_json_or_error,
344
+ dataset_type=dataset_type,
345
+ )
346
+ if validation_error is not None:
347
+ return validation_error
348
+
349
+ dataset = ModelTargetDataset(
350
+ request_body=request_json_or_error,
351
+ dataset_type=dataset_type,
352
+ processing_time_seconds=processing_time_seconds,
353
+ )
354
+ target_manager.add_model_target_dataset(model_target_dataset=dataset)
355
+ return _json_response(
356
+ status_code=HTTPStatus.CREATED,
357
+ body={"uuid": dataset.uuid_},
358
+ )
359
+
360
+
361
+ @beartype
362
+ def get_model_target_dataset_status(
363
+ *,
364
+ request: RequestData,
365
+ target_manager: TargetManager,
366
+ dataset_uuid: str,
367
+ ) -> _ResponseType:
368
+ """Return the status of a Model Target dataset."""
369
+ auth_error = _require_bearer_token(request=request)
370
+ if auth_error is not None:
371
+ return auth_error
372
+ try:
373
+ dataset = target_manager.model_target_datasets[dataset_uuid]
374
+ except KeyError:
375
+ return _error_response(
376
+ status_code=HTTPStatus.NOT_FOUND,
377
+ code="NOT_FOUND",
378
+ message=(
379
+ "Could not find a model-view database with uuid "
380
+ f"{dataset_uuid}"
381
+ ),
382
+ target=_MOCK_USER_TARGET,
383
+ details=None,
384
+ )
385
+ return _json_response(
386
+ status_code=HTTPStatus.OK,
387
+ body=dataset.status_body(),
388
+ )
389
+
390
+
391
+ @beartype
392
+ def _dataset_zip_bytes(dataset: ModelTargetDataset) -> bytes:
393
+ """Return a small valid zip file for a generated dataset."""
394
+ zip_buffer = io.BytesIO()
395
+ with zipfile.ZipFile(file=zip_buffer, mode="w") as zip_file:
396
+ dataset_file = zipfile.ZipInfo(
397
+ filename="dataset.json",
398
+ date_time=_ZIP_EPOCH,
399
+ )
400
+ zip_file.writestr(
401
+ zinfo_or_arcname=dataset_file,
402
+ data=json.dumps(
403
+ obj={
404
+ "uuid": dataset.uuid_,
405
+ "type": dataset.dataset_type.value,
406
+ "request": dataset.request_body,
407
+ },
408
+ separators=(",", ":"),
409
+ sort_keys=True,
410
+ ),
411
+ )
412
+ return zip_buffer.getvalue()
413
+
414
+
415
+ @beartype
416
+ def download_model_target_dataset(
417
+ *,
418
+ request: RequestData,
419
+ target_manager: TargetManager,
420
+ dataset_uuid: str,
421
+ ) -> _ResponseType:
422
+ """Download a generated Model Target dataset."""
423
+ auth_error = _require_bearer_token(request=request)
424
+ if auth_error is not None:
425
+ return auth_error
426
+ try:
427
+ dataset = target_manager.model_target_datasets[dataset_uuid]
428
+ except KeyError:
429
+ return _error_response(
430
+ status_code=HTTPStatus.NOT_FOUND,
431
+ code="NOT_FOUND",
432
+ message=(
433
+ "Could not find a model-view database with uuid "
434
+ f"{dataset_uuid}"
435
+ ),
436
+ target=_MOCK_USER_TARGET,
437
+ details=None,
438
+ )
439
+ if dataset.status != "done":
440
+ return _error_response(
441
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
442
+ code="UNSUPPORTED_STATE",
443
+ message=(
444
+ f"Training status for dataset {dataset_uuid} is "
445
+ "not-started != done"
446
+ ),
447
+ target=dataset_uuid,
448
+ details=None,
449
+ )
450
+
451
+ body = _dataset_zip_bytes(dataset=dataset)
452
+ return (
453
+ HTTPStatus.OK,
454
+ {
455
+ "Content-Length": str(object=len(body)),
456
+ "Content-Type": "application/zip",
457
+ },
458
+ body,
459
+ )
460
+
461
+
462
+ @beartype
463
+ def delete_model_target_dataset(
464
+ *,
465
+ request: RequestData,
466
+ target_manager: TargetManager,
467
+ dataset_uuid: str,
468
+ ) -> _ResponseType:
469
+ """Delete a Model Target dataset."""
470
+ auth_error = _require_bearer_token(request=request)
471
+ if auth_error is not None:
472
+ return auth_error
473
+ try:
474
+ target_manager.remove_model_target_dataset(dataset_uuid=dataset_uuid)
475
+ except KeyError:
476
+ return _error_response(
477
+ status_code=HTTPStatus.NOT_FOUND,
478
+ code="NOT_FOUND",
479
+ message=(
480
+ "Could not find a model-view database with uuid "
481
+ f"{dataset_uuid}"
482
+ ),
483
+ target=_MOCK_USER_TARGET,
484
+ details=None,
485
+ )
486
+ return HTTPStatus.OK, {"Content-Length": "0"}, ""
@@ -0,0 +1,136 @@
1
+ """Tools for making Vuforia queries."""
2
+
3
+ import base64
4
+ import io
5
+ import uuid
6
+ from collections.abc import Iterable, Mapping
7
+ from email.message import EmailMessage
8
+ from typing import Any
9
+
10
+ from beartype import beartype
11
+ from werkzeug.formparser import MultiPartParser
12
+
13
+ from mock_vws._base64_decoding import decode_base64
14
+ from mock_vws._constants import ResultCodes, TargetStatuses
15
+ from mock_vws._database_matchers import get_database_matching_client_keys
16
+ from mock_vws._mock_common import json_dump
17
+ from mock_vws.database import CloudDatabase
18
+ from mock_vws.image_matchers import ImageMatcher
19
+
20
+
21
+ @beartype
22
+ def get_query_match_response_text(
23
+ *,
24
+ request_headers: Mapping[str, str],
25
+ request_body: bytes,
26
+ request_method: str,
27
+ request_path: str,
28
+ databases: Iterable[CloudDatabase],
29
+ query_match_checker: ImageMatcher,
30
+ ) -> str:
31
+ """
32
+ Args:
33
+ request_path: The path of the request.
34
+ request_headers: The headers sent with the request.
35
+ request_body: The body of the request.
36
+ request_method: The HTTP method of the request.
37
+ databases: All Vuforia databases.
38
+ query_match_checker: A callable which takes two image values and
39
+ returns whether they match.
40
+
41
+ Returns:
42
+ The response text for a query endpoint request.
43
+ """
44
+ email_message = EmailMessage()
45
+ email_message["Content-Type"] = request_headers["Content-Type"]
46
+ boundary = email_message.get_boundary(failobj="")
47
+
48
+ parser = MultiPartParser()
49
+ fields, files = parser.parse(
50
+ stream=io.BytesIO(initial_bytes=request_body),
51
+ boundary=boundary.encode(encoding="utf-8"),
52
+ content_length=len(request_body),
53
+ )
54
+
55
+ max_num_results = fields.get(key="max_num_results", default="1")
56
+ include_target_data = fields.get(
57
+ key="include_target_data",
58
+ default="top",
59
+ ).lower()
60
+
61
+ image_part = files["image"]
62
+ image_value = image_part.stream.read()
63
+
64
+ database = get_database_matching_client_keys(
65
+ request_headers=request_headers,
66
+ request_body=request_body,
67
+ request_method=request_method,
68
+ request_path=request_path,
69
+ databases=databases,
70
+ )
71
+
72
+ matching_targets = [
73
+ target
74
+ for target in database.targets
75
+ if query_match_checker(
76
+ first_image_content=target.image_value,
77
+ second_image_content=image_value,
78
+ )
79
+ ]
80
+
81
+ not_deleted_matches = [
82
+ target
83
+ for target in matching_targets
84
+ if target.active_flag
85
+ # In the real Vuforia, targets which have just
86
+ # been deleted may still get recognized.
87
+ # We document this difference in ``differences-to-vws.rst``.
88
+ and not target.delete_date
89
+ and target.status == TargetStatuses.SUCCESS.value
90
+ ]
91
+
92
+ all_quality_matches = not_deleted_matches
93
+ minimum_rating = 0
94
+ matches = [
95
+ match
96
+ for match in all_quality_matches
97
+ if match.tracking_rating > minimum_rating
98
+ ]
99
+
100
+ results: list[dict[str, Any]] = []
101
+ for target in matches:
102
+ target_timestamp = target.last_modified_date.timestamp()
103
+ if target.application_metadata is None:
104
+ application_metadata = None
105
+ else:
106
+ application_metadata = base64.b64encode(
107
+ s=decode_base64(encoded_data=target.application_metadata),
108
+ ).decode(encoding="ascii")
109
+ target_data = {
110
+ "target_timestamp": int(target_timestamp),
111
+ "name": target.name,
112
+ "application_metadata": application_metadata,
113
+ }
114
+
115
+ if include_target_data == "all" or (
116
+ include_target_data == "top" and not results
117
+ ):
118
+ result = {
119
+ "target_id": target.target_id,
120
+ "target_data": target_data,
121
+ }
122
+ else:
123
+ result = {
124
+ "target_id": target.target_id,
125
+ }
126
+
127
+ results.append(result)
128
+
129
+ results = results[: int(max_num_results)]
130
+ body = {
131
+ "result_code": ResultCodes.SUCCESS.value,
132
+ "results": results,
133
+ "query_id": uuid.uuid4().hex,
134
+ }
135
+
136
+ return json_dump(body=body)