Skip to content

Commit 2b6883b

Browse files
Create VuMarkTarget type for semantically correct VuMark fixture (#2968)
* Create VuMarkTarget type for semantically correct VuMark fixture Previously, _vumark_database used ImageTarget for VuMark targets, which was semantically incorrect since VuMark targets don't have image data. Now VuMarkTarget is a proper type that reflects the differences: no image_value, no processing_time_seconds, always SUCCESS status, and hardcoded tracking_rating. Changes: - Add VuMarkTarget and VuMarkTargetDict to target.py - Add vumark_targets field to CloudDatabase (separate from image targets) - Update not_deleted_targets to include both types for validator lookups - Keep active_targets, inactive_targets, etc. as image-only for clarity - Add POST /databases/{name}/vumark_targets Flask endpoint - Update _vumark_database fixture to use VuMarkTarget - Update _enable_use_docker_in_memory to use new endpoint - Document VuMarkTarget in API reference Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * Fix type issues with get_target returning image targets only The validator uses not_deleted_targets (which returns union type) so it can find both image and VuMark targets. But get_target() is only called for image targets in the Flask and requests mock servers, so return ImageTarget only. * Fix mypy type hint for not_deleted_targets union * Separate VuMark database from CloudDatabase and minimize VuMarkTarget Introduce VuMarkDatabase and VuMarkDatabaseDict as distinct types from CloudDatabase, since VuMark databases don't have client keys, state, or quota fields. Minimize VuMarkTarget/VuMarkTargetDict to only the fields actually used (target_id, name, delete_date). Update all validators, Flask endpoints, and request mock servers to handle the AnyDatabase union type. Split target manager endpoints by database type: /databases for cloud, /vumark_databases for VuMark. Add typed lookup helpers instead of isinstance guards in endpoint functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Sphinx docs reference to renamed endpoint The create_database endpoint was renamed to create_cloud_database but the docs/source/docker.rst autoflask directive was not updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Split TargetManager.databases into typed cloud_databases and vumark_databases This eliminates 22 isinstance checks by storing each database type separately and narrowing all downstream signatures to the specific type they need (CloudDatabase for VWS/VWQ APIs and validators). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert unnecessary changes to cloud-specific functions Restore cloud functions to match main exactly - only VuMark additions remain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert cosmetic changes to existing code Remove unnecessary docstring tweaks, comment rewraps, and refactors to CloudDatabase that are unrelated to VuMark support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix VuMark auth by widening validators to accept both database types VuMark database credentials were not found during VWS authentication because validators only searched cloud databases. Widen all service validator signatures to accept AnyDatabase (CloudDatabase | VuMarkDatabase), pass both database types from the Flask and requests-mock servers, and add VuMarkDatabase to the Sphinx API docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address bugbot review: scope validate_request to cloud-only databases - Skip validate_request for VuMark endpoint; it does its own validation with both database types (fixes 500 when VuMark creds hit cloud endpoints) - Fix misleading error message in add_cloud_database ("cloud database" → "database") - Deduplicate AnyDatabase alias: import from _database_matchers in target_manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update tests to match new database conflict error message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix coverage: remove dead VuMark branches, add duplicate key tests - Remove delete_date field from VuMarkTarget (VuMark targets can't be deleted) - Simplify VuMarkDatabase.not_deleted_targets (no filtering needed) - Remove NOT_FOUND branch from delete_vumark_database (internal API) - Add test_duplicate_vumark_keys to both Flask and requests-mock test suites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix VuMark duplicate keys test isolation in Flask tests Use distinct keys ("v1", "v2", "v3") for VuMark database tests to avoid conflicts with the TARGET_MANAGER singleton state from cloud database tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 5d2bb0c commit 2b6883b

17 files changed

Lines changed: 529 additions & 87 deletions

docs/source/mock-api-reference.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ API Reference
2020
:undoc-members:
2121
:exclude-members: to_dict, get_target, from_dict, not_deleted_targets, active_targets, inactive_targets, failed_targets, processing_targets
2222

23+
.. autoclass:: mock_vws.database.VuMarkDatabase
24+
:members:
25+
:undoc-members:
26+
:exclude-members: to_dict, from_dict, not_deleted_targets
27+
2328
.. autoenum:: mock_vws.states.States
2429
:members:
2530
:undoc-members:
2631

2732
.. autoclass:: mock_vws.target.ImageTarget
2833

34+
.. autoclass:: mock_vws.target.VuMarkTarget
35+
2936
Image matchers
3037
--------------
3138

src/mock_vws/_database_matchers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from beartype import beartype
66
from vws_auth_tools import authorization_header
77

8-
from mock_vws.database import CloudDatabase
8+
from mock_vws.database import CloudDatabase, VuMarkDatabase
9+
10+
AnyDatabase = CloudDatabase | VuMarkDatabase
911

1012

1113
@beartype
@@ -58,14 +60,14 @@ def get_database_matching_client_keys(
5860

5961

6062
@beartype
61-
def get_database_matching_server_keys(
63+
def get_database_matching_server_keys[DatabaseT: AnyDatabase](
6264
*,
6365
request_headers: Mapping[str, str],
6466
request_body: bytes | None,
6567
request_method: str,
6668
request_path: str,
67-
databases: Iterable[CloudDatabase],
68-
) -> CloudDatabase:
69+
databases: Iterable[DatabaseT],
70+
) -> DatabaseT:
6971
"""Return the first of the given databases which is being accessed by
7072
the
7173
given server request.

src/mock_vws/_flask_server/target_manager.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from flask import Flask, Response, request
1313
from pydantic_settings import BaseSettings
1414

15-
from mock_vws.database import CloudDatabase
15+
from mock_vws.database import CloudDatabase, VuMarkDatabase
1616
from mock_vws.states import States
17-
from mock_vws.target import ImageTarget
17+
from mock_vws.target import ImageTarget, VuMarkTarget
1818
from mock_vws.target_manager import TargetManager
1919
from mock_vws.target_raters import (
2020
BrisqueTargetTrackingRater,
@@ -80,6 +80,25 @@ def delete_cloud_database(database_name: str) -> Response:
8080
return Response(response="", status=HTTPStatus.OK)
8181

8282

83+
@TARGET_MANAGER_FLASK_APP.route(
84+
rule="/vumark_databases/<string:database_name>",
85+
methods=[HTTPMethod.DELETE],
86+
)
87+
@beartype
88+
def delete_vumark_database(database_name: str) -> Response:
89+
"""Delete a VuMark database.
90+
91+
:status 200: The VuMark database has been deleted.
92+
"""
93+
(matching_database,) = {
94+
database
95+
for database in TARGET_MANAGER.vumark_databases
96+
if database_name == database.database_name
97+
}
98+
TARGET_MANAGER.remove_vumark_database(vumark_database=matching_database)
99+
return Response(response="", status=HTTPStatus.OK)
100+
101+
83102
@TARGET_MANAGER_FLASK_APP.route(
84103
rule="/cloud_databases", methods=[HTTPMethod.GET]
85104
)
@@ -95,6 +114,22 @@ def get_cloud_databases() -> Response:
95114
)
96115

97116

117+
@TARGET_MANAGER_FLASK_APP.route(
118+
rule="/vumark_databases",
119+
methods=[HTTPMethod.GET],
120+
)
121+
@beartype
122+
def get_vumark_databases() -> Response:
123+
"""Return a list of all VuMark databases."""
124+
databases = [
125+
database.to_dict() for database in TARGET_MANAGER.vumark_databases
126+
]
127+
return Response(
128+
response=json.dumps(obj=databases),
129+
status=HTTPStatus.OK,
130+
)
131+
132+
98133
@TARGET_MANAGER_FLASK_APP.route(
99134
rule="/cloud_databases", methods=[HTTPMethod.POST]
100135
)
@@ -194,6 +229,47 @@ def create_cloud_database() -> Response:
194229
)
195230

196231

232+
@TARGET_MANAGER_FLASK_APP.route(
233+
rule="/vumark_databases",
234+
methods=[HTTPMethod.POST],
235+
)
236+
@beartype
237+
def create_vumark_database() -> Response:
238+
"""Create a new VuMark database.
239+
240+
:status 201: The database has been successfully created.
241+
"""
242+
request_json = json.loads(s=request.data)
243+
random_vumark_database = VuMarkDatabase()
244+
database = VuMarkDatabase(
245+
server_access_key=request_json.get(
246+
"server_access_key",
247+
random_vumark_database.server_access_key,
248+
),
249+
server_secret_key=request_json.get(
250+
"server_secret_key",
251+
random_vumark_database.server_secret_key,
252+
),
253+
database_name=request_json.get(
254+
"database_name",
255+
random_vumark_database.database_name,
256+
),
257+
)
258+
259+
try:
260+
TARGET_MANAGER.add_vumark_database(vumark_database=database)
261+
except ValueError as exc:
262+
return Response(
263+
response=str(object=exc),
264+
status=HTTPStatus.CONFLICT,
265+
)
266+
267+
return Response(
268+
response=json.dumps(obj=database.to_dict()),
269+
status=HTTPStatus.CREATED,
270+
)
271+
272+
197273
@TARGET_MANAGER_FLASK_APP.route(
198274
rule="/cloud_databases/<string:database_name>/targets",
199275
methods=[HTTPMethod.POST],
@@ -230,6 +306,28 @@ def create_target(database_name: str) -> Response:
230306
)
231307

232308

309+
@TARGET_MANAGER_FLASK_APP.route(
310+
rule="/vumark_databases/<string:database_name>/vumark_targets",
311+
methods=[HTTPMethod.POST],
312+
)
313+
@beartype
314+
def create_vumark_target(database_name: str) -> Response:
315+
"""Create a new VuMark target in a given database."""
316+
(database,) = (
317+
database
318+
for database in TARGET_MANAGER.vumark_databases
319+
if database.database_name == database_name
320+
)
321+
request_json = json.loads(s=request.data)
322+
target = VuMarkTarget.from_dict(target_dict=request_json)
323+
database.vumark_targets.add(target)
324+
325+
return Response(
326+
response=json.dumps(obj=target.to_dict()),
327+
status=HTTPStatus.CREATED,
328+
)
329+
330+
233331
@TARGET_MANAGER_FLASK_APP.route(
234332
rule="/cloud_databases/<string:database_name>/targets/<string:target_id>",
235333
methods={HTTPMethod.DELETE},

src/mock_vws/_flask_server/vws.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
TargetStatusProcessingError,
3737
ValidatorError,
3838
)
39-
from mock_vws.database import CloudDatabase
39+
from mock_vws.database import CloudDatabase, VuMarkDatabase
4040
from mock_vws.image_matchers import (
4141
ExactMatcher,
4242
ImageMatcher,
@@ -100,6 +100,21 @@ def get_all_cloud_databases() -> set[CloudDatabase]:
100100
}
101101

102102

103+
@beartype
104+
def get_all_vumark_databases() -> set[VuMarkDatabase]:
105+
"""Get all VuMark database objects from the task manager back-end."""
106+
settings = VWSSettings.model_validate(obj={})
107+
timeout_seconds = 30
108+
response = requests.get(
109+
url=f"{settings.target_manager_base_url}/vumark_databases",
110+
timeout=timeout_seconds,
111+
)
112+
return {
113+
VuMarkDatabase.from_dict(database_dict=database_dict)
114+
for database_dict in response.json()
115+
}
116+
117+
103118
@VWS_FLASK_APP.before_request
104119
def set_terminate_wsgi_input() -> None:
105120
"""We set ``wsgi.input_terminated`` to ``True`` when going through
@@ -129,14 +144,19 @@ def set_terminate_wsgi_input() -> None:
129144
@VWS_FLASK_APP.before_request
130145
@beartype
131146
def validate_request() -> None:
132-
"""Run validators on the request."""
133-
databases = get_all_cloud_databases()
147+
"""Run validators on the request.
148+
149+
The VuMark endpoint does its own validation because it needs to
150+
authenticate against both cloud and VuMark databases.
151+
"""
152+
if request.endpoint == "generate_vumark_instance":
153+
return
134154
run_services_validators(
135155
request_headers=dict(request.headers),
136156
request_body=request.data,
137157
request_method=request.method,
138158
request_path=request.path,
139-
databases=databases,
159+
databases=get_all_cloud_databases(),
140160
)
141161

142162

@@ -357,6 +377,20 @@ def generate_vumark_instance(target_id: str) -> Response:
357377
Fake implementation of
358378
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#generate-instance
359379
"""
380+
cloud_databases = get_all_cloud_databases()
381+
vumark_databases = get_all_vumark_databases()
382+
all_databases: list[CloudDatabase | VuMarkDatabase] = [
383+
*cloud_databases,
384+
*vumark_databases,
385+
]
386+
run_services_validators(
387+
request_headers=dict(request.headers),
388+
request_body=request.data,
389+
request_method=request.method,
390+
request_path=request.path,
391+
databases=all_databases,
392+
)
393+
360394
# ``target_id`` is validated by request validators.
361395
del target_id
362396

src/mock_vws/_requests_mock_server/decorators.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from requests import PreparedRequest
1313
from responses import RequestsMock
1414

15-
from mock_vws.database import CloudDatabase
15+
from mock_vws.database import CloudDatabase, VuMarkDatabase
1616
from mock_vws.image_matchers import (
1717
ImageMatcher,
1818
StructuralSimilarityMatcher,
@@ -140,6 +140,20 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None:
140140
cloud_database=cloud_database,
141141
)
142142

143+
def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None:
144+
"""Add a VuMark database.
145+
146+
Args:
147+
vumark_database: The VuMark database to add.
148+
149+
Raises:
150+
ValueError: One of the given database keys matches a key for
151+
an existing database.
152+
"""
153+
self._target_manager.add_vumark_database(
154+
vumark_database=vumark_database,
155+
)
156+
143157
@staticmethod
144158
def _wrap_callback(
145159
callback: _Callback,

src/mock_vws/_requests_mock_server/mock_web_services_api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import uuid
1313
from collections.abc import Callable, Iterable, Mapping
1414
from http import HTTPMethod, HTTPStatus
15-
from typing import Any, ParamSpec, Protocol, runtime_checkable
15+
from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, runtime_checkable
1616
from zoneinfo import ZoneInfo
1717

1818
from beartype import BeartypeConf, beartype
@@ -41,6 +41,9 @@
4141
from mock_vws.target_manager import TargetManager
4242
from mock_vws.target_raters import TargetTrackingRater
4343

44+
if TYPE_CHECKING:
45+
from mock_vws.database import CloudDatabase, VuMarkDatabase
46+
4447
_TARGET_ID_PATTERN = "[A-Za-z0-9]+"
4548

4649

@@ -309,12 +312,16 @@ def generate_vumark_instance(
309312
"application/pdf": VUMARK_PDF,
310313
}
311314
try:
315+
all_databases: list[CloudDatabase | VuMarkDatabase] = [
316+
*self._target_manager.cloud_databases,
317+
*self._target_manager.vumark_databases,
318+
]
312319
run_services_validators(
313320
request_headers=request.headers,
314321
request_body=_body_bytes(request=request),
315322
request_method=request.method or "",
316323
request_path=request.path_url,
317-
databases=self._target_manager.cloud_databases,
324+
databases=all_databases,
318325
)
319326

320327
accept = dict(request.headers).get("Accept", "")

src/mock_vws/_services_validators/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections.abc import Iterable, Mapping
44

5-
from mock_vws.database import CloudDatabase
5+
from mock_vws._database_matchers import AnyDatabase
66

77
from .active_flag_validators import validate_active_flag
88
from .auth_validators import (
@@ -55,7 +55,7 @@ def run_services_validators(
5555
request_headers: Mapping[str, str],
5656
request_body: bytes,
5757
request_method: str,
58-
databases: Iterable[CloudDatabase],
58+
databases: Iterable[AnyDatabase],
5959
) -> None:
6060
"""Run all validators.
6161

src/mock_vws/_services_validators/auth_validators.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
from beartype import beartype
88

9-
from mock_vws._database_matchers import get_database_matching_server_keys
9+
from mock_vws._database_matchers import (
10+
AnyDatabase,
11+
get_database_matching_server_keys,
12+
)
1013
from mock_vws._services_validators.exceptions import (
1114
AuthenticationFailureError,
1215
FailError,
1316
)
14-
from mock_vws.database import CloudDatabase
1517

1618
_LOGGER = logging.getLogger(name=__name__)
1719

@@ -36,7 +38,7 @@ def validate_auth_header_exists(*, request_headers: Mapping[str, str]) -> None:
3638
def validate_access_key_exists(
3739
*,
3840
request_headers: Mapping[str, str],
39-
databases: Iterable[CloudDatabase],
41+
databases: Iterable[AnyDatabase],
4042
) -> None:
4143
"""Validate the authorization header includes an access key for a
4244
database.
@@ -92,7 +94,7 @@ def validate_authorization(
9294
request_headers: Mapping[str, str],
9395
request_body: bytes,
9496
request_method: str,
95-
databases: Iterable[CloudDatabase],
97+
databases: Iterable[AnyDatabase],
9698
) -> None:
9799
"""Validate the authorization header given to a VWS endpoint.
98100

0 commit comments

Comments
 (0)