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
49 changes: 49 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
79 changes: 79 additions & 0 deletions yepcode_run/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
103 changes: 103 additions & 0 deletions yepcode_run/api/yepcode_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
ScheduledProcessInput,
CreateStorageObjectInput,
StorageObject,
ServiceAccount,
ServiceAccountInput,
ProgrammingLanguage,
ProgrammingLanguageManifest,
UpdateTeamDependenciesInput,
Team,
UpdateTeamInput,
Sandbox,
CreateSandboxInput,
UpdateSandboxInput,
)


Expand Down Expand Up @@ -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]
Expand Down
Loading