Skip to content

Commit 6fb89b7

Browse files
Add signed URL support for storage objects
Expose a storage API method to request signed URLs and add coverage for expiry behavior and error handling so SDK users can fetch temporary download links. Made-with: Cursor
1 parent a932bb8 commit 6fb89b7

4 files changed

Lines changed: 119 additions & 1 deletion

File tree

tests/test_yepcode_storage.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import time
2+
from datetime import datetime, timezone
3+
4+
import pytest
5+
import requests
6+
7+
from yepcode_run import YepCodeStorage
8+
from yepcode_run.api.yepcode_api import YepCodeApiError
9+
10+
11+
TEST_NAME = "test-run-sdk.txt"
12+
TEST_CONTENT = b"hello signed url"
13+
14+
15+
@pytest.fixture
16+
def storage():
17+
return YepCodeStorage()
18+
19+
20+
@pytest.fixture
21+
def uploaded_file(storage):
22+
storage.upload(TEST_NAME, TEST_CONTENT)
23+
yield TEST_NAME
24+
try:
25+
storage.delete(TEST_NAME)
26+
except Exception:
27+
pass
28+
29+
30+
def _parse_iso(value: str) -> float:
31+
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
32+
33+
34+
@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment")
35+
def test_create_signed_url_default_expiry(storage, uploaded_file):
36+
result = storage.create_signed_url(uploaded_file)
37+
38+
assert isinstance(result.url, str) and len(result.url) > 0
39+
assert result.path == uploaded_file
40+
41+
expires_at = _parse_iso(result.expires_at)
42+
expected_expiry = time.time() + 3600
43+
assert abs(expires_at - expected_expiry) < 60
44+
45+
46+
@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment")
47+
def test_create_signed_url_custom_expiry(storage, uploaded_file):
48+
result = storage.create_signed_url(uploaded_file, expires_in_seconds=60)
49+
50+
expires_at = _parse_iso(result.expires_at)
51+
expected_expiry = time.time() + 60
52+
assert abs(expires_at - expected_expiry) < 30
53+
54+
55+
@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment")
56+
def test_create_signed_url_returns_fetchable_url(storage, uploaded_file):
57+
result = storage.create_signed_url(uploaded_file)
58+
59+
response = requests.get(result.url, timeout=30)
60+
assert response.ok
61+
assert response.content == TEST_CONTENT
62+
63+
64+
@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment")
65+
def test_create_signed_url_missing_file_raises_404(storage):
66+
with pytest.raises(YepCodeApiError) as exc_info:
67+
storage.create_signed_url("does-not-exist.txt")
68+
assert exc_info.value.status == 404
69+
70+
71+
@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment")
72+
def test_create_signed_url_out_of_range_expiry_raises_400(storage, uploaded_file):
73+
with pytest.raises(YepCodeApiError) as exc_info:
74+
storage.create_signed_url(uploaded_file, expires_in_seconds=999999)
75+
assert exc_info.value.status == 400

yepcode_run/api/types.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,27 @@ class CreateStorageObjectInput:
470470
file: Any
471471

472472

473+
@dataclass
474+
class CreateSignedUrlInput:
475+
path: str
476+
expires_in_seconds: Optional[int] = None
477+
478+
479+
@dataclass
480+
class SignedUrl:
481+
url: str
482+
path: str
483+
expires_at: str
484+
485+
@staticmethod
486+
def from_dict(data: dict) -> "SignedUrl":
487+
return SignedUrl(
488+
url=data["url"],
489+
path=data["path"],
490+
expires_at=data.get("expiresAt", data.get("expires_at")),
491+
)
492+
493+
473494
# Dependency manifest types
474495
@dataclass
475496
class ProgrammingLanguageManifest:

yepcode_run/api/yepcode_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
ScheduledProcessInput,
4343
CreateStorageObjectInput,
4444
StorageObject,
45+
CreateSignedUrlInput,
46+
SignedUrl,
4547
ProgrammingLanguage,
4648
ProgrammingLanguageManifest,
4749
UpdateTeamDependenciesInput,
@@ -629,3 +631,10 @@ def delete_object(self, name: str) -> None:
629631
response.status_code,
630632
)
631633
return None
634+
635+
def create_signed_url(self, data: CreateSignedUrlInput) -> SignedUrl:
636+
body: Dict[str, Any] = {"path": data.path}
637+
if data.expires_in_seconds is not None:
638+
body["expiresInSeconds"] = data.expires_in_seconds
639+
response = self._request("POST", "/storage/signed-urls", {"data": body})
640+
return SignedUrl.from_dict(response)

yepcode_run/storage/yepcode_storage.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from typing import List, Optional, Dict, Any
22

33
from ..api.api_manager import YepCodeApiManager
4-
from ..api.types import CreateStorageObjectInput, StorageObject, YepCodeApiConfig
4+
from ..api.types import (
5+
CreateSignedUrlInput,
6+
CreateStorageObjectInput,
7+
SignedUrl,
8+
StorageObject,
9+
YepCodeApiConfig,
10+
)
511

612

713
class YepCodeStorage:
@@ -27,3 +33,10 @@ def delete(self, name: str) -> None:
2733

2834
def list(self, **kwargs) -> List[StorageObject]:
2935
return self._api.get_objects(kwargs if kwargs else None)
36+
37+
def create_signed_url(
38+
self, name: str, expires_in_seconds: Optional[int] = None
39+
) -> SignedUrl:
40+
return self._api.create_signed_url(
41+
CreateSignedUrlInput(path=name, expires_in_seconds=expires_in_seconds)
42+
)

0 commit comments

Comments
 (0)