Skip to content

Commit b6e6590

Browse files
adamtheturtleclaudepre-commit-ci-lite[bot]
authored
Add database and target type support with VuMark validation (#2963)
* Add database type and target type support with VuMark validation (#2962) Add DatabaseType enum (CLOUD_RECO, VUMARK) to distinguish database types and TargetType enum (IMAGE, VUMARK_TEMPLATE) for target classification. Implement InvalidTargetTypeError in VuMark generation endpoints to validate that VuMark instance generation only works on VUMARK-type databases. Update database and target serialization to include type information, and allow pre-population of VuMark targets in VuMark-type databases. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * Document DatabaseType and TargetType in API reference; clarify Target class Add autoenum entries for DatabaseType and TargetType to the API reference docs. Add a docstring note to Target clarifying that some attributes are primarily meaningful for image targets rather than VuMark template targets. Add vulture whitelist entries for the new TypedDict field and enum value. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * [pre-commit.ci lite] apply automatic fixes * Refactor Target into ImageTarget and VuMarkTarget classes - Rename Target → ImageTarget and TargetDict → ImageTargetDict - Add VuMarkTarget dataclass for VuMark template targets (name, active_flag, processing_time_seconds, target_id, dates; status always succeeds after processing) - Remove TargetType enum (target_type.py deleted); class type is now the discriminator - VuforiaDatabase.targets holds set[ImageTarget | VuMarkTarget] - Image-only operations (duplicates, query matching, width/reco fields) guarded with isinstance(target, ImageTarget) checks - Update docs API reference and CHANGELOG Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add VuMark to spelling dictionary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix InvalidTargetType response status code to 422 Real Vuforia returns 422 Unprocessable Entity (not 403 Forbidden) when attempting VuMark generation on a non-VuMark database. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix mypy errors and stale 'databases' attribute reference - Change new_target type annotations from ImageTarget | VuMarkTarget to ImageTarget in delete_target and update_target, since CloudDatabase.targets is set[ImageTarget] - Remove dead else branches in update_target (target is always ImageTarget in cloud databases) - Remove unused type: ignore[assignment] comments - Fix generate_vumark_instance to use all_databases instead of stale self._target_manager.databases attribute - Remove unused VuMarkTarget import from mock_web_services_api Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove unnecessary isinstance checks for ImageTarget Since CloudDatabase.targets is set[ImageTarget], targets are always ImageTarget — isinstance checks are redundant and pyright flags them. Remove all unnecessary isinstance(target, ImageTarget) guards and their dead else branches. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 2b6883b commit b6e6590

14 files changed

Lines changed: 221 additions & 56 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44
Next
55
----
66

7+
- Add ``VuMarkTarget`` class for VuMark template targets, alongside the renamed ``ImageTarget`` class (previously ``Target``).
8+
``ImageTarget`` is for image-based targets and ``VuMarkTarget`` is for VuMark template targets.
9+
Both can be stored in a ``VuforiaDatabase``.
10+
711
2026.02.18.2
812
------------
913

docs/source/mock-api-reference.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ API Reference
2929
:members:
3030
:undoc-members:
3131

32+
.. autoenum:: mock_vws.database_type.DatabaseType
33+
:members:
34+
:undoc-members:
35+
3236
.. autoclass:: mock_vws.target.ImageTarget
3337

3438
.. autoclass:: mock_vws.target.VuMarkTarget

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ ignore_names = [
452452
# Used in TYPE_CHECKING for type hints
453453
"CloudDatabaseDict",
454454
"VuMarkDatabaseDict",
455+
"VuMarkTargetDict",
455456
]
456457
# Duplicate some of .gitignore
457458
exclude = [ ".venv" ]

spelling_private_dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ MPixel
33
MiB
44
MissingSchema
55
Ubuntu
6+
VuMark
67
admin
78
another's
89
api

src/mock_vws/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class ResultCodes(Enum):
6363
INVALID_ACCEPT_HEADER = "InvalidAcceptHeader"
6464
INVALID_INSTANCE_ID = "InvalidInstanceId"
6565
BAD_REQUEST = "BadRequest"
66+
INVALID_TARGET_TYPE = "InvalidTargetType"
6667

6768

6869
@beartype

src/mock_vws/_flask_server/target_manager.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pydantic_settings import BaseSettings
1414

1515
from mock_vws.database import CloudDatabase, VuMarkDatabase
16+
from mock_vws.database_type import DatabaseType
1617
from mock_vws.states import States
1718
from mock_vws.target import ImageTarget, VuMarkTarget
1819
from mock_vws.target_manager import TargetManager
@@ -204,8 +205,13 @@ def create_cloud_database() -> Response:
204205
"state_name",
205206
random_database.state.name,
206207
)
208+
database_type_name = request_json.get(
209+
"database_type_name",
210+
random_database.database_type.name,
211+
)
207212

208213
state = States[state_name]
214+
database_type = DatabaseType[database_type_name]
209215

210216
database = CloudDatabase(
211217
server_access_key=server_access_key,
@@ -214,6 +220,7 @@ def create_cloud_database() -> Response:
214220
client_secret_key=client_secret_key,
215221
database_name=database_name,
216222
state=state,
223+
database_type=database_type,
217224
)
218225
try:
219226
TARGET_MANAGER.add_cloud_database(cloud_database=database)
@@ -283,11 +290,10 @@ def create_target(database_name: str) -> Response:
283290
if database.database_name == database_name
284291
)
285292
request_json = json.loads(s=request.data)
286-
image_base64 = request_json["image_base64"]
287-
image_bytes = base64.b64decode(s=image_base64)
288293
settings = TargetManagerSettings.model_validate(obj={})
289-
target_tracking_rater = settings.target_rater.to_target_rater()
290294

295+
image_bytes = base64.b64decode(s=request_json["image_base64"])
296+
target_tracking_rater = settings.target_rater.to_target_rater()
291297
target = ImageTarget(
292298
name=request_json["name"],
293299
width=request_json["width"],
@@ -343,7 +349,7 @@ def delete_target(database_name: str, target_id: str) -> Response:
343349
target = database.get_target(target_id=target_id)
344350
now = datetime.datetime.now(tz=target.upload_date.tzinfo)
345351
# See https://github.com/facebook/pyrefly/issues/1897
346-
new_target = copy.replace(
352+
new_target: ImageTarget = copy.replace(
347353
target, # pyrefly: ignore[bad-argument-type]
348354
delete_date=now,
349355
)
@@ -369,24 +375,22 @@ def update_target(database_name: str, target_id: str) -> Response:
369375
target = database.get_target(target_id=target_id)
370376

371377
request_json = json.loads(s=request.data)
372-
width = request_json.get("width", target.width)
373378
name = request_json.get("name", target.name)
374379
active_flag = request_json.get("active_flag", target.active_flag)
380+
381+
gmt = ZoneInfo(key="GMT")
382+
last_modified_date = datetime.datetime.now(tz=gmt)
383+
384+
width = request_json.get("width", target.width)
375385
application_metadata = request_json.get(
376386
"application_metadata",
377387
target.application_metadata,
378388
)
379-
380389
image_value = target.image_value
381-
request_json = json.loads(s=request.data)
382390
if "image" in request_json:
383391
image_value = base64.b64decode(s=request_json["image"])
384-
385-
gmt = ZoneInfo(key="GMT")
386-
last_modified_date = datetime.datetime.now(tz=gmt)
387-
388392
# See https://github.com/facebook/pyrefly/issues/1897
389-
new_target = copy.replace(
393+
new_target: ImageTarget = copy.replace(
390394
target, # pyrefly: ignore[bad-argument-type]
391395
name=name,
392396
width=width,

src/mock_vws/_flask_server/vws.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
FailError,
3333
InvalidAcceptHeaderError,
3434
InvalidInstanceIdError,
35+
InvalidTargetTypeError,
3536
TargetStatusNotSuccessError,
3637
TargetStatusProcessingError,
3738
ValidatorError,
@@ -278,13 +279,16 @@ def get_target(target_id: str) -> Response:
278279
target for target in database.targets if target.target_id == target_id
279280
)
280281

282+
width = target.width
283+
tracking_rating = target.tracking_rating
284+
reco_rating = target.reco_rating
281285
target_record = {
282286
"target_id": target.target_id,
283287
"active_flag": target.active_flag,
284288
"name": target.name,
285-
"width": target.width,
286-
"tracking_rating": target.tracking_rating,
287-
"reco_rating": target.reco_rating,
289+
"width": width,
290+
"tracking_rating": tracking_rating,
291+
"reco_rating": reco_rating,
288292
}
289293

290294
date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
@@ -394,6 +398,16 @@ def generate_vumark_instance(target_id: str) -> Response:
394398
# ``target_id`` is validated by request validators.
395399
del target_id
396400

401+
database = get_database_matching_server_keys(
402+
request_headers=dict(request.headers),
403+
request_body=request.data,
404+
request_method=request.method,
405+
request_path=request.path,
406+
databases=all_databases,
407+
)
408+
if not isinstance(database, VuMarkDatabase):
409+
raise InvalidTargetTypeError
410+
397411
accept = request.headers.get(key="Accept", default="")
398412
valid_accept_types: dict[str, bytes] = {
399413
"image/png": VUMARK_PNG,
@@ -503,6 +517,10 @@ def target_summary(target_id: str) -> Response:
503517
(target,) = (
504518
target for target in database.targets if target.target_id == target_id
505519
)
520+
tracking_rating = target.tracking_rating
521+
total_recos = target.total_recos
522+
current_month_recos = target.current_month_recos
523+
previous_month_recos = target.previous_month_recos
506524
body = {
507525
"status": target.status,
508526
"transaction_id": uuid.uuid4().hex,
@@ -511,10 +529,10 @@ def target_summary(target_id: str) -> Response:
511529
"target_name": target.name,
512530
"upload_date": target.upload_date.strftime(format="%Y-%m-%d"),
513531
"active_flag": target.active_flag,
514-
"tracking_rating": target.tracking_rating,
515-
"total_recos": target.total_recos,
516-
"current_month_recos": target.current_month_recos,
517-
"previous_month_recos": target.previous_month_recos,
532+
"tracking_rating": tracking_rating,
533+
"total_recos": total_recos,
534+
"current_month_recos": current_month_recos,
535+
"previous_month_recos": previous_month_recos,
518536
}
519537
date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
520538
headers = {

src/mock_vws/_requests_mock_server/mock_web_services_api.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,19 @@
3232
FailError,
3333
InvalidAcceptHeaderError,
3434
InvalidInstanceIdError,
35+
InvalidTargetTypeError,
3536
TargetStatusNotSuccessError,
3637
TargetStatusProcessingError,
3738
ValidatorError,
3839
)
40+
from mock_vws.database import VuMarkDatabase
3941
from mock_vws.image_matchers import ImageMatcher
4042
from mock_vws.target import ImageTarget
4143
from mock_vws.target_manager import TargetManager
4244
from mock_vws.target_raters import TargetTrackingRater
4345

4446
if TYPE_CHECKING:
45-
from mock_vws.database import CloudDatabase, VuMarkDatabase
47+
from mock_vws.database import CloudDatabase
4648

4749
_TARGET_ID_PATTERN = "[A-Za-z0-9]+"
4850

@@ -268,7 +270,7 @@ def delete_target(self, request: PreparedRequest) -> _ResponseType:
268270

269271
now = datetime.datetime.now(tz=target.upload_date.tzinfo)
270272
# See https://github.com/facebook/pyrefly/issues/1897
271-
new_target = copy.replace(
273+
new_target: ImageTarget = copy.replace(
272274
target, # pyrefly: ignore[bad-argument-type]
273275
delete_date=now,
274276
)
@@ -324,6 +326,16 @@ def generate_vumark_instance(
324326
databases=all_databases,
325327
)
326328

329+
database = get_database_matching_server_keys(
330+
request_headers=request.headers,
331+
request_body=_body_bytes(request=request),
332+
request_method=request.method or "",
333+
request_path=request.path_url,
334+
databases=all_databases,
335+
)
336+
if not isinstance(database, VuMarkDatabase):
337+
raise InvalidTargetTypeError
338+
327339
accept = dict(request.headers).get("Accept", "")
328340
if accept not in valid_accept_types:
329341
raise InvalidAcceptHeaderError
@@ -500,13 +512,16 @@ def get_target(self, request: PreparedRequest) -> _ResponseType:
500512
target_id = request.path_url.split(sep="/")[-1]
501513
target = database.get_target(target_id=target_id)
502514

515+
width = target.width
516+
tracking_rating = target.tracking_rating
517+
reco_rating = target.reco_rating
503518
target_record = {
504519
"target_id": target.target_id,
505520
"active_flag": target.active_flag,
506521
"name": target.name,
507-
"width": target.width,
508-
"tracking_rating": target.tracking_rating,
509-
"reco_rating": target.reco_rating,
522+
"width": width,
523+
"tracking_rating": tracking_rating,
524+
"reco_rating": reco_rating,
510525
}
511526
date = email.utils.formatdate(
512527
timeval=None,
@@ -653,17 +668,8 @@ def update_target(self, request: PreparedRequest) -> _ResponseType:
653668
)
654669

655670
request_json: dict[str, Any] = json.loads(s=request.body or b"")
656-
width = request_json.get("width", target.width)
657671
name = request_json.get("name", target.name)
658672
active_flag = request_json.get("active_flag", target.active_flag)
659-
application_metadata = request_json.get(
660-
"application_metadata",
661-
target.application_metadata,
662-
)
663-
664-
image_value = target.image_value
665-
if "image" in request_json:
666-
image_value = base64.b64decode(s=request_json["image"])
667673

668674
if "active_flag" in request_json and active_flag is None:
669675
fail_exception = FailError(status_code=HTTPStatus.BAD_REQUEST)
@@ -673,6 +679,19 @@ def update_target(self, request: PreparedRequest) -> _ResponseType:
673679
fail_exception.response_text,
674680
)
675681

682+
gmt = ZoneInfo(key="GMT")
683+
last_modified_date = datetime.datetime.now(tz=gmt)
684+
685+
width = request_json.get("width", target.width)
686+
application_metadata = request_json.get(
687+
"application_metadata",
688+
target.application_metadata,
689+
)
690+
691+
image_value = target.image_value
692+
if "image" in request_json:
693+
image_value = base64.b64decode(s=request_json["image"])
694+
676695
if (
677696
"application_metadata" in request_json
678697
and application_metadata is None
@@ -684,11 +703,8 @@ def update_target(self, request: PreparedRequest) -> _ResponseType:
684703
fail_exception.response_text,
685704
)
686705

687-
gmt = ZoneInfo(key="GMT")
688-
last_modified_date = datetime.datetime.now(tz=gmt)
689-
690706
# See https://github.com/facebook/pyrefly/issues/1897
691-
new_target = copy.replace(
707+
new_target: ImageTarget = copy.replace(
692708
target, # pyrefly: ignore[bad-argument-type]
693709
name=name,
694710
width=width,
@@ -755,6 +771,10 @@ def target_summary(self, request: PreparedRequest) -> _ResponseType:
755771
localtime=False,
756772
usegmt=True,
757773
)
774+
tracking_rating = target.tracking_rating
775+
total_recos = target.total_recos
776+
current_month_recos = target.current_month_recos
777+
previous_month_recos = target.previous_month_recos
758778
body = {
759779
"status": target.status,
760780
"transaction_id": uuid.uuid4().hex,
@@ -763,10 +783,10 @@ def target_summary(self, request: PreparedRequest) -> _ResponseType:
763783
"target_name": target.name,
764784
"upload_date": target.upload_date.strftime(format="%Y-%m-%d"),
765785
"active_flag": target.active_flag,
766-
"tracking_rating": target.tracking_rating,
767-
"total_recos": target.total_recos,
768-
"current_month_recos": target.current_month_recos,
769-
"previous_month_recos": target.previous_month_recos,
786+
"tracking_rating": tracking_rating,
787+
"total_recos": total_recos,
788+
"current_month_recos": current_month_recos,
789+
"previous_month_recos": previous_month_recos,
770790
}
771791
body_json = json_dump(body=body)
772792
headers = {

src/mock_vws/_services_validators/exceptions.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,46 @@ def __init__(self) -> None:
646646
}
647647

648648

649+
@beartype
650+
class InvalidTargetTypeError(ValidatorError):
651+
"""Exception raised when the target type is not valid for the
652+
operation.
653+
"""
654+
655+
def __init__(self) -> None:
656+
"""
657+
Attributes:
658+
status_code: The status code to use in a response if this is
659+
raised.
660+
response_text: The response text to use in a response if this
661+
is
662+
raised.
663+
"""
664+
super().__init__()
665+
self.status_code = HTTPStatus.UNPROCESSABLE_ENTITY
666+
body = {
667+
"transaction_id": uuid.uuid4().hex,
668+
"result_code": ResultCodes.INVALID_TARGET_TYPE.value,
669+
}
670+
self.response_text = json_dump(body=body)
671+
date = email.utils.formatdate(
672+
timeval=None,
673+
localtime=False,
674+
usegmt=True,
675+
)
676+
self.headers = {
677+
"Connection": "keep-alive",
678+
"Content-Type": "application/json",
679+
"server": "envoy",
680+
"Date": date,
681+
"x-envoy-upstream-service-time": "5",
682+
"Content-Length": str(object=len(self.response_text)),
683+
"strict-transport-security": "max-age=31536000",
684+
"x-aws-region": "us-east-2, us-west-2",
685+
"x-content-type-options": "nosniff",
686+
}
687+
688+
649689
@beartype
650690
class TargetStatusProcessingError(ValidatorError):
651691
"""Exception raised when trying to delete a target which is processing."""

0 commit comments

Comments
 (0)