Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions docs/source/mock-api-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ API Reference
:undoc-members:
:exclude-members: to_dict, get_target, from_dict, not_deleted_targets, active_targets, inactive_targets, failed_targets, processing_targets

.. autoclass:: mock_vws.database.VuMarkDatabase
:members:
:undoc-members:
:exclude-members: to_dict, from_dict, not_deleted_targets

.. autoenum:: mock_vws.states.States
:members:
:undoc-members:

.. autoclass:: mock_vws.target.ImageTarget

.. autoclass:: mock_vws.target.VuMarkTarget

Image matchers
--------------

Expand Down
10 changes: 6 additions & 4 deletions src/mock_vws/_database_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from beartype import beartype
from vws_auth_tools import authorization_header

from mock_vws.database import CloudDatabase
from mock_vws.database import CloudDatabase, VuMarkDatabase

AnyDatabase = CloudDatabase | VuMarkDatabase


@beartype
Expand Down Expand Up @@ -58,14 +60,14 @@ def get_database_matching_client_keys(


@beartype
def get_database_matching_server_keys(
def get_database_matching_server_keys[DatabaseT: AnyDatabase](
*,
request_headers: Mapping[str, str],
request_body: bytes | None,
request_method: str,
request_path: str,
databases: Iterable[CloudDatabase],
) -> CloudDatabase:
databases: Iterable[DatabaseT],
) -> DatabaseT:
"""Return the first of the given databases which is being accessed by
the
given server request.
Expand Down
102 changes: 100 additions & 2 deletions src/mock_vws/_flask_server/target_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from flask import Flask, Response, request
from pydantic_settings import BaseSettings

from mock_vws.database import CloudDatabase
from mock_vws.database import CloudDatabase, VuMarkDatabase
from mock_vws.states import States
from mock_vws.target import ImageTarget
from mock_vws.target import ImageTarget, VuMarkTarget
from mock_vws.target_manager import TargetManager
from mock_vws.target_raters import (
BrisqueTargetTrackingRater,
Expand Down Expand Up @@ -80,6 +80,25 @@ def delete_cloud_database(database_name: str) -> Response:
return Response(response="", status=HTTPStatus.OK)


@TARGET_MANAGER_FLASK_APP.route(
rule="/vumark_databases/<string:database_name>",
methods=[HTTPMethod.DELETE],
)
@beartype
def delete_vumark_database(database_name: str) -> Response:
"""Delete a VuMark database.

:status 200: The VuMark database has been deleted.
"""
(matching_database,) = {
database
for database in TARGET_MANAGER.vumark_databases
if database_name == database.database_name
}
TARGET_MANAGER.remove_vumark_database(vumark_database=matching_database)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling in VuMark database deletion endpoint

Medium Severity

The delete_vumark_database endpoint lacks the try/except ValueError error handling that delete_cloud_database has. When no matching VuMark database exists for the given name, the set unpacking (matching_database,) = {...} raises an unhandled ValueError, resulting in a 500 error instead of the intended 404 response.

Fix in Cursor Fix in Web

return Response(response="", status=HTTPStatus.OK)


@TARGET_MANAGER_FLASK_APP.route(
rule="/cloud_databases", methods=[HTTPMethod.GET]
)
Expand All @@ -95,6 +114,22 @@ def get_cloud_databases() -> Response:
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/vumark_databases",
methods=[HTTPMethod.GET],
)
@beartype
def get_vumark_databases() -> Response:
"""Return a list of all VuMark databases."""
databases = [
database.to_dict() for database in TARGET_MANAGER.vumark_databases
]
return Response(
response=json.dumps(obj=databases),
status=HTTPStatus.OK,
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/cloud_databases", methods=[HTTPMethod.POST]
)
Expand Down Expand Up @@ -194,6 +229,47 @@ def create_cloud_database() -> Response:
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/vumark_databases",
methods=[HTTPMethod.POST],
)
@beartype
def create_vumark_database() -> Response:
"""Create a new VuMark database.

:status 201: The database has been successfully created.
"""
request_json = json.loads(s=request.data)
random_vumark_database = VuMarkDatabase()
database = VuMarkDatabase(
server_access_key=request_json.get(
"server_access_key",
random_vumark_database.server_access_key,
),
server_secret_key=request_json.get(
"server_secret_key",
random_vumark_database.server_secret_key,
),
database_name=request_json.get(
"database_name",
random_vumark_database.database_name,
),
)

try:
TARGET_MANAGER.add_vumark_database(vumark_database=database)
except ValueError as exc:
return Response(
response=str(object=exc),
status=HTTPStatus.CONFLICT,
)

return Response(
response=json.dumps(obj=database.to_dict()),
status=HTTPStatus.CREATED,
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/cloud_databases/<string:database_name>/targets",
methods=[HTTPMethod.POST],
Expand Down Expand Up @@ -230,6 +306,28 @@ def create_target(database_name: str) -> Response:
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/vumark_databases/<string:database_name>/vumark_targets",
methods=[HTTPMethod.POST],
)
@beartype
def create_vumark_target(database_name: str) -> Response:
"""Create a new VuMark target in a given database."""
(database,) = (
database
for database in TARGET_MANAGER.vumark_databases
if database.database_name == database_name
)
request_json = json.loads(s=request.data)
target = VuMarkTarget.from_dict(target_dict=request_json)
database.vumark_targets.add(target)

return Response(
response=json.dumps(obj=target.to_dict()),
status=HTTPStatus.CREATED,
)


@TARGET_MANAGER_FLASK_APP.route(
rule="/cloud_databases/<string:database_name>/targets/<string:target_id>",
methods={HTTPMethod.DELETE},
Expand Down
42 changes: 38 additions & 4 deletions src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
TargetStatusProcessingError,
ValidatorError,
)
from mock_vws.database import CloudDatabase
from mock_vws.database import CloudDatabase, VuMarkDatabase
from mock_vws.image_matchers import (
ExactMatcher,
ImageMatcher,
Expand Down Expand Up @@ -100,6 +100,21 @@ def get_all_cloud_databases() -> set[CloudDatabase]:
}


@beartype
def get_all_vumark_databases() -> set[VuMarkDatabase]:
"""Get all VuMark database objects from the task manager back-end."""
settings = VWSSettings.model_validate(obj={})
timeout_seconds = 30
response = requests.get(
url=f"{settings.target_manager_base_url}/vumark_databases",
timeout=timeout_seconds,
)
return {
VuMarkDatabase.from_dict(database_dict=database_dict)
for database_dict in response.json()
}


@VWS_FLASK_APP.before_request
def set_terminate_wsgi_input() -> None:
"""We set ``wsgi.input_terminated`` to ``True`` when going through
Expand Down Expand Up @@ -129,14 +144,19 @@ def set_terminate_wsgi_input() -> None:
@VWS_FLASK_APP.before_request
@beartype
def validate_request() -> None:
"""Run validators on the request."""
databases = get_all_cloud_databases()
"""Run validators on the request.

The VuMark endpoint does its own validation because it needs to
authenticate against both cloud and VuMark databases.
"""
if request.endpoint == "generate_vumark_instance":
return
run_services_validators(
request_headers=dict(request.headers),
request_body=request.data,
request_method=request.method,
request_path=request.path,
databases=databases,
databases=get_all_cloud_databases(),
)


Expand Down Expand Up @@ -357,6 +377,20 @@ def generate_vumark_instance(target_id: str) -> Response:
Fake implementation of
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#generate-instance
"""
cloud_databases = get_all_cloud_databases()
vumark_databases = get_all_vumark_databases()
all_databases: list[CloudDatabase | VuMarkDatabase] = [
*cloud_databases,
*vumark_databases,
]
run_services_validators(
request_headers=dict(request.headers),
request_body=request.data,
request_method=request.method,
request_path=request.path,
databases=all_databases,
)

# ``target_id`` is validated by request validators.
del target_id

Expand Down
16 changes: 15 additions & 1 deletion src/mock_vws/_requests_mock_server/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from requests import PreparedRequest
from responses import RequestsMock

from mock_vws.database import CloudDatabase
from mock_vws.database import CloudDatabase, VuMarkDatabase
from mock_vws.image_matchers import (
ImageMatcher,
StructuralSimilarityMatcher,
Expand Down Expand Up @@ -140,6 +140,20 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None:
cloud_database=cloud_database,
)

def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None:
"""Add a VuMark database.

Args:
vumark_database: The VuMark database to add.

Raises:
ValueError: One of the given database keys matches a key for
an existing database.
"""
self._target_manager.add_vumark_database(
vumark_database=vumark_database,
)

@staticmethod
def _wrap_callback(
callback: _Callback,
Expand Down
11 changes: 9 additions & 2 deletions src/mock_vws/_requests_mock_server/mock_web_services_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import uuid
from collections.abc import Callable, Iterable, Mapping
from http import HTTPMethod, HTTPStatus
from typing import Any, ParamSpec, Protocol, runtime_checkable
from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, runtime_checkable
from zoneinfo import ZoneInfo

from beartype import BeartypeConf, beartype
Expand Down Expand Up @@ -41,6 +41,9 @@
from mock_vws.target_manager import TargetManager
from mock_vws.target_raters import TargetTrackingRater

if TYPE_CHECKING:
from mock_vws.database import CloudDatabase, VuMarkDatabase

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


Expand Down Expand Up @@ -309,12 +312,16 @@ def generate_vumark_instance(
"application/pdf": VUMARK_PDF,
}
try:
all_databases: list[CloudDatabase | VuMarkDatabase] = [
*self._target_manager.cloud_databases,
*self._target_manager.vumark_databases,
]
run_services_validators(
request_headers=request.headers,
request_body=_body_bytes(request=request),
request_method=request.method or "",
request_path=request.path_url,
databases=self._target_manager.cloud_databases,
databases=all_databases,
)

accept = dict(request.headers).get("Accept", "")
Expand Down
4 changes: 2 additions & 2 deletions src/mock_vws/_services_validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Iterable, Mapping

from mock_vws.database import CloudDatabase
from mock_vws._database_matchers import AnyDatabase

from .active_flag_validators import validate_active_flag
from .auth_validators import (
Expand Down Expand Up @@ -55,7 +55,7 @@ def run_services_validators(
request_headers: Mapping[str, str],
request_body: bytes,
request_method: str,
databases: Iterable[CloudDatabase],
databases: Iterable[AnyDatabase],
) -> None:
"""Run all validators.

Expand Down
10 changes: 6 additions & 4 deletions src/mock_vws/_services_validators/auth_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

from beartype import beartype

from mock_vws._database_matchers import get_database_matching_server_keys
from mock_vws._database_matchers import (
AnyDatabase,
get_database_matching_server_keys,
)
from mock_vws._services_validators.exceptions import (
AuthenticationFailureError,
FailError,
)
from mock_vws.database import CloudDatabase

_LOGGER = logging.getLogger(name=__name__)

Expand All @@ -36,7 +38,7 @@ def validate_auth_header_exists(*, request_headers: Mapping[str, str]) -> None:
def validate_access_key_exists(
*,
request_headers: Mapping[str, str],
databases: Iterable[CloudDatabase],
databases: Iterable[AnyDatabase],
) -> None:
"""Validate the authorization header includes an access key for a
database.
Expand Down Expand Up @@ -92,7 +94,7 @@ def validate_authorization(
request_headers: Mapping[str, str],
request_body: bytes,
request_method: str,
databases: Iterable[CloudDatabase],
databases: Iterable[AnyDatabase],
) -> None:
"""Validate the authorization header given to a VWS endpoint.

Expand Down
Loading