Skip to content

Commit 38f577e

Browse files
adamtheturtleclaude
andcommitted
Extract multipart parsing helper; revert retry changes to separate issue
- Extract repeated multipart parsing logic in image_validators.py into _parse_multipart_files helper, eliminating four copies of the same six-line pattern - Revert _delete_target_when_processed retry changes from vuforia_backends.py; tracked separately as #2955 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a0df2cb commit 38f577e

File tree

2 files changed

+43
-64
lines changed

2 files changed

+43
-64
lines changed

src/mock_vws/_query_validators/image_validators.py

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from beartype import beartype
99
from PIL import Image
10+
from werkzeug.datastructures import FileStorage, MultiDict
1011
from werkzeug.formparser import MultiPartParser
1112

1213
from mock_vws._query_validators.exceptions import (
@@ -18,20 +19,19 @@
1819
_LOGGER = logging.getLogger(name=__name__)
1920

2021

21-
@beartype
22-
def validate_image_field_given(
22+
def _parse_multipart_files(
2323
*,
2424
request_headers: Mapping[str, str],
2525
request_body: bytes,
26-
) -> None:
27-
"""Validate that the image field is given.
26+
) -> MultiDict[str, FileStorage]:
27+
"""Parse the multipart body and return the files section.
2828
2929
Args:
3030
request_headers: The headers sent with the request.
3131
request_body: The body of the request.
3232
33-
Raises:
34-
ImageNotGivenError: The image field is not given.
33+
Returns:
34+
The files parsed from the multipart body.
3535
"""
3636
email_message = EmailMessage()
3737
email_message["Content-Type"] = request_headers["Content-Type"]
@@ -42,6 +42,28 @@ def validate_image_field_given(
4242
boundary=boundary.encode(encoding="utf-8"),
4343
content_length=len(request_body),
4444
)
45+
return files
46+
47+
48+
@beartype
49+
def validate_image_field_given(
50+
*,
51+
request_headers: Mapping[str, str],
52+
request_body: bytes,
53+
) -> None:
54+
"""Validate that the image field is given.
55+
56+
Args:
57+
request_headers: The headers sent with the request.
58+
request_body: The body of the request.
59+
60+
Raises:
61+
ImageNotGivenError: The image field is not given.
62+
"""
63+
files = _parse_multipart_files(
64+
request_headers=request_headers,
65+
request_body=request_body,
66+
)
4567
if files.get(key="image") is not None:
4668
return
4769

@@ -64,14 +86,9 @@ def validate_image_file_size(
6486
Raises:
6587
RequestEntityTooLargeError: The image file size is too large.
6688
"""
67-
email_message = EmailMessage()
68-
email_message["Content-Type"] = request_headers["Content-Type"]
69-
boundary = email_message.get_boundary(failobj="")
70-
parser = MultiPartParser()
71-
_, files = parser.parse(
72-
stream=io.BytesIO(initial_bytes=request_body),
73-
boundary=boundary.encode(encoding="utf-8"),
74-
content_length=len(request_body),
89+
files = _parse_multipart_files(
90+
request_headers=request_headers,
91+
request_body=request_body,
7592
)
7693
image_part = files["image"]
7794
image_value = image_part.stream.read()
@@ -105,14 +122,9 @@ def validate_image_dimensions(
105122
BadImageError: The image is given and is not within the maximum width
106123
and height limits.
107124
"""
108-
email_message = EmailMessage()
109-
email_message["Content-Type"] = request_headers["Content-Type"]
110-
boundary = email_message.get_boundary(failobj="")
111-
parser = MultiPartParser()
112-
_, files = parser.parse(
113-
stream=io.BytesIO(initial_bytes=request_body),
114-
boundary=boundary.encode(encoding="utf-8"),
115-
content_length=len(request_body),
125+
files = _parse_multipart_files(
126+
request_headers=request_headers,
127+
request_body=request_body,
116128
)
117129
image_part = files["image"]
118130
image_value = image_part.stream.read()
@@ -142,14 +154,9 @@ def validate_image_format(
142154
Raises:
143155
BadImageError: The image is given and is not either a PNG or a JPEG.
144156
"""
145-
email_message = EmailMessage()
146-
email_message["Content-Type"] = request_headers["Content-Type"]
147-
boundary = email_message.get_boundary(failobj="")
148-
parser = MultiPartParser()
149-
_, files = parser.parse(
150-
stream=io.BytesIO(initial_bytes=request_body),
151-
boundary=boundary.encode(encoding="utf-8"),
152-
content_length=len(request_body),
157+
files = _parse_multipart_files(
158+
request_headers=request_headers,
159+
request_body=request_body,
153160
)
154161
image_part = files["image"]
155162
pil_image = Image.open(fp=image_part.stream)
@@ -175,17 +182,11 @@ def validate_image_is_image(
175182
Raises:
176183
BadImageError: Image data is given and it is not an image file.
177184
"""
178-
email_message = EmailMessage()
179-
email_message["Content-Type"] = request_headers["Content-Type"]
180-
boundary = email_message.get_boundary(failobj="")
181-
parser = MultiPartParser()
182-
_, files = parser.parse(
183-
stream=io.BytesIO(initial_bytes=request_body),
184-
boundary=boundary.encode(encoding="utf-8"),
185-
content_length=len(request_body),
185+
files = _parse_multipart_files(
186+
request_headers=request_headers,
187+
request_body=request_body,
186188
)
187-
image_part = files["image"]
188-
image_file = image_part.stream
189+
image_file = files["image"].stream
189190

190191
try:
191192
Image.open(fp=image_file)

tests/mock_vws/fixtures/vuforia_backends.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@
1010
import responses
1111
from beartype import beartype
1212
from requests_mock_flask import add_flask_app_to_mock
13-
from tenacity import retry
14-
from tenacity.retry import retry_if_exception_type
15-
from tenacity.wait import wait_fixed
1613
from vws import VWS
1714
from vws.exceptions.vws_exceptions import (
1815
TargetStatusNotSuccessError,
19-
TargetStatusProcessingError,
2016
)
2117

2218
from mock_vws import MockVWS
@@ -35,25 +31,6 @@
3531
LOGGER.setLevel(level=logging.DEBUG)
3632

3733

38-
@retry(
39-
retry=retry_if_exception_type(exception_types=TargetStatusProcessingError),
40-
wait=wait_fixed(wait=2),
41-
reraise=True,
42-
)
43-
def _delete_target_when_processed(*, vws_client: VWS, target_id: str) -> None:
44-
"""Wait for a target to finish processing, then delete it.
45-
46-
Retries if Vuforia briefly returns a processing state immediately
47-
after the prior wait completes (race condition after update_target).
48-
49-
Args:
50-
vws_client: The VWS client to use.
51-
target_id: The target to delete.
52-
"""
53-
vws_client.wait_for_target_processed(target_id=target_id)
54-
vws_client.delete_target(target_id=target_id)
55-
56-
5734
@RETRY_ON_TOO_MANY_REQUESTS
5835
def _delete_all_targets(*, database_keys: VuforiaDatabase) -> None:
5936
"""Delete all targets.
@@ -80,7 +57,8 @@ def _delete_all_targets(*, database_keys: VuforiaDatabase) -> None:
8057
# we change the target to inactive before deleting it.
8158
with contextlib.suppress(TargetStatusNotSuccessError):
8259
vws_client.update_target(target_id=target, active_flag=False)
83-
_delete_target_when_processed(vws_client=vws_client, target_id=target)
60+
vws_client.wait_for_target_processed(target_id=target)
61+
vws_client.delete_target(target_id=target)
8462

8563

8664
@beartype

0 commit comments

Comments
 (0)