From 6fb89b7c9e827b84ef3ecfa57c392d0c1a819576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Mui=CC=81n=CC=83o=20Garci=CC=81a?= Date: Tue, 28 Apr 2026 13:46:05 +0200 Subject: [PATCH 1/2] 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 --- tests/test_yepcode_storage.py | 75 ++++++++++++++++++++++++++ yepcode_run/api/types.py | 21 ++++++++ yepcode_run/api/yepcode_api.py | 9 ++++ yepcode_run/storage/yepcode_storage.py | 15 +++++- 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/test_yepcode_storage.py diff --git a/tests/test_yepcode_storage.py b/tests/test_yepcode_storage.py new file mode 100644 index 0000000..f07e8cc --- /dev/null +++ b/tests/test_yepcode_storage.py @@ -0,0 +1,75 @@ +import time +from datetime import datetime, timezone + +import pytest +import requests + +from yepcode_run import YepCodeStorage +from yepcode_run.api.yepcode_api import YepCodeApiError + + +TEST_NAME = "test-run-sdk.txt" +TEST_CONTENT = b"hello signed url" + + +@pytest.fixture +def storage(): + return YepCodeStorage() + + +@pytest.fixture +def uploaded_file(storage): + storage.upload(TEST_NAME, TEST_CONTENT) + yield TEST_NAME + try: + storage.delete(TEST_NAME) + except Exception: + pass + + +def _parse_iso(value: str) -> float: + return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() + + +@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment") +def test_create_signed_url_default_expiry(storage, uploaded_file): + result = storage.create_signed_url(uploaded_file) + + assert isinstance(result.url, str) and len(result.url) > 0 + assert result.path == uploaded_file + + expires_at = _parse_iso(result.expires_at) + expected_expiry = time.time() + 3600 + assert abs(expires_at - expected_expiry) < 60 + + +@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment") +def test_create_signed_url_custom_expiry(storage, uploaded_file): + result = storage.create_signed_url(uploaded_file, expires_in_seconds=60) + + expires_at = _parse_iso(result.expires_at) + expected_expiry = time.time() + 60 + assert abs(expires_at - expected_expiry) < 30 + + +@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment") +def test_create_signed_url_returns_fetchable_url(storage, uploaded_file): + result = storage.create_signed_url(uploaded_file) + + response = requests.get(result.url, timeout=30) + assert response.ok + assert response.content == TEST_CONTENT + + +@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment") +def test_create_signed_url_missing_file_raises_404(storage): + with pytest.raises(YepCodeApiError) as exc_info: + storage.create_signed_url("does-not-exist.txt") + assert exc_info.value.status == 404 + + +@pytest.mark.skip(reason="Requires the signed-urls endpoint deployed in the target environment") +def test_create_signed_url_out_of_range_expiry_raises_400(storage, uploaded_file): + with pytest.raises(YepCodeApiError) as exc_info: + storage.create_signed_url(uploaded_file, expires_in_seconds=999999) + assert exc_info.value.status == 400 diff --git a/yepcode_run/api/types.py b/yepcode_run/api/types.py index f867651..79c9bb0 100644 --- a/yepcode_run/api/types.py +++ b/yepcode_run/api/types.py @@ -470,6 +470,27 @@ class CreateStorageObjectInput: file: Any +@dataclass +class CreateSignedUrlInput: + path: str + expires_in_seconds: Optional[int] = None + + +@dataclass +class SignedUrl: + url: str + path: str + expires_at: str + + @staticmethod + def from_dict(data: dict) -> "SignedUrl": + return SignedUrl( + url=data["url"], + path=data["path"], + expires_at=data.get("expiresAt", data.get("expires_at")), + ) + + # Dependency manifest types @dataclass class ProgrammingLanguageManifest: diff --git a/yepcode_run/api/yepcode_api.py b/yepcode_run/api/yepcode_api.py index 13289a4..56fa9b7 100644 --- a/yepcode_run/api/yepcode_api.py +++ b/yepcode_run/api/yepcode_api.py @@ -42,6 +42,8 @@ ScheduledProcessInput, CreateStorageObjectInput, StorageObject, + CreateSignedUrlInput, + SignedUrl, ProgrammingLanguage, ProgrammingLanguageManifest, UpdateTeamDependenciesInput, @@ -629,3 +631,10 @@ def delete_object(self, name: str) -> None: response.status_code, ) return None + + def create_signed_url(self, data: CreateSignedUrlInput) -> SignedUrl: + body: Dict[str, Any] = {"path": data.path} + if data.expires_in_seconds is not None: + body["expiresInSeconds"] = data.expires_in_seconds + response = self._request("POST", "/storage/signed-urls", {"data": body}) + return SignedUrl.from_dict(response) diff --git a/yepcode_run/storage/yepcode_storage.py b/yepcode_run/storage/yepcode_storage.py index 6640d58..6ed6606 100644 --- a/yepcode_run/storage/yepcode_storage.py +++ b/yepcode_run/storage/yepcode_storage.py @@ -1,7 +1,13 @@ from typing import List, Optional, Dict, Any from ..api.api_manager import YepCodeApiManager -from ..api.types import CreateStorageObjectInput, StorageObject, YepCodeApiConfig +from ..api.types import ( + CreateSignedUrlInput, + CreateStorageObjectInput, + SignedUrl, + StorageObject, + YepCodeApiConfig, +) class YepCodeStorage: @@ -27,3 +33,10 @@ def delete(self, name: str) -> None: def list(self, **kwargs) -> List[StorageObject]: return self._api.get_objects(kwargs if kwargs else None) + + def create_signed_url( + self, name: str, expires_in_seconds: Optional[int] = None + ) -> SignedUrl: + return self._api.create_signed_url( + CreateSignedUrlInput(path=name, expires_in_seconds=expires_in_seconds) + ) From 30ba6142199a1108ad4e8f8c832c91f5d51d69b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Mui=CC=81n=CC=83o=20Garci=CC=81a?= Date: Tue, 28 Apr 2026 14:49:05 +0200 Subject: [PATCH 2/2] Document signed URL support in storage docs. Add README examples and API reference entries for create_signed_url and SignedUrl types so SDK usage matches the new storage feature. Made-with: Cursor --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index ee5aaa8..472af42 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,14 @@ objects = storage.list() for obj in objects: print(obj.name, obj.size, obj.link) +# Create a signed URL (default expiry: 1 hour) +signed = storage.create_signed_url('myfile.txt') +print('Signed URL:', signed.url) +print('Expires at:', signed.expires_at) + +# Create a signed URL with custom expiry (in seconds) +short_lived = storage.create_signed_url('myfile.txt', expires_in_seconds=300) + # Download a file content = storage.download('myfile.txt') with open('downloaded.txt', 'wb') as f: @@ -383,6 +391,17 @@ Lists all files in YepCode storage. **Returns:** List of StorageObject +##### `create_signed_url(name: str, expires_in_seconds: Optional[int] = None) -> SignedUrl` + +Creates a temporary signed URL for a storage object. + +**Parameters:** + +- `name`: Name of the file in storage +- `expires_in_seconds`: Expiration time for the URL in seconds (optional) + +**Returns:** SignedUrl + #### Types ```python @@ -398,6 +417,15 @@ class StorageObject: class CreateStorageObjectInput: name: str # File name file: Any # File content (bytes or file-like) + +class CreateSignedUrlInput: + path: str # File path in storage + expires_in_seconds: Optional[int] # Expiration time in seconds + +class SignedUrl: + url: str # Temporary signed URL + path: str # File path in storage + expires_at: str # Expiration timestamp (ISO8601) ``` ## License