From 0dda9ba2404a65af4b9e8cbecaa6bbf5654d0607 Mon Sep 17 00:00:00 2001 From: alvaro Date: Fri, 27 Jun 2025 12:14:42 +0200 Subject: [PATCH 1/2] Support storage endpoints in api --- yepcode_run/api/types.py | 30 ++++++++++++++++ yepcode_run/api/yepcode_api.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/yepcode_run/api/types.py b/yepcode_run/api/types.py index 31bc15c..42d4039 100644 --- a/yepcode_run/api/types.py +++ b/yepcode_run/api/types.py @@ -439,3 +439,33 @@ class VersionedModuleAliasInput: @dataclass class VersionedModuleAliasesPaginatedResult(PaginatedResult): data: Optional[List[VersionedModuleAlias]] = None + + +# Storage types +@dataclass +class StorageObject: + name: str + size: int + md5_hash: str + content_type: str + created_at: str + updated_at: str + link: str + + @staticmethod + def from_dict(data: dict) -> "StorageObject": + return StorageObject( + name=data["name"], + size=data["size"], + md5_hash=data.get("md5Hash", data.get("md5_hash")), + content_type=data.get("contentType", data.get("content_type")), + created_at=data.get("createdAt", data.get("created_at")), + updated_at=data.get("updatedAt", data.get("updated_at")), + link=str(data.get("link")), + ) + + +@dataclass +class CreateStorageObjectInput: + name: str + file: Any diff --git a/yepcode_run/api/yepcode_api.py b/yepcode_run/api/yepcode_api.py index f503db6..8daed1c 100644 --- a/yepcode_run/api/yepcode_api.py +++ b/yepcode_run/api/yepcode_api.py @@ -4,6 +4,7 @@ from datetime import datetime import requests from urllib.parse import urljoin +import mimetypes from .types import ( YepCodeApiConfig, @@ -38,6 +39,8 @@ VersionedProcessAliasInput, VersionedModuleAliasInput, ScheduledProcessInput, + CreateStorageObjectInput, + StorageObject, ) @@ -443,3 +446,66 @@ def create_module_version_alias( self, module_id: str, data: VersionedModuleAliasInput ) -> VersionedModuleAlias: return self._request("POST", f"/modules/{module_id}/aliases", {"data": data}) + + def get_objects(self) -> List[StorageObject]: + response = self._request("GET", "/storage/objects") + return [StorageObject.from_dict(obj) for obj in response] + + def get_object(self, name: str) -> requests.Response: + if not self.access_token: + self._get_access_token() + headers = { + "Authorization": f"Bearer {self.access_token}", + } + endpoint = f"/storage/objects/{name}" + url = urljoin(f"{self._get_base_url()}/", endpoint.lstrip("/")) + response = requests.get(url, headers=headers, stream=True, timeout=self.timeout / 1000) + response.raise_for_status() + return response + + def create_object(self, data: CreateStorageObjectInput) -> StorageObject: + if not data.file: + raise ValueError("File or stream is required") + if not self.access_token: + self._get_access_token() + headers = { + "Authorization": f"Bearer {self.access_token}", + } + endpoint = f"/storage/objects?name={requests.utils.quote(data.name)}" + url = urljoin(f"{self._get_base_url()}/", endpoint.lstrip("/")) + # Detect content type + content_type, _ = mimetypes.guess_type(data.name) + files = {"file": (data.name, data.file, content_type or "application/octet-stream")} + response = requests.post(url, headers=headers, files=files, timeout=self.timeout / 1000) + if not response.ok: + try: + error_response = response.json() + message = error_response.get("message", response.reason) + except ValueError: + message = response.reason + raise YepCodeApiError( + f"HTTP error {response.status_code} in endpoint POST {endpoint}: {message}", + response.status_code, + ) + return StorageObject.from_dict(response.json()) + + def delete_object(self, name: str) -> None: + if not self.access_token: + self._get_access_token() + headers = { + "Authorization": f"Bearer {self.access_token}", + } + endpoint = f"/storage/objects/{requests.utils.quote(name)}" + url = urljoin(f"{self._get_base_url()}/", endpoint.lstrip("/")) + response = requests.delete(url, headers=headers, timeout=self.timeout / 1000) + if not response.ok: + try: + error_response = response.json() + message = error_response.get("message", response.reason) + except ValueError: + message = response.reason + raise YepCodeApiError( + f"HTTP error {response.status_code} in endpoint DELETE {endpoint}: {message}", + response.status_code, + ) + return None From 14f82e4a285ae3b7bd4eabd23c418a97afa4ba6f Mon Sep 17 00:00:00 2001 From: alvaro Date: Fri, 27 Jun 2025 12:16:23 +0200 Subject: [PATCH 2/2] Add YepCode Storage sdk --- README.md | 83 ++++++++++++++++++++++++++ yepcode_run/storage/yepcode_storage.py | 29 +++++++++ 2 files changed, 112 insertions(+) create mode 100644 yepcode_run/storage/yepcode_storage.py diff --git a/README.md b/README.md index 77795b9..e00eb4f 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,36 @@ api = YepCodeApi( processes = api.get_processes() ``` +### 6. YepCode Storage + +You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, and delete files easily. + +```python +from yepcode_run import YepCodeStorage, YepCodeApiConfig + +storage = YepCodeStorage( + YepCodeApiConfig(api_token='your-api-token') +) + +# Upload a file +with open('myfile.txt', 'rb') as f: + obj = storage.upload('myfile.txt', f) + print('Uploaded:', obj.name, obj.size, obj.link) + +# List all storage objects +objects = storage.list() +for obj in objects: + print(obj.name, obj.size, obj.link) + +# Download a file +content = storage.download('myfile.txt') +with open('downloaded.txt', 'wb') as f: + f.write(content) + +# Delete a file +storage.delete('myfile.txt') +``` + ## SDK API Reference ### YepCodeRun @@ -237,6 +267,59 @@ class Process: created_at: str ``` +### YepCodeStorage + +The main class for managing files in YepCode's cloud storage. + +#### Methods + +##### `upload(name: str, file: bytes) -> StorageObject` +Uploads a file to YepCode storage. + +**Parameters:** +- `name`: Name of the file in storage +- `file`: File content as bytes or a file-like object + +**Returns:** StorageObject + +##### `download(name: str) -> bytes` +Downloads a file from YepCode storage. + +**Parameters:** +- `name`: Name of the file to download + +**Returns:** File content as bytes + +##### `delete(name: str) -> None` +Deletes a file from YepCode storage. + +**Parameters:** +- `name`: Name of the file to delete + +**Returns:** None + +##### `list() -> List[StorageObject]` +Lists all files in YepCode storage. + +**Returns:** List of StorageObject + +#### Types + +```python +class StorageObject: + name: str # File name + size: int # File size in bytes + md5_hash: str # MD5 hash of the file + content_type: str # MIME type + created_at: str # Creation timestamp (ISO8601) + updated_at: str # Last update timestamp (ISO8601) + link: str # Download link + +class CreateStorageObjectInput: + name: str # File name + file: Any # File content (bytes or file-like) +``` + ## License All rights reserved by YepCode. This package is part of the YepCode Platform and is subject to the [YepCode Terms of Service](https://yepcode.io/terms-of-use). \ No newline at end of file diff --git a/yepcode_run/storage/yepcode_storage.py b/yepcode_run/storage/yepcode_storage.py new file mode 100644 index 0000000..62e411e --- /dev/null +++ b/yepcode_run/storage/yepcode_storage.py @@ -0,0 +1,29 @@ +from typing import List + +from ..api.api_manager import YepCodeApiManager +from ..api.types import CreateStorageObjectInput, StorageObject, YepCodeApiConfig + + +class YepCodeStorage: + def __init__(self, config: YepCodeApiConfig = None): + """ + Initialize YepCodeStorage with optional configuration. + + Args: + config: YepCodeApiConfig instance for API configuration + """ + self._api = YepCodeApiManager.get_instance(config) + + def download(self, name: str) -> bytes: + return self._api.get_object(name).content + + def upload(self, name: str, file: bytes) -> StorageObject: + return self._api.create_object( + CreateStorageObjectInput(name=name, file=file) + ) + + def delete(self, name: str) -> None: + return self._api.delete_object(name) + + def list(self) -> List[StorageObject]: + return self._api.get_objects()