Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/reco-fields-round-trip.change
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
Preserve the recognition count fields, the reco rating and the reco threshold when dumping a ``CloudDatabase`` or an ``ImageTarget`` to a dictionary and loading it back.
12 changes: 12 additions & 0 deletions src/mock_vws/database.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,10 @@ class CloudDatabaseDict(TypedDict):
database_type_name: str
targets: Iterable[ImageTargetDict]
request_quota: NotRequired[int]
reco_threshold: NotRequired[int]
current_month_recos: NotRequired[int]
previous_month_recos: NotRequired[int]
total_recos: NotRequired[int]
target_quota: NotRequired[int]
requests_per_second_limit: NotRequired[int | None]
request_rate_limits: NotRequired[RequestRateLimitsDict | None]
Expand DownExpand Up@@ -144,6 +148,10 @@ def to_dict(self) -> CloudDatabaseDict:
"database_type_name": self.database_type.name,
"targets": targets,
"request_quota": self.request_quota,
"reco_threshold": self.reco_threshold,
"current_month_recos": self.current_month_recos,
"previous_month_recos": self.previous_month_recos,
"total_recos": self.total_recos,
"target_quota": self.target_quota,
"requests_per_second_limit": self.requests_per_second_limit,
"request_rate_limits": request_rate_limits,
Expand DownExpand Up@@ -183,6 +191,10 @@ def from_dict(cls, database_dict: CloudDatabaseDict) -> Self:
database_type=DatabaseType[database_dict["database_type_name"]],
targets=targets,
request_quota=database_dict.get("request_quota", 100000),
reco_threshold=database_dict.get("reco_threshold", 1000),
current_month_recos=database_dict.get("current_month_recos", 0),
previous_month_recos=database_dict.get("previous_month_recos", 0),
total_recos=database_dict.get("total_recos", 0),
target_quota=database_dict.get("target_quota", 1000),
requests_per_second_limit=database_dict.get(
"requests_per_second_limit"
Expand Down
14 changes: 13 additions & 1 deletion src/mock_vws/target.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
import statistics
import uuid
from dataclasses import dataclass, field
from typing import Self, TypedDict
from typing import NotRequired, Self, TypedDict
from zoneinfo import ZoneInfo

from beartype import BeartypeConf, beartype
Expand DownExpand Up@@ -43,6 +43,10 @@ class ImageTargetDict(TypedDict):
delete_date_optional: str | None
upload_date: str
tracking_rating: int
current_month_recos: NotRequired[int]
previous_month_recos: NotRequired[int]
total_recos: NotRequired[int]
reco_rating: NotRequired[str]


@beartype
Expand DownExpand Up@@ -193,6 +197,10 @@ def from_dict(cls, target_dict: ImageTargetDict) -> Self:
last_modified_date=last_modified_date,
upload_date=upload_date,
target_tracking_rater=target_tracking_rater,
current_month_recos=target_dict.get("current_month_recos", 0),
previous_month_recos=target_dict.get("previous_month_recos", 0),
total_recos=target_dict.get("total_recos", 0),
reco_rating=target_dict.get("reco_rating", ""),
)

def to_dict(self) -> ImageTargetDict:
Expand All@@ -215,6 +223,10 @@ def to_dict(self) -> ImageTargetDict:
"delete_date_optional": delete_date,
"upload_date": self.upload_date.isoformat(),
"tracking_rating": self.tracking_rating,
"current_month_recos": self.current_month_recos,
"previous_month_recos": self.previous_month_recos,
"total_recos": self.total_recos,
"reco_rating": self.reco_rating,
}


Expand Down
148 changes: 148 additions & 0 deletions tests/mock_vws/test_requests_mock_usage.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
"""Tests for the usage of the mock for ``requests``."""

import dataclasses
import datetime
import email.utils
import io
Expand All@@ -8,6 +9,7 @@
import zipfile
from http import HTTPStatus
from urllib.parse import urlparse
from zoneinfo import ZoneInfo

import httpx
import pytest
Expand All@@ -33,6 +35,7 @@
RequestRateLimiter,
)
from mock_vws.database import CloudDatabase, VuMarkDatabase
from mock_vws.database_type import DatabaseType
from mock_vws.image_matchers import ExactMatcher, StructuralSimilarityMatcher
from mock_vws.request_rate_limits import (
DOCUMENTED_REQUEST_RATE_LIMITS,
Expand All@@ -42,6 +45,7 @@
)
from mock_vws.states import States
from mock_vws.target import ImageTarget, VuMarkTarget
from mock_vws.target_raters import HardcodedTargetTrackingRater
from tests.mock_vws.utils import Endpoint
from tests.mock_vws.utils.assertions import assert_vws_failure
from tests.mock_vws.utils.usage_test_helpers import (
Expand DownExpand Up@@ -974,6 +978,74 @@ def test_to_dict_deleted(high_quality_image: io.BytesIO) -> None:
new_target = ImageTarget.from_dict(target_dict=target_dict)
assert new_target.delete_date == target.delete_date

@staticmethod
def test_round_trip_non_default_fields(
high_quality_image: io.BytesIO,
) -> None:
"""Every field of a target survives a dictionary round trip.

The target tracking rater is deliberately not preserved:
``to_dict`` writes the computed tracking rating and ``from_dict``
rebuilds the target with a hardcoded rater which gives that
rating.
"""
gmt = ZoneInfo(key="GMT")
target = ImageTarget(
active_flag=False,
application_metadata="example-metadata",
current_month_recos=1,
delete_date=datetime.datetime(
year=2020, month=1, day=4, tzinfo=gmt
),
image_value=high_quality_image.getvalue(),
last_modified_date=datetime.datetime(
year=2020, month=1, day=3, tzinfo=gmt
),
name="example",
previous_month_recos=2,
processing_time_seconds=0.5,
reco_rating="example-reco-rating",
target_id="example-target-id",
target_tracking_rater=HardcodedTargetTrackingRater(rating=4),
total_recos=3,
upload_date=datetime.datetime(
year=2020, month=1, day=2, tzinfo=gmt
),
width=1.5,
)
# Adding a field to ``ImageTarget`` must mean adding it to this
# test, and therefore to the round trip.
expected_field_names = {
"active_flag",
"application_metadata",
"current_month_recos",
"delete_date",
"image_value",
"last_modified_date",
"name",
"previous_month_recos",
"processing_time_seconds",
"reco_rating",
"target_id",
"target_tracking_rater",
"total_recos",
"upload_date",
"width",
}
field_names = {
field.name
for field in dataclasses.fields(class_or_instance=ImageTarget)
}
assert field_names == expected_field_names

target_dict = target.to_dict()
# The dictionary is JSON dump-able
assert json.dumps(obj=target_dict)

new_target = ImageTarget.from_dict(target_dict=target_dict)
assert new_target == target
assert new_target.tracking_rating == target.tracking_rating

@staticmethod
def test_vumark_target_to_dict() -> None:
"""It is possible to dump a VuMark target to a dictionary and
Expand DownExpand Up@@ -1078,6 +1150,82 @@ def test_custom_request_rate_limits() -> None:
new_database.request_rate_limits == DOCUMENTED_REQUEST_RATE_LIMITS
)

@staticmethod
def test_round_trip_non_default_fields(
high_quality_image: io.BytesIO,
) -> None:
"""Every field of a database survives a dictionary round trip."""
gmt = ZoneInfo(key="GMT")
target = ImageTarget(
active_flag=True,
application_metadata=None,
image_value=high_quality_image.getvalue(),
last_modified_date=datetime.datetime(
year=2020, month=1, day=3, tzinfo=gmt
),
name="example",
processing_time_seconds=0.5,
target_tracking_rater=HardcodedTargetTrackingRater(rating=4),
upload_date=datetime.datetime(
year=2020, month=1, day=2, tzinfo=gmt
),
width=1.5,
)
database = CloudDatabase(
client_access_key="example-client-access-key",
client_secret_key="example-client-secret-key",
current_month_recos=1,
database_id="example-database-id",
database_name="example-database-name",
# ``CLOUD_RECO`` is the only database type, so it is not
# possible to use a non-default value here.
database_type=DatabaseType.CLOUD_RECO,
previous_month_recos=2,
reco_threshold=3,
request_quota=4,
request_rate_limits=DOCUMENTED_REQUEST_RATE_LIMITS,
requests_per_second_limit=5,
server_access_key="example-server-access-key",
server_secret_key="example-server-secret-key",
state=States.PROJECT_SUSPENDED,
target_quota=6,
targets={target},
total_recos=7,
)
# Adding a field to ``CloudDatabase`` must mean adding it to this
# test, and therefore to the round trip.
expected_field_names = {
"client_access_key",
"client_secret_key",
"current_month_recos",
"database_id",
"database_name",
"database_type",
"previous_month_recos",
"reco_threshold",
"request_quota",
"request_rate_limits",
"requests_per_second_limit",
"server_access_key",
"server_secret_key",
"state",
"target_quota",
"targets",
"total_recos",
}
field_names = {
field.name
for field in dataclasses.fields(class_or_instance=CloudDatabase)
}
assert field_names == expected_field_names

database_dict = database.to_dict()
# The dictionary is JSON dump-able
assert json.dumps(obj=database_dict)

new_database = CloudDatabase.from_dict(database_dict=database_dict)
assert new_database == database

@staticmethod
def test_vumark_database_to_dict() -> None:
"""It is possible to dump a VuMark database to a dictionary and
Expand Down