Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
75 changes: 75 additions & 0 deletions tests/test_yepcode_storage.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions yepcode_run/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions yepcode_run/api/yepcode_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
ScheduledProcessInput,
CreateStorageObjectInput,
StorageObject,
CreateSignedUrlInput,
SignedUrl,
ProgrammingLanguage,
ProgrammingLanguageManifest,
UpdateTeamDependenciesInput,
Expand Down Expand Up @@ -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)
15 changes: 14 additions & 1 deletion yepcode_run/storage/yepcode_storage.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
)
Loading