Skip to content

Commit 61c00ca

Browse files
committed
improved error message handling to use notification pattern instead of fast fail with exception
1 parent 48a3c0f commit 61c00ca

3 files changed

Lines changed: 45 additions & 14 deletions

File tree

src/runloop_api_client/_utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# isort: skip_file
12
from ._sync import asyncify as asyncify
23
from ._proxy import LazyProxy as LazyProxy
34
from ._utils import (
@@ -57,6 +58,7 @@
5758
maybe_transform as maybe_transform,
5859
async_maybe_transform as async_maybe_transform,
5960
)
61+
from ._validation import ValidationNotification as ValidationNotification
6062
from ._reflection import (
6163
function_has_argument as function_has_argument,
6264
assert_signatures_in_sync as assert_signatures_in_sync,

src/runloop_api_client/resources/blueprints.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
# isort: skip_file
23

34
from __future__ import annotations
45

@@ -14,7 +15,7 @@
1415
blueprint_create_from_inspection_params,
1516
)
1617
from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
17-
from .._utils import maybe_transform, async_maybe_transform
18+
from .._utils import maybe_transform, async_maybe_transform, ValidationNotification
1819
from .._compat import cached_property
1920
from .._resource import SyncAPIResource, AsyncAPIResource
2021
from .._response import (
@@ -51,32 +52,56 @@ class BlueprintRequestArgs(TypedDict, total=False):
5152
__all__ = ["BlueprintsResource", "AsyncBlueprintsResource", "BlueprintRequestArgs"]
5253

5354

54-
def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> None:
55-
"""Validate file_mounts are within size constraints.
55+
def _format_bytes(num_bytes: int) -> str:
56+
"""Format a byte count in a human-friendly way (KB/MB/GB).
57+
58+
Uses binary units (1024). Avoids decimals when exact.
59+
"""
60+
if num_bytes < 1024:
61+
return f"{num_bytes} bytes"
62+
for factor, unit in ((1 << 30, "GB"), (1 << 20, "MB"), (1 << 10, "KB")):
63+
if num_bytes >= factor:
64+
value = num_bytes / factor
65+
if float(value).is_integer():
66+
return f"{int(value)} {unit}"
67+
return f"{value:.1f} {unit}"
68+
return f"{num_bytes} bytes"
69+
70+
71+
def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> ValidationNotification:
72+
"""Validate file_mounts are within size constraints: returns validation failures.
5673
5774
Currently enforces a maximum per-file size to avoid server-side issues with
5875
large inline file contents. Also enforces a maximum total size across all
5976
file_mounts.
6077
"""
6178

79+
note = ValidationNotification()
80+
6281
if file_mounts is omit or file_mounts is None:
63-
return
82+
return note
6483

6584
total_size_bytes = 0
6685
for mount_path, content in file_mounts.items():
6786
# Measure size in bytes using UTF-8 encoding since payloads are JSON strings
6887
size_bytes = len(content.encode("utf-8"))
6988
if size_bytes > FILE_MOUNT_MAX_SIZE_BYTES:
70-
raise ValueError(
71-
f"file_mount '{mount_path}' exceeds maximum size of {FILE_MOUNT_MAX_SIZE_BYTES} bytes. Use object_mounts instead."
89+
over = size_bytes - FILE_MOUNT_MAX_SIZE_BYTES
90+
note.add_error(
91+
f"file_mount '{mount_path}' is {_format_bytes(over)} over the limit "
92+
f"({_format_bytes(size_bytes)} / {_format_bytes(FILE_MOUNT_MAX_SIZE_BYTES)}). Use object_mounts instead."
7293
)
7394
total_size_bytes += size_bytes
7495

7596
if total_size_bytes > FILE_MOUNT_TOTAL_MAX_SIZE_BYTES:
76-
raise ValueError(
77-
f"total file_mounts size exceeds maximum of {FILE_MOUNT_TOTAL_MAX_SIZE_BYTES} bytes. Use object_mounts instead."
97+
total_over = total_size_bytes - FILE_MOUNT_TOTAL_MAX_SIZE_BYTES
98+
note.add_error(
99+
f"total file_mounts size is {_format_bytes(total_over)} over the limit "
100+
f"({_format_bytes(total_size_bytes)} / {_format_bytes(FILE_MOUNT_TOTAL_MAX_SIZE_BYTES)}). Use object_mounts instead."
78101
)
79102

103+
return note
104+
80105

81106
class BlueprintsResource(SyncAPIResource):
82107
@cached_property
@@ -172,7 +197,9 @@ def create(
172197
173198
idempotency_key: Specify a custom idempotency key for this request
174199
"""
175-
_validate_file_mounts(file_mounts)
200+
note = _validate_file_mounts(file_mounts)
201+
if note.has_errors():
202+
raise ValueError(note.error_message())
176203

177204
return self._post(
178205
"/v1/blueprints",
@@ -788,7 +815,9 @@ async def create(
788815
789816
idempotency_key: Specify a custom idempotency key for this request
790817
"""
791-
_validate_file_mounts(file_mounts)
818+
note = _validate_file_mounts(file_mounts)
819+
if note.has_errors():
820+
raise ValueError(note.error_message())
792821

793822
return await self._post(
794823
"/v1/blueprints",

tests/api_resources/test_blueprints.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_streaming_response_create(self, client: Runloop) -> None:
112112
def test_create_rejects_large_file_mount(self, client: Runloop) -> None:
113113
# 512KB + 1 byte
114114
too_large_content = "a" * (512 * 1024 + 1)
115-
with pytest.raises(ValueError, match=r"exceeds maximum size"):
115+
with pytest.raises(ValueError, match=r"over the limit"):
116116
client.blueprints.create(
117117
name="name",
118118
file_mounts={"/tmp/large.txt": too_large_content},
@@ -125,7 +125,7 @@ def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None:
125125
content_a = "a" * per_file_max
126126
content_b = "b" * per_file_max
127127
content_c = "c" * 1
128-
with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"):
128+
with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"):
129129
client.blueprints.create(
130130
name="name",
131131
file_mounts={
@@ -567,7 +567,7 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No
567567
async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None:
568568
# 512KB + 1 byte
569569
too_large_content = "a" * (512 * 1024 + 1)
570-
with pytest.raises(ValueError, match=r"exceeds maximum size"):
570+
with pytest.raises(ValueError, match=r"over the limit"):
571571
await async_client.blueprints.create(
572572
name="name",
573573
file_mounts={"/tmp/large.txt": too_large_content},
@@ -580,7 +580,7 @@ async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRun
580580
content_a = "a" * per_file_max
581581
content_b = "b" * per_file_max
582582
content_c = "c" * 1
583-
with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"):
583+
with pytest.raises(ValueError, match=r"total file_mounts size .* over the limit"):
584584
await async_client.blueprints.create(
585585
name="name",
586586
file_mounts={

0 commit comments

Comments
 (0)