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,954 @@
1
+ """A fake implementation of the Vuforia Web Services API.
2
+
3
+ See
4
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api
5
+ """
6
+
7
+ import base64
8
+ import copy
9
+ import datetime
10
+ import email.utils
11
+ import json
12
+ import uuid
13
+ from collections.abc import Callable, Iterable, Mapping
14
+ from http import HTTPMethod, HTTPStatus
15
+ from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, runtime_checkable
16
+ from zoneinfo import ZoneInfo
17
+
18
+ from beartype import BeartypeConf, beartype
19
+
20
+ from mock_vws._constants import (
21
+ VUMARK_PDF,
22
+ VUMARK_PNG,
23
+ VUMARK_SVG,
24
+ ResultCodes,
25
+ TargetStatuses,
26
+ )
27
+ from mock_vws._database_matchers import get_database_matching_server_keys
28
+ from mock_vws._mock_common import RequestData, Route, json_dump
29
+ from mock_vws._model_target_web_api import (
30
+ create_model_target_dataset,
31
+ delete_model_target_dataset,
32
+ download_model_target_dataset,
33
+ get_model_target_dataset_status,
34
+ oauth2_token,
35
+ )
36
+ from mock_vws._services_validators import run_services_validators
37
+ from mock_vws._services_validators.exceptions import (
38
+ FailError,
39
+ InvalidAcceptHeaderError,
40
+ InvalidInstanceIdError,
41
+ InvalidTargetTypeError,
42
+ TargetStatusNotSuccessError,
43
+ TargetStatusProcessingError,
44
+ ValidatorError,
45
+ )
46
+ from mock_vws.database import VuMarkDatabase
47
+ from mock_vws.image_matchers import ImageMatcher
48
+ from mock_vws.model_target import ModelTargetDatasetType
49
+ from mock_vws.target import ImageTarget
50
+ from mock_vws.target_manager import TargetManager
51
+ from mock_vws.target_raters import TargetTrackingRater
52
+
53
+ if TYPE_CHECKING:
54
+ from mock_vws.database import CloudDatabase
55
+
56
+ _TARGET_ID_PATTERN = "[A-Za-z0-9]+"
57
+ _MODEL_TARGET_DATASET_UUID_PATTERN = "[A-Za-z0-9-]+"
58
+
59
+
60
+ _ROUTES: set[Route] = set()
61
+
62
+ _ResponseType = tuple[int, Mapping[str, str], str | bytes]
63
+ _P = ParamSpec("_P")
64
+
65
+
66
+ @runtime_checkable
67
+ class _RouteMethod(Protocol[_P]):
68
+ """Callable used for routing which also exposes ``__name__``."""
69
+
70
+ __name__: str
71
+
72
+ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _ResponseType:
73
+ """Return a mock response."""
74
+ ... # pylint: disable=unnecessary-ellipsis
75
+
76
+
77
+ @beartype
78
+ def route(
79
+ *,
80
+ path_pattern: str,
81
+ http_methods: Iterable[HTTPMethod],
82
+ ) -> Callable[[_RouteMethod[_P]], _RouteMethod[_P]]:
83
+ """Register a decorated method so that it can be recognized as a route.
84
+
85
+ Args:
86
+ path_pattern: The end part of a URL pattern. E.g. `/targets` or
87
+ `/targets/.+`.
88
+ http_methods: HTTP methods that map to the route function.
89
+
90
+ Returns:
91
+ A decorator which takes methods and makes them recognizable as routes.
92
+ """
93
+
94
+ @beartype
95
+ def decorator(
96
+ method: _RouteMethod[_P],
97
+ ) -> _RouteMethod[_P]:
98
+ """Register a decorated method so that it can be recognized as a
99
+ route.
100
+
101
+ Returns:
102
+ The given `method` with multiple changes, including added
103
+ validators.
104
+ """
105
+ new_route = Route(
106
+ route_name=method.__name__,
107
+ path_pattern=path_pattern,
108
+ http_methods=frozenset(http_methods),
109
+ )
110
+ _ROUTES.add(new_route)
111
+
112
+ return method
113
+
114
+ return decorator
115
+
116
+
117
+ @beartype(conf=BeartypeConf(is_pep484_tower=True))
118
+ class MockVuforiaWebServicesAPI:
119
+ """A fake implementation of the Vuforia Web Services API."""
120
+
121
+ def __init__(
122
+ self,
123
+ *,
124
+ target_manager: TargetManager,
125
+ processing_time_seconds: float,
126
+ duplicate_match_checker: ImageMatcher,
127
+ target_tracking_rater: TargetTrackingRater,
128
+ ) -> None:
129
+ """
130
+ Args:
131
+ target_manager: Target Manager which stores databases.
132
+ processing_time_seconds: The number of seconds to process each
133
+ image for. In the real Vuforia Web Services, this is not
134
+ deterministic.
135
+ duplicate_match_checker: A callable which takes two image
136
+ values
137
+ and returns whether they are duplicates.
138
+ target_tracking_rater: A callable for rating targets for
139
+ tracking.
140
+
141
+ Attributes:
142
+ routes: The `Route`s to be used in the mock.
143
+ """
144
+ self._target_manager = target_manager
145
+ self.routes = _ROUTES
146
+ self._processing_time_seconds = processing_time_seconds
147
+ self._duplicate_match_checker = duplicate_match_checker
148
+ self._target_tracking_rater = target_tracking_rater
149
+
150
+ @route(path_pattern="/oauth2/token", http_methods={HTTPMethod.POST})
151
+ def oauth2_token( # pylint: disable=no-self-use
152
+ self,
153
+ request: RequestData,
154
+ ) -> _ResponseType:
155
+ """Obtain an OAuth2 token for the Model Target Web API."""
156
+ return oauth2_token(request=request)
157
+
158
+ @route(
159
+ path_pattern="/modeltargets/datasets",
160
+ http_methods={HTTPMethod.POST},
161
+ )
162
+ def create_standard_model_target_dataset(
163
+ self,
164
+ request: RequestData,
165
+ ) -> _ResponseType:
166
+ """Create a standard Model Target dataset."""
167
+ return create_model_target_dataset(
168
+ request=request,
169
+ target_manager=self._target_manager,
170
+ processing_time_seconds=self._processing_time_seconds,
171
+ dataset_type=ModelTargetDatasetType.STANDARD,
172
+ )
173
+
174
+ @route(
175
+ path_pattern="/modeltargets/advancedDatasets",
176
+ http_methods={HTTPMethod.POST},
177
+ )
178
+ def create_advanced_model_target_dataset(
179
+ self,
180
+ request: RequestData,
181
+ ) -> _ResponseType:
182
+ """Create an advanced Model Target dataset."""
183
+ return create_model_target_dataset(
184
+ request=request,
185
+ target_manager=self._target_manager,
186
+ processing_time_seconds=self._processing_time_seconds,
187
+ dataset_type=ModelTargetDatasetType.ADVANCED,
188
+ )
189
+
190
+ @route(
191
+ path_pattern=(
192
+ "/modeltargets/datasets/"
193
+ f"{_MODEL_TARGET_DATASET_UUID_PATTERN}/status"
194
+ ),
195
+ http_methods={HTTPMethod.GET},
196
+ )
197
+ def get_standard_model_target_dataset_status(
198
+ self,
199
+ request: RequestData,
200
+ ) -> _ResponseType:
201
+ """Return a standard Model Target dataset creation status."""
202
+ dataset_uuid = request.path.split(sep="/")[-2]
203
+ return get_model_target_dataset_status(
204
+ request=request,
205
+ target_manager=self._target_manager,
206
+ dataset_uuid=dataset_uuid,
207
+ )
208
+
209
+ @route(
210
+ path_pattern=(
211
+ "/modeltargets/advancedDatasets/"
212
+ f"{_MODEL_TARGET_DATASET_UUID_PATTERN}/status"
213
+ ),
214
+ http_methods={HTTPMethod.GET},
215
+ )
216
+ def get_advanced_model_target_dataset_status(
217
+ self,
218
+ request: RequestData,
219
+ ) -> _ResponseType:
220
+ """Return an advanced Model Target dataset creation status."""
221
+ dataset_uuid = request.path.split(sep="/")[-2]
222
+ return get_model_target_dataset_status(
223
+ request=request,
224
+ target_manager=self._target_manager,
225
+ dataset_uuid=dataset_uuid,
226
+ )
227
+
228
+ @route(
229
+ path_pattern=(
230
+ "/modeltargets/datasets/"
231
+ f"{_MODEL_TARGET_DATASET_UUID_PATTERN}/dataset"
232
+ ),
233
+ http_methods={HTTPMethod.GET},
234
+ )
235
+ def download_standard_model_target_dataset(
236
+ self,
237
+ request: RequestData,
238
+ ) -> _ResponseType:
239
+ """Download a standard Model Target dataset."""
240
+ dataset_uuid = request.path.split(sep="/")[-2]
241
+ return download_model_target_dataset(
242
+ request=request,
243
+ target_manager=self._target_manager,
244
+ dataset_uuid=dataset_uuid,
245
+ )
246
+
247
+ @route(
248
+ path_pattern=(
249
+ "/modeltargets/advancedDatasets/"
250
+ f"{_MODEL_TARGET_DATASET_UUID_PATTERN}/dataset"
251
+ ),
252
+ http_methods={HTTPMethod.GET},
253
+ )
254
+ def download_advanced_model_target_dataset(
255
+ self,
256
+ request: RequestData,
257
+ ) -> _ResponseType:
258
+ """Download an advanced Model Target dataset."""
259
+ dataset_uuid = request.path.split(sep="/")[-2]
260
+ return download_model_target_dataset(
261
+ request=request,
262
+ target_manager=self._target_manager,
263
+ dataset_uuid=dataset_uuid,
264
+ )
265
+
266
+ @route(
267
+ path_pattern=(
268
+ f"/modeltargets/datasets/{_MODEL_TARGET_DATASET_UUID_PATTERN}"
269
+ ),
270
+ http_methods={HTTPMethod.DELETE},
271
+ )
272
+ def delete_standard_model_target_dataset(
273
+ self,
274
+ request: RequestData,
275
+ ) -> _ResponseType:
276
+ """Delete a standard Model Target dataset."""
277
+ dataset_uuid = request.path.split(sep="/")[-1]
278
+ return delete_model_target_dataset(
279
+ request=request,
280
+ target_manager=self._target_manager,
281
+ dataset_uuid=dataset_uuid,
282
+ )
283
+
284
+ @route(
285
+ path_pattern=(
286
+ "/modeltargets/advancedDatasets/"
287
+ f"{_MODEL_TARGET_DATASET_UUID_PATTERN}"
288
+ ),
289
+ http_methods={HTTPMethod.DELETE},
290
+ )
291
+ def delete_advanced_model_target_dataset(
292
+ self,
293
+ request: RequestData,
294
+ ) -> _ResponseType:
295
+ """Delete an advanced Model Target dataset."""
296
+ dataset_uuid = request.path.split(sep="/")[-1]
297
+ return delete_model_target_dataset(
298
+ request=request,
299
+ target_manager=self._target_manager,
300
+ dataset_uuid=dataset_uuid,
301
+ )
302
+
303
+ @route(
304
+ path_pattern="/targets",
305
+ http_methods={HTTPMethod.POST},
306
+ )
307
+ def add_target(self, request: RequestData) -> _ResponseType:
308
+ """Add a target.
309
+
310
+ Fake implementation of
311
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#add
312
+ """
313
+ try:
314
+ run_services_validators(
315
+ request_headers=request.headers,
316
+ request_body=request.body,
317
+ request_method=request.method,
318
+ request_path=request.path,
319
+ databases=self._target_manager.cloud_databases,
320
+ )
321
+ except ValidatorError as exc:
322
+ return exc.status_code, exc.headers, exc.response_text
323
+
324
+ database = get_database_matching_server_keys(
325
+ request_headers=request.headers,
326
+ request_body=request.body,
327
+ request_method=request.method,
328
+ request_path=request.path,
329
+ databases=self._target_manager.cloud_databases,
330
+ )
331
+
332
+ request_json: dict[str, Any] = json.loads(s=request.body)
333
+ given_active_flag = request_json.get("active_flag")
334
+ active_flag = {
335
+ None: True,
336
+ True: True,
337
+ False: False,
338
+ }[given_active_flag]
339
+
340
+ application_metadata = request_json.get("application_metadata")
341
+
342
+ new_target = ImageTarget(
343
+ name=request_json["name"],
344
+ width=request_json["width"],
345
+ image_value=base64.b64decode(s=request_json["image"]),
346
+ active_flag=active_flag,
347
+ processing_time_seconds=self._processing_time_seconds,
348
+ application_metadata=application_metadata,
349
+ target_tracking_rater=self._target_tracking_rater,
350
+ )
351
+ database.targets.add(new_target)
352
+
353
+ date = email.utils.formatdate(
354
+ timeval=None,
355
+ localtime=False,
356
+ usegmt=True,
357
+ )
358
+ status_code = HTTPStatus.CREATED
359
+ body = {
360
+ "transaction_id": uuid.uuid4().hex,
361
+ "result_code": ResultCodes.TARGET_CREATED.value,
362
+ "target_id": new_target.target_id,
363
+ }
364
+ body_json = json_dump(body=body)
365
+ headers = {
366
+ "Connection": "keep-alive",
367
+ "Content-Type": "application/json",
368
+ "server": "envoy",
369
+ "Date": date,
370
+ "Content-Length": str(object=len(body_json)),
371
+ "x-envoy-upstream-service-time": "5",
372
+ "strict-transport-security": "max-age=31536000",
373
+ "x-aws-region": "us-east-2, us-west-2",
374
+ "x-content-type-options": "nosniff",
375
+ }
376
+ return status_code, headers, body_json
377
+
378
+ @route(
379
+ path_pattern=f"/targets/{_TARGET_ID_PATTERN}",
380
+ http_methods={HTTPMethod.DELETE},
381
+ )
382
+ def delete_target(self, request: RequestData) -> _ResponseType:
383
+ """Delete a target.
384
+
385
+ Fake implementation of
386
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#delete
387
+ """
388
+ try:
389
+ run_services_validators(
390
+ request_headers=request.headers,
391
+ request_body=request.body,
392
+ request_method=request.method,
393
+ request_path=request.path,
394
+ databases=self._target_manager.cloud_databases,
395
+ )
396
+ except ValidatorError as exc:
397
+ return exc.status_code, exc.headers, exc.response_text
398
+
399
+ database = get_database_matching_server_keys(
400
+ request_headers=request.headers,
401
+ request_body=request.body,
402
+ request_method=request.method,
403
+ request_path=request.path,
404
+ databases=self._target_manager.cloud_databases,
405
+ )
406
+
407
+ target_id = request.path.split(sep="/")[-1]
408
+ target = database.get_target(target_id=target_id)
409
+
410
+ if target.status == TargetStatuses.PROCESSING.value:
411
+ target_processing_exception = TargetStatusProcessingError()
412
+ return (
413
+ target_processing_exception.status_code,
414
+ target_processing_exception.headers,
415
+ target_processing_exception.response_text,
416
+ )
417
+
418
+ now = datetime.datetime.now(tz=target.upload_date.tzinfo)
419
+ # See https://github.com/facebook/pyrefly/issues/1897
420
+ new_target: ImageTarget = copy.replace(
421
+ target, # pyrefly: ignore[bad-argument-type]
422
+ delete_date=now,
423
+ )
424
+ database.targets.remove(target)
425
+ database.targets.add(new_target)
426
+ date = email.utils.formatdate(
427
+ timeval=None,
428
+ localtime=False,
429
+ usegmt=True,
430
+ )
431
+
432
+ body = {
433
+ "transaction_id": uuid.uuid4().hex,
434
+ "result_code": ResultCodes.SUCCESS.value,
435
+ }
436
+ body_json = json_dump(body=body)
437
+ headers = {
438
+ "Connection": "keep-alive",
439
+ "Content-Length": str(object=len(body_json)),
440
+ "Content-Type": "application/json",
441
+ "Date": date,
442
+ "server": "envoy",
443
+ "x-envoy-upstream-service-time": "5",
444
+ "strict-transport-security": "max-age=31536000",
445
+ "x-aws-region": "us-east-2, us-west-2",
446
+ "x-content-type-options": "nosniff",
447
+ }
448
+ return HTTPStatus.OK, headers, body_json
449
+
450
+ @route(
451
+ path_pattern=f"/targets/{_TARGET_ID_PATTERN}/instances",
452
+ http_methods={HTTPMethod.POST},
453
+ )
454
+ def generate_vumark_instance(self, request: RequestData) -> _ResponseType:
455
+ """Generate a VuMark instance."""
456
+ valid_accept_types: dict[str, bytes] = {
457
+ "image/png": VUMARK_PNG,
458
+ "image/svg+xml": VUMARK_SVG,
459
+ "application/pdf": VUMARK_PDF,
460
+ }
461
+ try:
462
+ all_databases: list[CloudDatabase | VuMarkDatabase] = [
463
+ *self._target_manager.cloud_databases,
464
+ *self._target_manager.vumark_databases,
465
+ ]
466
+ run_services_validators(
467
+ request_headers=request.headers,
468
+ request_body=request.body,
469
+ request_method=request.method,
470
+ request_path=request.path,
471
+ databases=all_databases,
472
+ )
473
+
474
+ database = get_database_matching_server_keys(
475
+ request_headers=request.headers,
476
+ request_body=request.body,
477
+ request_method=request.method,
478
+ request_path=request.path,
479
+ databases=all_databases,
480
+ )
481
+ if not isinstance(database, VuMarkDatabase):
482
+ raise InvalidTargetTypeError
483
+
484
+ target_id = request.path.split(sep="/")[-2]
485
+ target = database.get_vumark_target(target_id=target_id)
486
+ if target.status != TargetStatuses.SUCCESS.value:
487
+ raise TargetStatusNotSuccessError
488
+
489
+ accept = dict(request.headers).get("Accept", "")
490
+ if accept not in valid_accept_types:
491
+ raise InvalidAcceptHeaderError
492
+
493
+ request_json = json.loads(s=request.body)
494
+ instance_id = request_json.get("instance_id", "")
495
+ if not instance_id:
496
+ raise InvalidInstanceIdError
497
+ except ValidatorError as exc:
498
+ return exc.status_code, exc.headers, exc.response_text
499
+
500
+ response_body = valid_accept_types[accept]
501
+ content_type = accept
502
+ date = email.utils.formatdate(
503
+ timeval=None,
504
+ localtime=False,
505
+ usegmt=True,
506
+ )
507
+ headers = {
508
+ "Connection": "keep-alive",
509
+ "Content-Type": content_type,
510
+ "Date": date,
511
+ "server": "envoy",
512
+ "x-envoy-upstream-service-time": "5",
513
+ "strict-transport-security": "max-age=31536000",
514
+ "x-aws-region": "us-east-2, us-west-2",
515
+ "x-content-type-options": "nosniff",
516
+ }
517
+ return HTTPStatus.OK, headers, response_body
518
+
519
+ @route(path_pattern="/summary", http_methods={HTTPMethod.GET})
520
+ def database_summary(self, request: RequestData) -> _ResponseType:
521
+ """Get a database summary report.
522
+
523
+ Fake implementation of
524
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#summary-report
525
+ """
526
+ try:
527
+ run_services_validators(
528
+ request_headers=request.headers,
529
+ request_body=request.body,
530
+ request_method=request.method,
531
+ request_path=request.path,
532
+ databases=self._target_manager.cloud_databases,
533
+ )
534
+ except ValidatorError as exc:
535
+ return exc.status_code, exc.headers, exc.response_text
536
+
537
+ database = get_database_matching_server_keys(
538
+ request_headers=request.headers,
539
+ request_body=request.body,
540
+ request_method=request.method,
541
+ request_path=request.path,
542
+ databases=self._target_manager.cloud_databases,
543
+ )
544
+
545
+ date = email.utils.formatdate(
546
+ timeval=None,
547
+ localtime=False,
548
+ usegmt=True,
549
+ )
550
+ body = {
551
+ "result_code": ResultCodes.SUCCESS.value,
552
+ "transaction_id": uuid.uuid4().hex,
553
+ "name": database.database_name,
554
+ "active_images": len(database.active_targets),
555
+ "inactive_images": len(database.inactive_targets),
556
+ "failed_images": len(database.failed_targets),
557
+ "target_quota": database.target_quota,
558
+ "total_recos": database.total_recos,
559
+ "current_month_recos": database.current_month_recos,
560
+ "previous_month_recos": database.previous_month_recos,
561
+ "processing_images": len(database.processing_targets),
562
+ "reco_threshold": database.reco_threshold,
563
+ "request_quota": database.request_quota,
564
+ "request_usage": 0,
565
+ }
566
+ body_json = json_dump(body=body)
567
+ headers = {
568
+ "Connection": "keep-alive",
569
+ "Content-Length": str(object=len(body_json)),
570
+ "Content-Type": "application/json",
571
+ "Date": date,
572
+ "server": "envoy",
573
+ "x-envoy-upstream-service-time": "5",
574
+ "strict-transport-security": "max-age=31536000",
575
+ "x-aws-region": "us-east-2, us-west-2",
576
+ "x-content-type-options": "nosniff",
577
+ }
578
+ return HTTPStatus.OK, headers, body_json
579
+
580
+ @route(path_pattern="/targets", http_methods={HTTPMethod.GET})
581
+ def target_list(self, request: RequestData) -> _ResponseType:
582
+ """Get a list of all targets.
583
+
584
+ Fake implementation of
585
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#details-list
586
+ """
587
+ try:
588
+ run_services_validators(
589
+ request_headers=request.headers,
590
+ request_body=request.body,
591
+ request_method=request.method,
592
+ request_path=request.path,
593
+ databases=self._target_manager.cloud_databases,
594
+ )
595
+ except ValidatorError as exc:
596
+ return exc.status_code, exc.headers, exc.response_text
597
+
598
+ database = get_database_matching_server_keys(
599
+ request_headers=request.headers,
600
+ request_body=request.body,
601
+ request_method=request.method,
602
+ request_path=request.path,
603
+ databases=self._target_manager.cloud_databases,
604
+ )
605
+
606
+ date = email.utils.formatdate(
607
+ timeval=None,
608
+ localtime=False,
609
+ usegmt=True,
610
+ )
611
+
612
+ response_results = [
613
+ target.target_id for target in database.not_deleted_targets
614
+ ]
615
+ body = {
616
+ "transaction_id": uuid.uuid4().hex,
617
+ "result_code": ResultCodes.SUCCESS.value,
618
+ "results": response_results,
619
+ }
620
+ body_json = json_dump(body=body)
621
+ headers = {
622
+ "Connection": "keep-alive",
623
+ "Content-Length": str(object=len(body_json)),
624
+ "Content-Type": "application/json",
625
+ "Date": date,
626
+ "server": "envoy",
627
+ "x-envoy-upstream-service-time": "5",
628
+ "strict-transport-security": "max-age=31536000",
629
+ "x-aws-region": "us-east-2, us-west-2",
630
+ "x-content-type-options": "nosniff",
631
+ }
632
+ return HTTPStatus.OK, headers, body_json
633
+
634
+ @route(
635
+ path_pattern=f"/targets/{_TARGET_ID_PATTERN}",
636
+ http_methods={HTTPMethod.GET},
637
+ )
638
+ def get_target(self, request: RequestData) -> _ResponseType:
639
+ """Get details of a target.
640
+
641
+ Fake implementation of
642
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#target-record
643
+ """
644
+ try:
645
+ run_services_validators(
646
+ request_headers=request.headers,
647
+ request_body=request.body,
648
+ request_method=request.method,
649
+ request_path=request.path,
650
+ databases=self._target_manager.cloud_databases,
651
+ )
652
+ except ValidatorError as exc:
653
+ return exc.status_code, exc.headers, exc.response_text
654
+
655
+ database = get_database_matching_server_keys(
656
+ request_headers=request.headers,
657
+ request_body=request.body,
658
+ request_method=request.method,
659
+ request_path=request.path,
660
+ databases=self._target_manager.cloud_databases,
661
+ )
662
+ target_id = request.path.split(sep="/")[-1]
663
+ target = database.get_target(target_id=target_id)
664
+
665
+ width = target.width
666
+ tracking_rating = target.tracking_rating
667
+ reco_rating = target.reco_rating
668
+ target_record = {
669
+ "target_id": target.target_id,
670
+ "active_flag": target.active_flag,
671
+ "name": target.name,
672
+ "width": width,
673
+ "tracking_rating": tracking_rating,
674
+ "reco_rating": reco_rating,
675
+ }
676
+ date = email.utils.formatdate(
677
+ timeval=None,
678
+ localtime=False,
679
+ usegmt=True,
680
+ )
681
+
682
+ body = {
683
+ "result_code": ResultCodes.SUCCESS.value,
684
+ "transaction_id": uuid.uuid4().hex,
685
+ "target_record": target_record,
686
+ "status": target.status,
687
+ }
688
+ body_json = json_dump(body=body)
689
+ headers = {
690
+ "Connection": "keep-alive",
691
+ "Content-Length": str(object=len(body_json)),
692
+ "Content-Type": "application/json",
693
+ "Date": date,
694
+ "server": "envoy",
695
+ "x-envoy-upstream-service-time": "5",
696
+ "strict-transport-security": "max-age=31536000",
697
+ "x-aws-region": "us-east-2, us-west-2",
698
+ "x-content-type-options": "nosniff",
699
+ }
700
+ return HTTPStatus.OK, headers, body_json
701
+
702
+ @route(
703
+ path_pattern=f"/duplicates/{_TARGET_ID_PATTERN}",
704
+ http_methods={HTTPMethod.GET},
705
+ )
706
+ def get_duplicates(self, request: RequestData) -> _ResponseType:
707
+ """Get targets which may be considered duplicates of a given
708
+ target.
709
+
710
+ Fake implementation of
711
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#check
712
+ """
713
+ try:
714
+ run_services_validators(
715
+ request_headers=request.headers,
716
+ request_body=request.body,
717
+ request_method=request.method,
718
+ request_path=request.path,
719
+ databases=self._target_manager.cloud_databases,
720
+ )
721
+ except ValidatorError as exc:
722
+ return exc.status_code, exc.headers, exc.response_text
723
+
724
+ database = get_database_matching_server_keys(
725
+ request_headers=request.headers,
726
+ request_body=request.body,
727
+ request_method=request.method,
728
+ request_path=request.path,
729
+ databases=self._target_manager.cloud_databases,
730
+ )
731
+ target_id = request.path.split(sep="/")[-1]
732
+ target = database.get_target(target_id=target_id)
733
+
734
+ other_targets = database.targets - {target}
735
+
736
+ similar_targets = [
737
+ other.target_id
738
+ for other in other_targets
739
+ if self._duplicate_match_checker(
740
+ first_image_content=target.image_value,
741
+ second_image_content=other.image_value,
742
+ )
743
+ and TargetStatuses.FAILED.value
744
+ not in {target.status, other.status}
745
+ and TargetStatuses.PROCESSING.value != other.status
746
+ and other.active_flag
747
+ ]
748
+
749
+ date = email.utils.formatdate(
750
+ timeval=None,
751
+ localtime=False,
752
+ usegmt=True,
753
+ )
754
+ body = {
755
+ "transaction_id": uuid.uuid4().hex,
756
+ "result_code": ResultCodes.SUCCESS.value,
757
+ "similar_targets": similar_targets,
758
+ }
759
+ body_json = json_dump(body=body)
760
+ headers = {
761
+ "Connection": "keep-alive",
762
+ "Content-Length": str(object=len(body_json)),
763
+ "Content-Type": "application/json",
764
+ "Date": date,
765
+ "server": "envoy",
766
+ "x-envoy-upstream-service-time": "5",
767
+ "strict-transport-security": "max-age=31536000",
768
+ "x-aws-region": "us-east-2, us-west-2",
769
+ "x-content-type-options": "nosniff",
770
+ }
771
+
772
+ return HTTPStatus.OK, headers, body_json
773
+
774
+ @route(
775
+ path_pattern=f"/targets/{_TARGET_ID_PATTERN}",
776
+ http_methods={HTTPMethod.PUT},
777
+ )
778
+ def update_target(self, request: RequestData) -> _ResponseType:
779
+ """Update a target.
780
+
781
+ Fake implementation of
782
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#update
783
+ """
784
+ try:
785
+ run_services_validators(
786
+ request_headers=request.headers,
787
+ request_body=request.body,
788
+ request_method=request.method,
789
+ request_path=request.path,
790
+ databases=self._target_manager.cloud_databases,
791
+ )
792
+ except ValidatorError as exc:
793
+ return exc.status_code, exc.headers, exc.response_text
794
+
795
+ database = get_database_matching_server_keys(
796
+ request_headers=request.headers,
797
+ request_body=request.body,
798
+ request_method=request.method,
799
+ request_path=request.path,
800
+ databases=self._target_manager.cloud_databases,
801
+ )
802
+
803
+ target_id = request.path.split(sep="/")[-1]
804
+ target = database.get_target(target_id=target_id)
805
+
806
+ date = email.utils.formatdate(
807
+ timeval=None,
808
+ localtime=False,
809
+ usegmt=True,
810
+ )
811
+
812
+ if target.status != TargetStatuses.SUCCESS.value:
813
+ exception = TargetStatusNotSuccessError()
814
+ return (
815
+ exception.status_code,
816
+ exception.headers,
817
+ exception.response_text,
818
+ )
819
+
820
+ request_json: dict[str, Any] = json.loads(s=request.body)
821
+ name = request_json.get("name", target.name)
822
+ active_flag = request_json.get("active_flag", target.active_flag)
823
+
824
+ if "active_flag" in request_json and active_flag is None:
825
+ fail_exception = FailError(status_code=HTTPStatus.BAD_REQUEST)
826
+ return (
827
+ fail_exception.status_code,
828
+ fail_exception.headers,
829
+ fail_exception.response_text,
830
+ )
831
+
832
+ gmt = ZoneInfo(key="GMT")
833
+ last_modified_date = datetime.datetime.now(tz=gmt)
834
+
835
+ width = request_json.get("width", target.width)
836
+ application_metadata = request_json.get(
837
+ "application_metadata",
838
+ target.application_metadata,
839
+ )
840
+
841
+ image_value = target.image_value
842
+ if "image" in request_json:
843
+ image_value = base64.b64decode(s=request_json["image"])
844
+
845
+ if (
846
+ "application_metadata" in request_json
847
+ and application_metadata is None
848
+ ):
849
+ fail_exception = FailError(status_code=HTTPStatus.BAD_REQUEST)
850
+ return (
851
+ fail_exception.status_code,
852
+ fail_exception.headers,
853
+ fail_exception.response_text,
854
+ )
855
+
856
+ # See https://github.com/facebook/pyrefly/issues/1897
857
+ new_target: ImageTarget = copy.replace(
858
+ target, # pyrefly: ignore[bad-argument-type]
859
+ name=name,
860
+ width=width,
861
+ active_flag=active_flag,
862
+ application_metadata=application_metadata,
863
+ image_value=image_value,
864
+ last_modified_date=last_modified_date,
865
+ )
866
+
867
+ database.targets.remove(target)
868
+ database.targets.add(new_target)
869
+
870
+ body = {
871
+ "result_code": ResultCodes.SUCCESS.value,
872
+ "transaction_id": uuid.uuid4().hex,
873
+ }
874
+ body_json = json_dump(body=body)
875
+ headers = {
876
+ "Connection": "keep-alive",
877
+ "Content-Type": "application/json",
878
+ "server": "envoy",
879
+ "Date": date,
880
+ "Content-Length": str(object=len(body_json)),
881
+ "x-envoy-upstream-service-time": "5",
882
+ "strict-transport-security": "max-age=31536000",
883
+ "x-aws-region": "us-east-2, us-west-2",
884
+ "x-content-type-options": "nosniff",
885
+ }
886
+ return HTTPStatus.OK, headers, body_json
887
+
888
+ @route(
889
+ path_pattern=f"/summary/{_TARGET_ID_PATTERN}",
890
+ http_methods={HTTPMethod.GET},
891
+ )
892
+ def target_summary(self, request: RequestData) -> _ResponseType:
893
+ """Get a summary report for a target.
894
+
895
+ Fake implementation of
896
+ https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#retrieve-report
897
+ """
898
+ try:
899
+ run_services_validators(
900
+ request_headers=request.headers,
901
+ request_body=request.body,
902
+ request_method=request.method,
903
+ request_path=request.path,
904
+ databases=self._target_manager.cloud_databases,
905
+ )
906
+ except ValidatorError as exc:
907
+ return exc.status_code, exc.headers, exc.response_text
908
+
909
+ database = get_database_matching_server_keys(
910
+ request_headers=request.headers,
911
+ request_body=request.body,
912
+ request_method=request.method,
913
+ request_path=request.path,
914
+ databases=self._target_manager.cloud_databases,
915
+ )
916
+ target_id = request.path.split(sep="/")[-1]
917
+ target = database.get_target(target_id=target_id)
918
+
919
+ date = email.utils.formatdate(
920
+ timeval=None,
921
+ localtime=False,
922
+ usegmt=True,
923
+ )
924
+ tracking_rating = target.tracking_rating
925
+ total_recos = target.total_recos
926
+ current_month_recos = target.current_month_recos
927
+ previous_month_recos = target.previous_month_recos
928
+ body = {
929
+ "status": target.status,
930
+ "transaction_id": uuid.uuid4().hex,
931
+ "result_code": ResultCodes.SUCCESS.value,
932
+ "database_name": database.database_name,
933
+ "target_name": target.name,
934
+ "upload_date": target.upload_date.strftime(format="%Y-%m-%d"),
935
+ "active_flag": target.active_flag,
936
+ "tracking_rating": tracking_rating,
937
+ "total_recos": total_recos,
938
+ "current_month_recos": current_month_recos,
939
+ "previous_month_recos": previous_month_recos,
940
+ }
941
+ body_json = json_dump(body=body)
942
+ headers = {
943
+ "Connection": "keep-alive",
944
+ "Content-Length": str(object=len(body_json)),
945
+ "Content-Type": "application/json",
946
+ "Date": date,
947
+ "server": "envoy",
948
+ "x-envoy-upstream-service-time": "5",
949
+ "strict-transport-security": "max-age=31536000",
950
+ "x-aws-region": "us-east-2, us-west-2",
951
+ "x-content-type-options": "nosniff",
952
+ }
953
+
954
+ return HTTPStatus.OK, headers, body_json