diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bd1b26f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Install dependencies +poetry install + +# Run all tests +poetry run pytest + +# Run a single test file +poetry run pytest tests/test_language_detector.py + +# Run a single test function +poetry run pytest tests/test_yepcode_run.py::test_run_python_code + +# Run with coverage +poetry run pytest --cov=yepcode_run + +# Build the package +poetry build +``` + +Tests require a `YEPCODE_API_TOKEN` environment variable. Set it in `.env` or export it before running tests. Most tests are integration tests that hit the live YepCode cloud API. + +## Architecture + +The SDK is a thin Python client for executing code in YepCode's serverless runtime. + +**Layers:** + +1. **Public API** — `YepCodeRun`, `YepCodeEnv`, `YepCodeStorage` (in `yepcode_run/`) +2. **Execution engine** — `yepcode_run/run/execution.py` handles polling loop, status transitions (`CREATED → RUNNING → FINISHED/ERROR`), and event callbacks (`onLog`, `onFinish`, `onError`) +3. **API Manager** — `yepcode_run/api/api_manager.py` singleton keyed by config hash; merges env vars + constructor params +4. **HTTP client** — `yepcode_run/api/yepcode_api.py` handles auth (API token or JWT with auto-refresh), all REST calls + +**Key design decisions:** +- `YepCodeRun` hashes submitted code (SHA256) to reuse existing cloud processes rather than creating new ones each run +- `YepCodeApiManager` uses a singleton per config hash, so multiple `YepCodeRun` instances with the same credentials share one API client +- Language detection (`yepcode_run/utils/language_detector.py`) uses a score-based heuristic on stripped code (comments removed) when `language` is not specified + +**Config priority:** constructor params > environment variables > `.env` file. Key env vars: `YEPCODE_API_TOKEN`, `YEPCODE_API_HOST` (defaults to `https://cloud.yepcode.io`), `YEPCODE_TIMEOUT` (ms, default 60000). + +## OpenAPI spec + +The live spec is always available at `https://cloud.yepcode.io/api/rest/public/api-docs`. Fetch it with WebFetch to audit which endpoints are missing from `yepcode_run/api/yepcode_api.py` before adding new ones. New endpoints are deployed to that URL before this SDK is updated. diff --git a/yepcode_run/api/types.py b/yepcode_run/api/types.py index 80b96b2..21e4460 100644 --- a/yepcode_run/api/types.py +++ b/yepcode_run/api/types.py @@ -468,3 +468,82 @@ def from_dict(data: dict) -> "StorageObject": class CreateStorageObjectInput: name: str file: Any + + +# Service account types +@dataclass +class ServiceAccount: + id: str + name: str + client_id: str + client_secret: Optional[str] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +@dataclass +class ServiceAccountInput: + name: str + + +# Dependency manifest types +@dataclass +class ProgrammingLanguageManifest: + id: str + programming_language: ProgrammingLanguage + dependencies: Optional[Dict[str, str]] = None + next_installation: Optional[Dict[str, str]] = None + + +@dataclass +class UpdateTeamDependenciesInput: + dependencies: Optional[Dict[str, str]] = None + + +# Team types +@dataclass +class Team: + slug: str + name: str + zone_id: Optional[str] = None + parent_team_slugs: Optional[List[str]] = None + params_schema_validation_enabled: Optional[bool] = None + error_handler_config: Optional[Dict[str, Any]] = None + created_at: Optional[datetime] = None + + +@dataclass +class UpdateTeamInput: + name: Optional[str] = None + zone_id: Optional[str] = None + parent_team_slugs: Optional[List[str]] = None + params_schema_validation_enabled: Optional[bool] = None + error_handler_config: Optional[Dict[str, Any]] = None + + +# Sandbox types +@dataclass +class Sandbox: + id: str + name: str + grpc_server_url: Optional[str] = None + grpc_api_key: Optional[str] = None + image_id: Optional[str] = None + public_http_ports: Optional[List[int]] = None + metadata: Optional[Dict[str, Any]] = None + timeout_at: Optional[datetime] = None + + +@dataclass +class CreateSandboxInput: + name: str + image_id: Optional[str] = None + timeout: Optional[int] = None + metadata: Optional[Dict[str, Any]] = None + public_http_ports: Optional[List[int]] = None + public_http_ports_basic_auth: Optional[Dict[str, str]] = None + + +@dataclass +class UpdateSandboxInput: + timeout: Optional[int] = None diff --git a/yepcode_run/api/yepcode_api.py b/yepcode_run/api/yepcode_api.py index e30ac72..1f80293 100644 --- a/yepcode_run/api/yepcode_api.py +++ b/yepcode_run/api/yepcode_api.py @@ -42,6 +42,16 @@ ScheduledProcessInput, CreateStorageObjectInput, StorageObject, + ServiceAccount, + ServiceAccountInput, + ProgrammingLanguage, + ProgrammingLanguageManifest, + UpdateTeamDependenciesInput, + Team, + UpdateTeamInput, + Sandbox, + CreateSandboxInput, + UpdateSandboxInput, ) @@ -469,6 +479,99 @@ def create_module_version_alias( ) -> VersionedModuleAlias: return self._request("POST", f"/modules/{module_id}/aliases", {"data": data}) + def get_module_version(self, module_id: str, version_id: str) -> VersionedModule: + return self._request("GET", f"/modules/{module_id}/versions/{version_id}") + + def delete_module_version(self, module_id: str, version_id: str) -> None: + self._request("DELETE", f"/modules/{module_id}/versions/{version_id}") + + def get_module_version_alias( + self, module_id: str, alias_id: str + ) -> VersionedModuleAlias: + return self._request("GET", f"/modules/{module_id}/aliases/{alias_id}") + + def update_module_version_alias( + self, module_id: str, alias_id: str, data: VersionedModuleAliasInput + ) -> VersionedModuleAlias: + return self._request( + "PATCH", f"/modules/{module_id}/aliases/{alias_id}", {"data": data} + ) + + def delete_module_version_alias(self, module_id: str, alias_id: str) -> None: + self._request("DELETE", f"/modules/{module_id}/aliases/{alias_id}") + + def get_process_version( + self, process_id: str, version_id: str + ) -> VersionedProcess: + return self._request("GET", f"/processes/{process_id}/versions/{version_id}") + + def delete_process_version(self, process_id: str, version_id: str) -> None: + self._request("DELETE", f"/processes/{process_id}/versions/{version_id}") + + def get_process_version_alias( + self, process_id: str, alias_id: str + ) -> VersionedProcessAlias: + return self._request("GET", f"/processes/{process_id}/aliases/{alias_id}") + + def update_process_version_alias( + self, process_id: str, alias_id: str, data: VersionedProcessAliasInput + ) -> VersionedProcessAlias: + return self._request( + "PATCH", f"/processes/{process_id}/aliases/{alias_id}", {"data": data} + ) + + def delete_process_version_alias(self, process_id: str, alias_id: str) -> None: + self._request("DELETE", f"/processes/{process_id}/aliases/{alias_id}") + + def update_schedule(self, id: str, data: ScheduledProcessInput) -> Schedule: + return self._request("PATCH", f"/schedules/{id}", {"data": data}) + + def get_service_accounts(self) -> List[ServiceAccount]: + return self._request("GET", "/auth/service-accounts") + + def create_service_account(self, data: ServiceAccountInput) -> ServiceAccount: + return self._request("POST", "/auth/service-accounts", {"data": data}) + + def delete_service_account(self, id: str) -> None: + self._request("DELETE", f"/auth/service-accounts/{id}") + + def get_team_dependencies( + self, language: ProgrammingLanguage + ) -> ProgrammingLanguageManifest: + return self._request("GET", f"/dependencies/{language.value}") + + def update_team_dependencies( + self, language: ProgrammingLanguage, data: UpdateTeamDependenciesInput + ) -> ProgrammingLanguageManifest: + return self._request( + "PUT", f"/dependencies/{language.value}", {"data": data} + ) + + def install_team_dependencies( + self, language: ProgrammingLanguage + ) -> ProgrammingLanguageManifest: + return self._request("POST", f"/dependencies/{language.value}/install") + + def discard_team_dependencies_installation( + self, language: ProgrammingLanguage + ) -> None: + self._request("DELETE", f"/dependencies/{language.value}/install") + + def get_team(self) -> Team: + return self._request("GET", "/team") + + def update_team(self, data: UpdateTeamInput) -> Team: + return self._request("PATCH", "/team", {"data": data}) + + def create_sandbox(self, data: CreateSandboxInput) -> Sandbox: + return self._request("POST", "/sandboxes", {"data": data}) + + def update_sandbox(self, sandbox_id: str, data: UpdateSandboxInput) -> Sandbox: + return self._request("POST", f"/sandboxes/{sandbox_id}", {"data": data}) + + def kill_sandbox(self, sandbox_id: str) -> None: + self._request("POST", f"/sandboxes/{sandbox_id}/kill") + def get_objects(self, params: Optional[Dict[str, Any]] = None) -> List[StorageObject]: response = self._request("GET", "/storage/objects", {"params": params or {}}) return [StorageObject.from_dict(obj) for obj in response]