Skip to content

Commit 9eed732

Browse files
committed
Consolidate request JSON validation
1 parent ab37ab2 commit 9eed732

10 files changed

Lines changed: 273 additions & 87 deletions

File tree

src/mock_vws/_flask_server/target_manager.py

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from collections.abc import Callable, Mapping, Sequence
88
from enum import Enum, StrEnum, auto
99
from http import HTTPMethod, HTTPStatus
10-
from typing import Annotated, TypeIs, assert_never
10+
from typing import Annotated, NotRequired, TypedDict, TypeIs, assert_never
1111
from zoneinfo import ZoneInfo
1212

1313
from beartype import beartype
@@ -16,6 +16,7 @@
1616
BaseModel,
1717
BeforeValidator,
1818
ConfigDict,
19+
TypeAdapter,
1920
ValidationError,
2021
model_validator,
2122
)
@@ -244,6 +245,59 @@ def to_vumark_database(self) -> VuMarkDatabase:
244245
)
245246

246247

248+
class OAuth2ClientCredentialBody(BaseModel):
249+
"""A request to register an OAuth2 client credential."""
250+
251+
model_config = ConfigDict(strict=True, extra="forbid")
252+
253+
client_id: str
254+
client_secret: str
255+
scopes: tuple[str, ...]
256+
257+
258+
class ImageTargetBody(TypedDict):
259+
"""An image target sent to the storage service."""
260+
261+
name: str
262+
width: float
263+
image_base64: str
264+
active_flag: bool
265+
processing_time_seconds: float
266+
application_metadata: str | None
267+
target_id: str
268+
last_modified_date: NotRequired[str]
269+
delete_date_optional: NotRequired[str | None]
270+
upload_date: NotRequired[str]
271+
tracking_rating: NotRequired[int]
272+
current_month_recos: NotRequired[int]
273+
previous_month_recos: NotRequired[int]
274+
total_recos: NotRequired[int]
275+
reco_rating: NotRequired[str]
276+
277+
278+
class ImageTargetUpdateBody(TypedDict):
279+
"""Fields which may update a stored image target."""
280+
281+
name: NotRequired[str]
282+
width: NotRequired[float]
283+
active_flag: NotRequired[bool]
284+
application_metadata: NotRequired[str | None]
285+
image: NotRequired[str]
286+
287+
288+
class RecognitionCountsBody(TypedDict):
289+
"""Recognition counts to update on a stored target."""
290+
291+
current_month_recos: NotRequired[int]
292+
previous_month_recos: NotRequired[int]
293+
total_recos: NotRequired[int]
294+
295+
296+
_IMAGE_TARGET_ADAPTER = TypeAdapter(type=ImageTargetBody)
297+
_IMAGE_TARGET_UPDATE_ADAPTER = TypeAdapter(type=ImageTargetUpdateBody)
298+
_RECOGNITION_COUNTS_ADAPTER = TypeAdapter(type=RecognitionCountsBody)
299+
300+
247301
@beartype
248302
class _InvalidRequestBodyError(Exception):
249303
"""A request body which cannot be used to create a resource.
@@ -685,11 +739,13 @@ def get_oauth2_client_credentials() -> Response:
685739
@beartype
686740
def put_oauth2_client_credential() -> Response:
687741
"""Add or replace an OAuth2 client credential."""
688-
value = json.loads(s=request.data)
742+
value = OAuth2ClientCredentialBody.model_validate_json(
743+
json_data=request.data,
744+
)
689745
credential = OAuth2ClientCredential(
690-
client_id=value["client_id"], # pyrefly: ignore [unknown-argument-type]
691-
client_secret=value["client_secret"], # pyrefly: ignore [unknown-argument-type]
692-
scopes=tuple(value["scopes"]), # pyrefly: ignore [unknown-argument-type]
746+
client_id=value.client_id,
747+
client_secret=value.client_secret,
748+
scopes=value.scopes,
693749
)
694750
TARGET_MANAGER.add_oauth2_client_credential(credential=credential)
695751
return Response(response="", status=HTTPStatus.NO_CONTENT)
@@ -719,19 +775,22 @@ def create_target(database_name: str) -> Response:
719775
:status 201: The target has been created.
720776
:status 404: There is no cloud database with the given name.
721777
"""
722-
request_json = json.loads(s=request.data)
778+
request_body = _IMAGE_TARGET_ADAPTER.validate_json(
779+
request.data,
780+
strict=True,
781+
)
723782
settings = TargetManagerSettings.model_validate(obj={})
724783

725-
image_bytes = base64.b64decode(s=request_json["image_base64"]) # pyrefly: ignore [unknown-argument-type]
784+
image_bytes = base64.b64decode(s=request_body["image_base64"])
726785
target_tracking_rater = settings.target_rater.to_target_rater()
727786
target = ImageTarget(
728-
name=request_json["name"], # pyrefly: ignore [unknown-argument-type]
729-
width=request_json["width"], # pyrefly: ignore [unknown-argument-type]
787+
name=request_body["name"],
788+
width=request_body["width"],
730789
image_value=image_bytes,
731-
active_flag=request_json["active_flag"], # pyrefly: ignore [unknown-argument-type]
732-
processing_time_seconds=request_json["processing_time_seconds"], # pyrefly: ignore [unknown-argument-type]
733-
application_metadata=request_json["application_metadata"], # pyrefly: ignore [unknown-argument-type]
734-
target_id=request_json["target_id"], # pyrefly: ignore [unknown-argument-type]
790+
active_flag=request_body["active_flag"],
791+
processing_time_seconds=request_body["processing_time_seconds"],
792+
application_metadata=request_body["application_metadata"],
793+
target_id=request_body["target_id"],
735794
target_tracking_rater=target_tracking_rater,
736795
)
737796
with TARGET_MANAGER.lock:
@@ -807,7 +866,10 @@ def delete_target(database_name: str, target_id: str) -> Response:
807866
@beartype
808867
def update_target(database_name: str, target_id: str) -> Response:
809868
"""Update a target."""
810-
request_json = json.loads(s=request.data)
869+
request_body = _IMAGE_TARGET_UPDATE_ADAPTER.validate_json(
870+
request.data,
871+
strict=True,
872+
)
811873

812874
with TARGET_MANAGER.lock:
813875
database = _find_cloud_database(database_name=database_name)
@@ -816,26 +878,25 @@ def update_target(database_name: str, target_id: str) -> Response:
816878

817879
target = database.get_target(target_id=target_id)
818880

819-
name = request_json.get("name", target.name) # pyrefly: ignore [unknown-variable-type]
820-
active_flag = request_json.get("active_flag", target.active_flag) # pyrefly: ignore [unknown-variable-type]
881+
name = request_body.get("name", target.name)
882+
active_flag = request_body.get("active_flag", target.active_flag)
821883

822884
gmt = ZoneInfo(key="GMT")
823885
last_modified_date = datetime.datetime.now(tz=gmt)
824886

825-
width = request_json.get("width", target.width) # pyrefly: ignore [unknown-variable-type]
826-
application_metadata = request_json.get( # pyrefly: ignore [unknown-variable-type]
827-
"application_metadata",
828-
target.application_metadata,
887+
width = request_body.get("width", target.width)
888+
application_metadata = request_body.get(
889+
"application_metadata", target.application_metadata
829890
)
830891
image_value = target.image_value
831-
if "image" in request_json:
832-
image_value = base64.b64decode(s=request_json["image"]) # pyrefly: ignore [unknown-argument-type]
892+
if "image" in request_body:
893+
image_value = base64.b64decode(s=request_body["image"])
833894
new_target = copy.replace(
834895
target,
835-
name=name, # pyrefly: ignore [unknown-argument-type]
836-
width=width, # pyrefly: ignore [unknown-argument-type]
837-
active_flag=active_flag, # pyrefly: ignore [unknown-argument-type]
838-
application_metadata=application_metadata, # pyrefly: ignore [unknown-argument-type]
896+
name=name,
897+
width=width,
898+
active_flag=active_flag,
899+
application_metadata=application_metadata,
839900
image_value=image_value,
840901
last_modified_date=last_modified_date,
841902
)
@@ -880,7 +941,10 @@ def set_target_recognition_counts(
880941
881942
:status 200: The recognition counts have been set.
882943
"""
883-
request_json = json.loads(s=request.data)
944+
request_body = _RECOGNITION_COUNTS_ADAPTER.validate_json(
945+
request.data,
946+
strict=True,
947+
)
884948

885949
with TARGET_MANAGER.lock:
886950
database = _find_cloud_database(database_name=database_name)
@@ -891,15 +955,13 @@ def set_target_recognition_counts(
891955

892956
new_target = copy.replace(
893957
target,
894-
current_month_recos=request_json.get( # pyrefly: ignore [unknown-argument-type]
895-
"current_month_recos",
896-
target.current_month_recos,
958+
current_month_recos=request_body.get(
959+
"current_month_recos", target.current_month_recos
897960
),
898-
previous_month_recos=request_json.get( # pyrefly: ignore [unknown-argument-type]
899-
"previous_month_recos",
900-
target.previous_month_recos,
961+
previous_month_recos=request_body.get(
962+
"previous_month_recos", target.previous_month_recos
901963
),
902-
total_recos=request_json.get("total_recos", target.total_recos), # pyrefly: ignore [unknown-argument-type]
964+
total_recos=request_body.get("total_recos", target.total_recos),
903965
)
904966

905967
database.targets.remove(target)

src/mock_vws/_flask_server/vws.py

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
import email.utils
99
import gzip
1010
import html
11-
import json
1211
import logging
1312
import threading
1413
import time
1514
import uuid
1615
from enum import StrEnum, auto
1716
from http import HTTPMethod, HTTPStatus
18-
from typing import assert_never
17+
from typing import NotRequired, TypedDict, assert_never
1918

2019
import requests
2120
from beartype import beartype
2221
from flask import Flask, Response, request
22+
from pydantic import TypeAdapter
2323
from pydantic_settings import BaseSettings
2424
from werkzeug.exceptions import MethodNotAllowed, NotFound
2525

@@ -100,6 +100,30 @@
100100
_LOGGER = logging.getLogger(name=__name__)
101101

102102

103+
class _AddTargetBody(TypedDict):
104+
"""A validated add-target request body."""
105+
106+
name: str
107+
width: float
108+
image: str
109+
active_flag: NotRequired[bool | None]
110+
application_metadata: NotRequired[str | None]
111+
112+
113+
class _UpdateTargetBody(TypedDict):
114+
"""A validated update-target request body."""
115+
116+
width: NotRequired[float]
117+
active_flag: NotRequired[bool | None]
118+
application_metadata: NotRequired[str | None]
119+
name: NotRequired[str]
120+
image: NotRequired[str]
121+
122+
123+
_ADD_TARGET_BODY_ADAPTER = TypeAdapter(type=_AddTargetBody)
124+
_UPDATE_TARGET_BODY_ADAPTER = TypeAdapter(type=_UpdateTargetBody)
125+
126+
103127
@beartype
104128
class _ImageMatcherChoice(StrEnum):
105129
"""Image matcher choices."""
@@ -750,22 +774,25 @@ def add_target() -> Response:
750774

751775
# We do not use ``request.get_json(force=True)`` because this only works
752776
# when the content type is given as ``application/json``.
753-
request_json = json.loads(s=request.data)
754-
name = request_json["name"] # pyrefly: ignore [unknown-variable-type]
755-
active_flag = request_json.get("active_flag") # pyrefly: ignore [unknown-variable-type]
777+
request_json = _ADD_TARGET_BODY_ADAPTER.validate_json(
778+
request.data,
779+
strict=True,
780+
)
781+
name = request_json["name"]
782+
active_flag = request_json.get("active_flag")
756783
if active_flag is None:
757784
active_flag = True
758785

759786
# This rater is not used.
760787
target_tracking_rater = HardcodedTargetTrackingRater(rating=1)
761788

762789
new_target = ImageTarget(
763-
name=name, # pyrefly: ignore [unknown-argument-type]
764-
width=request_json["width"], # pyrefly: ignore [unknown-argument-type]
765-
image_value=base64.b64decode(s=request_json["image"]), # pyrefly: ignore [unknown-argument-type]
766-
active_flag=active_flag, # pyrefly: ignore [unknown-argument-type]
790+
name=name,
791+
width=request_json["width"],
792+
image_value=base64.b64decode(s=request_json["image"]),
793+
active_flag=active_flag,
767794
processing_time_seconds=settings.processing_time_seconds,
768-
application_metadata=request_json.get("application_metadata"), # pyrefly: ignore [unknown-argument-type]
795+
application_metadata=request_json.get("application_metadata"),
769796
target_tracking_rater=target_tracking_rater,
770797
)
771798

@@ -1220,7 +1247,10 @@ def update_target(target_id: str) -> Response:
12201247
settings = VWSSettings.model_validate(obj={})
12211248
# We do not use ``request.get_json(force=True)`` because this only works
12221249
# when the content type is given as ``application/json``.
1223-
request_json = json.loads(s=request.data)
1250+
request_json = _UPDATE_TARGET_BODY_ADAPTER.validate_json(
1251+
request.data,
1252+
strict=True,
1253+
)
12241254
databases = get_all_cloud_databases()
12251255
database = get_database_matching_server_keys(
12261256
request_headers=dict(request.headers),
@@ -1239,10 +1269,10 @@ def update_target(target_id: str) -> Response:
12391269

12401270
update_values: dict[str, str | int | float | bool | None] = {}
12411271
if "width" in request_json:
1242-
update_values["width"] = request_json["width"] # pyrefly: ignore [unknown-argument-type]
1272+
update_values["width"] = request_json["width"]
12431273

12441274
if "active_flag" in request_json:
1245-
active_flag = request_json["active_flag"] # pyrefly: ignore [unknown-variable-type]
1275+
active_flag = request_json["active_flag"]
12461276
if active_flag is None:
12471277
_LOGGER.warning(
12481278
msg=(
@@ -1251,10 +1281,10 @@ def update_target(target_id: str) -> Response:
12511281
),
12521282
)
12531283
raise FailError(status_code=HTTPStatus.BAD_REQUEST)
1254-
update_values["active_flag"] = active_flag # pyrefly: ignore [unknown-argument-type]
1284+
update_values["active_flag"] = active_flag
12551285

12561286
if "application_metadata" in request_json:
1257-
application_metadata = request_json["application_metadata"] # pyrefly: ignore [unknown-variable-type]
1287+
application_metadata = request_json["application_metadata"]
12581288
if application_metadata is None:
12591289
_LOGGER.warning(
12601290
msg=(
@@ -1263,15 +1293,15 @@ def update_target(target_id: str) -> Response:
12631293
),
12641294
)
12651295
raise FailError(status_code=HTTPStatus.BAD_REQUEST)
1266-
update_values["application_metadata"] = application_metadata # pyrefly: ignore [unknown-argument-type]
1296+
update_values["application_metadata"] = application_metadata
12671297

12681298
if "name" in request_json:
1269-
name = request_json["name"] # pyrefly: ignore [unknown-variable-type]
1270-
update_values["name"] = name # pyrefly: ignore [unknown-argument-type]
1299+
name = request_json["name"]
1300+
update_values["name"] = name
12711301

12721302
if "image" in request_json:
1273-
image = request_json["image"] # pyrefly: ignore [unknown-variable-type]
1274-
update_values["image"] = image # pyrefly: ignore [unknown-argument-type]
1303+
image = request_json["image"]
1304+
update_values["image"] = image
12751305

12761306
put_url = (
12771307
f"{settings.target_manager_base_url}/cloud_databases/"

0 commit comments

Comments
 (0)