diff --git a/conftest.py b/conftest.py index e8f679b7..f7b14d1d 100644 --- a/conftest.py +++ b/conftest.py @@ -32,8 +32,8 @@ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: ).pytest() -@pytest.hookimpl(optionalhook=True) @beartype +@pytest.hookimpl(optionalhook=True) def pytest_set_filtered_exceptions() -> tuple[type[Exception], ...]: """ Return exceptions to retry on. diff --git a/src/mock_vws/_constants.py b/src/mock_vws/_constants.py index 84b22380..dc964520 100644 --- a/src/mock_vws/_constants.py +++ b/src/mock_vws/_constants.py @@ -4,7 +4,10 @@ from enum import Enum +from beartype import beartype + +@beartype class ResultCodes(Enum): """ Constants representing various VWS result codes. @@ -37,6 +40,7 @@ class ResultCodes(Enum): TOO_MANY_REQUESTS = "TooManyRequests" +@beartype class TargetStatuses(Enum): """ Constants representing VWS target statuses. diff --git a/src/mock_vws/_flask_server/healthcheck.py b/src/mock_vws/_flask_server/healthcheck.py index c4513fbf..a20f3a8c 100644 --- a/src/mock_vws/_flask_server/healthcheck.py +++ b/src/mock_vws/_flask_server/healthcheck.py @@ -7,7 +7,10 @@ import sys from http import HTTPStatus +from beartype import beartype + +@beartype def flask_app_healthy(port: int) -> bool: """ Check if the Flask app is healthy. diff --git a/src/mock_vws/_flask_server/target_manager.py b/src/mock_vws/_flask_server/target_manager.py index 5ba64629..83afda03 100644 --- a/src/mock_vws/_flask_server/target_manager.py +++ b/src/mock_vws/_flask_server/target_manager.py @@ -30,6 +30,7 @@ TARGET_MANAGER = TargetManager() +@beartype class _TargetRaterChoice(StrEnum): """Target rater choices.""" @@ -48,6 +49,7 @@ def to_target_rater(self) -> TargetTrackingRater: return rater +@beartype class TargetManagerSettings(BaseSettings): """Settings for the Target Manager Flask app.""" @@ -59,6 +61,7 @@ class TargetManagerSettings(BaseSettings): "/databases/", methods=[HTTPMethod.DELETE], ) +@beartype def delete_database(database_name: str) -> Response: """ Delete a database. @@ -92,6 +95,7 @@ def get_databases() -> Response: @TARGET_MANAGER_FLASK_APP.route("/databases", methods=[HTTPMethod.POST]) +@beartype def create_database() -> Response: """ Create a new database. @@ -177,6 +181,7 @@ def create_database() -> Response: "/databases//targets", methods=[HTTPMethod.POST], ) +@beartype def create_target(database_name: str) -> Response: """ Create a new target in a given database. @@ -212,8 +217,9 @@ def create_target(database_name: str) -> Response: @TARGET_MANAGER_FLASK_APP.route( "/databases//targets/", - methods=[HTTPMethod.DELETE], + methods={HTTPMethod.DELETE}, ) +@beartype def delete_target(database_name: str, target_id: str) -> Response: """ Delete a target. diff --git a/src/mock_vws/_flask_server/vwq.py b/src/mock_vws/_flask_server/vwq.py index 567b62ee..f0b7394c 100644 --- a/src/mock_vws/_flask_server/vwq.py +++ b/src/mock_vws/_flask_server/vwq.py @@ -32,6 +32,7 @@ CLOUDRECO_FLASK_APP.config["PROPAGATE_EXCEPTIONS"] = True +@beartype class _ImageMatcherChoice(StrEnum): """Image matcher choices.""" @@ -49,6 +50,7 @@ def to_image_matcher(self) -> ImageMatcher: return matcher +@beartype class VWQSettings(BaseSettings): """Settings for the VWQ Flask app.""" @@ -76,6 +78,7 @@ def get_all_databases() -> set[VuforiaDatabase]: @CLOUDRECO_FLASK_APP.before_request +@beartype def set_terminate_wsgi_input() -> None: """ We set ``wsgi.input_terminated`` to ``True`` when going through diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index 4d6f28d3..d5913f47 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -46,6 +46,7 @@ _LOGGER = logging.getLogger(name=__name__) +@beartype class _ImageMatcherChoice(StrEnum): """Image matcher choices.""" @@ -63,6 +64,7 @@ def to_image_matcher(self) -> ImageMatcher: return matcher +@beartype class VWSSettings(BaseSettings): """Settings for the VWS Flask app.""" @@ -151,6 +153,7 @@ def handle_exceptions(exc: ValidatorError) -> Response: @VWS_FLASK_APP.route("/targets", methods=[HTTPMethod.POST]) +@beartype def add_target() -> Response: """ Add a target. @@ -334,6 +337,7 @@ def delete_target(target_id: str) -> Response: @VWS_FLASK_APP.route("/summary", methods=[HTTPMethod.GET]) +@beartype def database_summary() -> Response: """ Get a database summary report. diff --git a/src/mock_vws/_query_validators/exceptions.py b/src/mock_vws/_query_validators/exceptions.py index 48e8fb71..8a39d468 100644 --- a/src/mock_vws/_query_validators/exceptions.py +++ b/src/mock_vws/_query_validators/exceptions.py @@ -7,10 +7,13 @@ import uuid from http import HTTPStatus +from beartype import beartype + from mock_vws._constants import ResultCodes from mock_vws._mock_common import json_dump +@beartype class ValidatorError(Exception): """ A base class for exceptions thrown from mock Vuforia cloud recognition @@ -22,6 +25,7 @@ class ValidatorError(Exception): headers: dict[str, str] +@beartype class DateHeaderNotGivenError(ValidatorError): """ Exception raised when a date header is not given. @@ -52,6 +56,7 @@ def __init__(self) -> None: } +@beartype class DateFormatNotValidError(ValidatorError): """ Exception raised when the date format is not valid. @@ -83,6 +88,7 @@ def __init__(self) -> None: } +@beartype class RequestTimeTooSkewedError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -118,6 +124,7 @@ def __init__(self) -> None: } +@beartype class BadImageError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -160,6 +167,7 @@ def __init__(self) -> None: } +@beartype class AuthenticationFailureError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -202,6 +210,7 @@ def __init__(self) -> None: } +@beartype class AuthenticationFailureGoodFormattingError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -239,6 +248,7 @@ def __init__(self) -> None: } +@beartype class ImageNotGivenError(ValidatorError): """ Exception raised when an image is not given. @@ -270,6 +280,7 @@ def __init__(self) -> None: } +@beartype class AuthHeaderMissingError(ValidatorError): """ Exception raised when an auth header is not given. @@ -302,6 +313,7 @@ def __init__(self) -> None: } +@beartype class MalformedAuthHeaderError(ValidatorError): """ Exception raised when an auth header is not given. @@ -335,6 +347,7 @@ def __init__(self) -> None: } +@beartype class UnknownParametersError(ValidatorError): """ Exception raised when unknown parameters are given. @@ -366,6 +379,7 @@ def __init__(self) -> None: } +@beartype class InactiveProjectError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -407,6 +421,7 @@ def __init__(self) -> None: } +@beartype class InvalidMaxNumResultsError(ValidatorError): """ Exception raised when an invalid value is given as the @@ -443,6 +458,7 @@ def __init__(self, given_value: str) -> None: } +@beartype class MaxNumResultsOutOfRangeError(ValidatorError): """ Exception raised when an integer value is given as the "max_num_results" @@ -479,6 +495,7 @@ def __init__(self, given_value: str) -> None: } +@beartype class InvalidIncludeTargetDataError(ValidatorError): """ Exception raised when an invalid value is given as the @@ -517,6 +534,7 @@ def __init__(self, given_value: str) -> None: } +@beartype class UnsupportedMediaTypeError(ValidatorError): """ Exception raised when no boundary is found for multipart data. @@ -547,6 +565,7 @@ def __init__(self) -> None: } +@beartype class InvalidAcceptHeaderError(ValidatorError): """ Exception raised when there is an invalid accept header given. @@ -577,6 +596,7 @@ def __init__(self) -> None: } +@beartype class NoBoundaryFoundError(ValidatorError): """ Exception raised when an invalid media type is given. @@ -611,6 +631,7 @@ def __init__(self) -> None: } +@beartype class ContentLengthHeaderTooLargeError(ValidatorError): """ Exception raised when the given content length header is too large. @@ -634,6 +655,7 @@ def __init__(self) -> None: # pragma: no cover } +@beartype class ContentLengthHeaderNotIntError(ValidatorError): """ Exception raised when the given content length header is not an integer. @@ -656,6 +678,7 @@ def __init__(self) -> None: } +@beartype class RequestEntityTooLargeError(ValidatorError): """ Exception raised when the given image file size is too large. @@ -699,6 +722,7 @@ def __init__(self) -> None: # pragma: no cover } +@beartype class NoContentTypeError(ValidatorError): """ Exception raised when a content type is either not given or is empty. diff --git a/src/mock_vws/_requests_mock_server/decorators.py b/src/mock_vws/_requests_mock_server/decorators.py index 1c856e9d..69f6c149 100644 --- a/src/mock_vws/_requests_mock_server/decorators.py +++ b/src/mock_vws/_requests_mock_server/decorators.py @@ -8,6 +8,7 @@ from urllib.parse import urljoin, urlparse import requests +from beartype import BeartypeConf, beartype from responses import RequestsMock from mock_vws.database import VuforiaDatabase @@ -28,6 +29,7 @@ _BRISQUE_TRACKING_RATER = BrisqueTargetTrackingRater() +@beartype(conf=BeartypeConf(is_pep484_tower=True)) class MockVWS(ContextDecorator): """ Route requests to Vuforia's Web Service APIs to fakes of those APIs. @@ -86,7 +88,7 @@ def __init__( self._mock_vws_api = MockVuforiaWebServicesAPI( target_manager=self._target_manager, - processing_time_seconds=processing_time_seconds, + processing_time_seconds=float(processing_time_seconds), duplicate_match_checker=duplicate_match_checker, target_tracking_rater=target_tracking_rater, ) diff --git a/src/mock_vws/_requests_mock_server/mock_web_query_api.py b/src/mock_vws/_requests_mock_server/mock_web_query_api.py index 13ba6cef..cc6871b8 100644 --- a/src/mock_vws/_requests_mock_server/mock_web_query_api.py +++ b/src/mock_vws/_requests_mock_server/mock_web_query_api.py @@ -9,6 +9,7 @@ from collections.abc import Callable from http import HTTPMethod, HTTPStatus +from beartype import beartype from requests.models import PreparedRequest from mock_vws._mock_common import Route @@ -27,6 +28,7 @@ _ResponseType = tuple[int, dict[str, str], str] +@beartype def route( path_pattern: str, http_methods: set[str], @@ -66,6 +68,7 @@ def decorator( return decorator +@beartype def _body_bytes(request: PreparedRequest) -> bytes: """ Return the body of a request as bytes. @@ -77,6 +80,7 @@ def _body_bytes(request: PreparedRequest) -> bytes: return request.body +@beartype class MockVuforiaWebQueryAPI: """ A fake implementation of the Vuforia Web Query API. diff --git a/src/mock_vws/_requests_mock_server/mock_web_services_api.py b/src/mock_vws/_requests_mock_server/mock_web_services_api.py index bcd6bfb2..36936c03 100644 --- a/src/mock_vws/_requests_mock_server/mock_web_services_api.py +++ b/src/mock_vws/_requests_mock_server/mock_web_services_api.py @@ -59,6 +59,7 @@ def route( A decorator which takes methods and makes them recognizable as routes. """ + @beartype def decorator( method: Callable[..., _ResponseType], ) -> Callable[..., _ResponseType]: @@ -82,6 +83,7 @@ def decorator( return decorator +@beartype def _body_bytes(request: PreparedRequest) -> bytes: """ Return the body of a request as bytes. @@ -96,7 +98,7 @@ def _body_bytes(request: PreparedRequest) -> bytes: return request.body -@beartype +@beartype(conf=BeartypeConf(is_pep484_tower=True)) class MockVuforiaWebServicesAPI: """ A fake implementation of the Vuforia Web Services API. @@ -104,7 +106,6 @@ class MockVuforiaWebServicesAPI: This implementation is tied to the implementation of ``responses``. """ - @beartype(conf=BeartypeConf(is_pep484_tower=True)) def __init__( self, *, diff --git a/src/mock_vws/_services_validators/exceptions.py b/src/mock_vws/_services_validators/exceptions.py index 8fc8fc4b..f528f1bf 100644 --- a/src/mock_vws/_services_validators/exceptions.py +++ b/src/mock_vws/_services_validators/exceptions.py @@ -8,10 +8,13 @@ from http import HTTPStatus from pathlib import Path +from beartype import beartype + from mock_vws._constants import ResultCodes from mock_vws._mock_common import json_dump +@beartype class ValidatorError(Exception): """ A base class for exceptions thrown from mock Vuforia services endpoints. @@ -22,6 +25,7 @@ class ValidatorError(Exception): headers: dict[str, str] +@beartype class UnknownTargetError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -61,6 +65,7 @@ def __init__(self) -> None: } +@beartype class ProjectInactiveError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -100,6 +105,7 @@ def __init__(self) -> None: } +@beartype class AuthenticationFailureError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -139,6 +145,7 @@ def __init__(self) -> None: } +@beartype class FailError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code 'Fail'. @@ -177,6 +184,7 @@ def __init__(self, *, status_code: HTTPStatus) -> None: } +@beartype class MetadataTooLargeError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -216,6 +224,7 @@ def __init__(self) -> None: } +@beartype class TargetNameExistError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -255,6 +264,7 @@ def __init__(self) -> None: } +@beartype class OopsErrorOccurredResponseError(ValidatorError): """ Exception raised when VWS returns an HTML page which says "Oops, an error @@ -296,6 +306,7 @@ def __init__(self) -> None: } +@beartype class BadImageError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -335,6 +346,7 @@ def __init__(self) -> None: } +@beartype class ImageTooLargeError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -374,6 +386,7 @@ def __init__(self) -> None: } +@beartype class RequestTimeTooSkewedError(ValidatorError): """ Exception raised when Vuforia returns a response with a result code @@ -413,6 +426,7 @@ def __init__(self) -> None: } +@beartype class ContentLengthHeaderTooLargeError(ValidatorError): """ Exception raised when the given content length header is too large. @@ -444,6 +458,7 @@ def __init__(self) -> None: # pragma: no cover } +@beartype class ContentLengthHeaderNotIntError(ValidatorError): """ Exception raised when the given content length header is not an integer. @@ -483,6 +498,7 @@ def __init__(self) -> None: } +@beartype class UnnecessaryRequestBodyError(ValidatorError): """ Exception raised when a request body is given but not necessary. @@ -512,6 +528,7 @@ def __init__(self) -> None: } +@beartype class TargetStatusNotSuccessError(ValidatorError): """ Exception raised when trying to update a target that does not have a @@ -551,6 +568,7 @@ def __init__(self) -> None: } +@beartype class TargetStatusProcessingError(ValidatorError): """ Exception raised when trying to delete a target which is processing. diff --git a/src/mock_vws/database.py b/src/mock_vws/database.py index 75f68edb..153c8f58 100644 --- a/src/mock_vws/database.py +++ b/src/mock_vws/database.py @@ -13,6 +13,7 @@ from mock_vws.target import Target, TargetDict +@beartype class DatabaseDict(TypedDict): """ A dictionary type which represents a database. @@ -76,7 +77,6 @@ class VuforiaDatabase: total_recos: int = 0 target_quota: int = 1000 - @beartype def to_dict(self) -> DatabaseDict: """ Dump a target to a dictionary which can be loaded as JSON. @@ -92,7 +92,6 @@ def to_dict(self) -> DatabaseDict: "targets": targets, } - @beartype def get_target(self, target_id: str) -> Target: """ Return a target from the database with the given ID. @@ -121,7 +120,6 @@ def from_dict(cls, database_dict: DatabaseDict) -> Self: ) @property - @beartype def not_deleted_targets(self) -> set[Target]: """ All targets which have not been deleted. @@ -129,7 +127,6 @@ def not_deleted_targets(self) -> set[Target]: return {target for target in self.targets if not target.delete_date} @property - @beartype def active_targets(self) -> set[Target]: """ All active targets. @@ -142,7 +139,6 @@ def active_targets(self) -> set[Target]: } @property - @beartype def inactive_targets(self) -> set[Target]: """ All inactive targets. @@ -155,7 +151,6 @@ def inactive_targets(self) -> set[Target]: } @property - @beartype def failed_targets(self) -> set[Target]: """ All failed targets. @@ -167,7 +162,6 @@ def failed_targets(self) -> set[Target]: } @property - @beartype def processing_targets(self) -> set[Target]: """ All processing targets. diff --git a/src/mock_vws/image_matchers.py b/src/mock_vws/image_matchers.py index 7b1c834d..5768ad8b 100644 --- a/src/mock_vws/image_matchers.py +++ b/src/mock_vws/image_matchers.py @@ -33,10 +33,10 @@ def __call__( ... # pylint: disable=unnecessary-ellipsis +@beartype class ExactMatcher: """A matcher which returns whether two images are exactly equal.""" - @beartype def __call__( self, first_image_content: bytes, @@ -52,10 +52,10 @@ def __call__( return bool(first_image_content == second_image_content) +@beartype class StructuralSimilarityMatcher: """A matcher which returns whether two images are similar using SSIM.""" - @beartype def __call__( self, first_image_content: bytes, diff --git a/src/mock_vws/states.py b/src/mock_vws/states.py index 53654d67..c3f55b01 100644 --- a/src/mock_vws/states.py +++ b/src/mock_vws/states.py @@ -4,7 +4,10 @@ from enum import StrEnum, auto +from beartype import beartype + +@beartype class States(StrEnum): """ Constants representing various web service states. diff --git a/src/mock_vws/target.py b/src/mock_vws/target.py index d1185bac..67419ec8 100644 --- a/src/mock_vws/target.py +++ b/src/mock_vws/target.py @@ -11,7 +11,7 @@ from typing import Self, TypedDict from zoneinfo import ZoneInfo -from beartype import beartype +from beartype import BeartypeConf, beartype from PIL import Image, ImageStat from mock_vws._constants import TargetStatuses @@ -39,6 +39,7 @@ class TargetDict(TypedDict): tracking_rating: int +@beartype def _random_hex() -> str: """ Return a random hex value. @@ -46,6 +47,7 @@ def _random_hex() -> str: return uuid.uuid4().hex +@beartype def _time_now() -> datetime.datetime: """ Return the current time in the GMT time zone. @@ -54,6 +56,7 @@ def _time_now() -> datetime.datetime: return datetime.datetime.now(tz=gmt) +@beartype(conf=BeartypeConf(is_pep484_tower=True)) @dataclass(frozen=True, eq=True) class Target: """ @@ -78,7 +81,6 @@ class Target: upload_date: datetime.datetime = field(default_factory=_time_now) @property - @beartype def _post_processing_status(self) -> TargetStatuses: """ Return the status of the target, or what it will be when processing is @@ -102,7 +104,6 @@ def _post_processing_status(self) -> TargetStatuses: return TargetStatuses.FAILED @property - @beartype def status(self) -> str: """ Return the status of the target. @@ -128,13 +129,11 @@ def status(self) -> str: return self._post_processing_status.value @property - @beartype def _post_processing_target_rating(self) -> int: """The rating of the target after processing.""" return self.target_tracking_rater(image_content=self.image_value) @property - @beartype def tracking_rating(self) -> int: """ Return the tracking rating of the target recognition image. diff --git a/src/mock_vws/target_manager.py b/src/mock_vws/target_manager.py index 75ed7486..fb8543c2 100644 --- a/src/mock_vws/target_manager.py +++ b/src/mock_vws/target_manager.py @@ -13,14 +13,12 @@ class TargetManager: A target manager as per https://developer.vuforia.com/target-manager. """ - @beartype def __init__(self) -> None: """ Create a target manager with no databases. """ self._databases: set[VuforiaDatabase] = set() - @beartype def remove_database(self, database: VuforiaDatabase) -> None: """ Remove a cloud database. @@ -33,7 +31,6 @@ def remove_database(self, database: VuforiaDatabase) -> None: """ self._databases.remove(database) - @beartype def add_database(self, database: VuforiaDatabase) -> None: """ Add a cloud database. @@ -84,8 +81,6 @@ def add_database(self, database: VuforiaDatabase) -> None: self._databases.add(database) @property - @beartype - @beartype def databases(self) -> set[VuforiaDatabase]: """ All cloud databases. diff --git a/src/mock_vws/target_raters.py b/src/mock_vws/target_raters.py index d0012e36..dc7542f4 100644 --- a/src/mock_vws/target_raters.py +++ b/src/mock_vws/target_raters.py @@ -59,6 +59,7 @@ def __call__(self, image_content: bytes) -> int: ... # pylint: disable=unnecessary-ellipsis +@beartype class RandomTargetTrackingRater: """A rater which returns a random number.""" @@ -73,6 +74,7 @@ def __call__(self, image_content: bytes) -> int: return secrets.randbelow(exclusive_upper_bound=6) +@beartype class HardcodedTargetTrackingRater: """A rater which returns a hardcoded number.""" @@ -94,6 +96,7 @@ def __call__(self, image_content: bytes) -> int: return self._rating +@beartype class BrisqueTargetTrackingRater: """A rater which returns a rating based on a BRISQUE score."""