Skip to content

Commit 48a3c0f

Browse files
committed
added file_mount validation limits
1 parent 6efc023 commit 48a3c0f

4 files changed

Lines changed: 93 additions & 1 deletion

File tree

src/runloop_api_client/_constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@
1212

1313
INITIAL_RETRY_DELAY = 1.0
1414
MAX_RETRY_DELAY = 60.0
15+
16+
# Maximum allowed size (in bytes) for individual entries in `file_mounts` when creating Blueprints
17+
FILE_MOUNT_MAX_SIZE_BYTES = 512 * 1024
18+
19+
# Maximum allowed total size (in bytes) across all `file_mounts` when creating Blueprints
20+
FILE_MOUNT_TOTAL_MAX_SIZE_BYTES = 1024 * 1024

src/runloop_api_client/resources/blueprints.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
async_to_raw_response_wrapper,
2424
async_to_streamed_response_wrapper,
2525
)
26+
from .._constants import FILE_MOUNT_MAX_SIZE_BYTES, FILE_MOUNT_TOTAL_MAX_SIZE_BYTES
2627
from ..pagination import SyncBlueprintsCursorIDPage, AsyncBlueprintsCursorIDPage
2728
from .._exceptions import RunloopError
2829
from ..lib.polling import PollingConfig, poll_until
@@ -50,6 +51,33 @@ class BlueprintRequestArgs(TypedDict, total=False):
5051
__all__ = ["BlueprintsResource", "AsyncBlueprintsResource", "BlueprintRequestArgs"]
5152

5253

54+
def _validate_file_mounts(file_mounts: Optional[Dict[str, str]] | Omit) -> None:
55+
"""Validate file_mounts are within size constraints.
56+
57+
Currently enforces a maximum per-file size to avoid server-side issues with
58+
large inline file contents. Also enforces a maximum total size across all
59+
file_mounts.
60+
"""
61+
62+
if file_mounts is omit or file_mounts is None:
63+
return
64+
65+
total_size_bytes = 0
66+
for mount_path, content in file_mounts.items():
67+
# Measure size in bytes using UTF-8 encoding since payloads are JSON strings
68+
size_bytes = len(content.encode("utf-8"))
69+
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."
72+
)
73+
total_size_bytes += size_bytes
74+
75+
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."
78+
)
79+
80+
5381
class BlueprintsResource(SyncAPIResource):
5482
@cached_property
5583
def with_raw_response(self) -> BlueprintsResourceWithRawResponse:
@@ -144,6 +172,8 @@ def create(
144172
145173
idempotency_key: Specify a custom idempotency key for this request
146174
"""
175+
_validate_file_mounts(file_mounts)
176+
147177
return self._post(
148178
"/v1/blueprints",
149179
body=maybe_transform(
@@ -758,6 +788,8 @@ async def create(
758788
759789
idempotency_key: Specify a custom idempotency key for this request
760790
"""
791+
_validate_file_mounts(file_mounts)
792+
761793
return await self._post(
762794
"/v1/blueprints",
763795
body=await async_maybe_transform(

tests/api_resources/test_blueprints.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,33 @@ def test_streaming_response_create(self, client: Runloop) -> None:
108108

109109
assert cast(Any, response.is_closed) is True
110110

111+
@parametrize
112+
def test_create_rejects_large_file_mount(self, client: Runloop) -> None:
113+
# 512KB + 1 byte
114+
too_large_content = "a" * (512 * 1024 + 1)
115+
with pytest.raises(ValueError, match=r"exceeds maximum size"):
116+
client.blueprints.create(
117+
name="name",
118+
file_mounts={"/tmp/large.txt": too_large_content},
119+
)
120+
121+
@parametrize
122+
def test_create_rejects_total_file_mount_size(self, client: Runloop) -> None:
123+
# Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total
124+
per_file_max = 512 * 1024
125+
content_a = "a" * per_file_max
126+
content_b = "b" * per_file_max
127+
content_c = "c" * 1
128+
with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"):
129+
client.blueprints.create(
130+
name="name",
131+
file_mounts={
132+
"/tmp/a.txt": content_a,
133+
"/tmp/b.txt": content_b,
134+
"/tmp/c.txt": content_c,
135+
},
136+
)
137+
111138
@parametrize
112139
def test_method_retrieve(self, client: Runloop) -> None:
113140
blueprint = client.blueprints.retrieve(
@@ -536,6 +563,33 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No
536563

537564
assert cast(Any, response.is_closed) is True
538565

566+
@parametrize
567+
async def test_create_rejects_large_file_mount(self, async_client: AsyncRunloop) -> None:
568+
# 512KB + 1 byte
569+
too_large_content = "a" * (512 * 1024 + 1)
570+
with pytest.raises(ValueError, match=r"exceeds maximum size"):
571+
await async_client.blueprints.create(
572+
name="name",
573+
file_mounts={"/tmp/large.txt": too_large_content},
574+
)
575+
576+
@parametrize
577+
async def test_create_rejects_total_file_mount_size(self, async_client: AsyncRunloop) -> None:
578+
# Two files at exactly per-file max, plus 1 extra byte across a third file to exceed 1MB total
579+
per_file_max = 512 * 1024
580+
content_a = "a" * per_file_max
581+
content_b = "b" * per_file_max
582+
content_c = "c" * 1
583+
with pytest.raises(ValueError, match=r"total file_mounts size exceeds maximum"):
584+
await async_client.blueprints.create(
585+
name="name",
586+
file_mounts={
587+
"/tmp/a.txt": content_a,
588+
"/tmp/b.txt": content_b,
589+
"/tmp/c.txt": content_c,
590+
},
591+
)
592+
539593
@parametrize
540594
async def test_method_retrieve(self, async_client: AsyncRunloop) -> None:
541595
blueprint = await async_client.blueprints.retrieve(

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)