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