diff --git a/README.md b/README.md index 2a48a06..e6facbe 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,8 @@ import sys # Stream a new command for event in capsule.commands.stream("python", args=["-u", "train.py"]): match event.type: + case "start": + print(f"PID: {event.pid}") case "stdout": print(event.data, end="") case "stderr": @@ -181,8 +183,11 @@ for event in capsule.commands.stream("python", args=["-u", "train.py"]): # Connect to a running background process for event in capsule.commands.connect(handle.pid): - if event.type == "stdout": - print(event.data, end="") + match event.type: + case "start": + print(f"PID: {event.pid}") + case "stdout": + print(event.data, end="") ``` #### Process Management @@ -211,6 +216,7 @@ capsule.files.exists("/app/main.py") # True # List directory entries = capsule.files.list("/home/user", depth=1) +# FileEntry has: name, type (file/dir), size, modified_at for entry in entries: print(entry.name, entry.type, entry.size) @@ -289,8 +295,27 @@ value = capsule.git.get_config("user.name", cwd="/app") # str | None capsule.git.remote_add("upstream", "https://github.com/org/repo.git", cwd="/app") url = capsule.git.remote_get("origin", cwd="/app") # str | None + +# Reset and restore +capsule.git.reset(mode="hard", ref="HEAD~1", cwd="/app") +capsule.git.restore(["file.txt"], staged=True, cwd="/app") ``` +#### Persistent Credential Store + +For workflows that need repeated authenticated operations, you can persist credentials via the git credential store: + +```python +capsule.git.dangerously_authenticate( + username="user", + password="ghp_token", + host="github.com", + protocol="https", +) +``` + +> **Warning:** Credentials are written in plaintext inside the capsule and are accessible to any process running there. Prefer per-operation `username`/`password` on `clone`, `push`, and `pull` instead. + Git errors raise `GitCommandError` (or `GitAuthError` for authentication failures), both inheriting from `GitError`: ```python @@ -308,7 +333,7 @@ except GitAuthError as e: ```python import sys -with capsule.pty(cmd="/bin/bash", cols=120, rows=40, cwd="/home/user") as term: +with capsule.pty(cmd="/bin/bash", cols=80, rows=24, cwd="/home/user") as term: term.write(b"ls -la\n") for event in term: if event.type == "output": @@ -451,9 +476,10 @@ result = capsule.run_code("print('running on custom template')") | `logs` | `Logs` | `.stdout: list[str]` and `.stderr: list[str]` chunks | | `error` | `ExecutionError \| None` | `.name`, `.value`, `.traceback` | | `execution_count` | `int \| None` | Jupyter cell execution counter | +| `timed_out` | `bool` | ``True`` when execution was cut short by the timeout | | `text` | `str \| None` | (property) `text/plain` of the main `execute_result` | -Each `Result` has typed MIME fields: `text`, `html`, `markdown`, `svg`, `png`, `jpeg`, `pdf`, `latex`, `json`, `javascript`, plus `extra` for unknown types. The `text` field is Jupyter's `text/plain` bundle verbatim — the Python `repr()` of the cell's last expression. So `run_code("'hi'").text` is `"'hi'"` (with quotes), and `run_code("42").text` is `"42"`. This preserves the distinction between the string `'2'` and the int `2`. +Each `Result` has typed MIME fields: `text`, `html`, `markdown`, `svg`, `png`, `jpeg`, `gif`, `pdf`, `latex`, `json`, `javascript`, `plotly`, plus `extra` for unknown types. The `text` field is Jupyter's `text/plain` bundle verbatim — the Python `repr()` of the cell's last expression. So `run_code("'hi'").text` is `"'hi'"` (with quotes), and `run_code("42").text` is `"42"`. This preserves the distinction between the string `'2'` and the int `2`. ### Code Runner + Commands/Files @@ -527,15 +553,15 @@ The SDK maps server error codes to typed exceptions: ```python from wrenn import ( WrennError, - WrennValidationError, # 400 - WrennAuthenticationError, # 401 - WrennForbiddenError, # 403 - WrennNotFoundError, # 404 - WrennConflictError, # 409 - WrennHostHasCapsulesError, # 409 (host has running capsules) - WrennAgentError, # 502 - WrennInternalError, # 500 - WrennHostUnavailableError, # 503 + WrennValidationError, # 400 + WrennAuthenticationError, # 401 + WrennForbiddenError, # 403 + WrennNotFoundError, # 404 + WrennConflictError, # 409 + WrennHostHasCapsulesError, # 409 (host has running capsules) + WrennInternalError, # 500 + WrennAgentError, # 502 + WrennHostUnavailableError, # 503 ) try: @@ -603,7 +629,7 @@ with WrennClient(api_key="wrn_...") as client: # Snapshots template = client.snapshots.create(capsule_id="cl-abc", name="my-snap") - templates = client.snapshots.list() + templates = client.snapshots.list(type="custom") # optional type filter client.snapshots.delete("my-snap") ``` diff --git a/api/openapi.yaml b/api/openapi.yaml index c8ad59f..0a4b218 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2716,14 +2716,39 @@ paths: tags: [admin] security: - sessionAuth: [] + parameters: + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + description: Page number for pagination. responses: "200": - description: Teams list + description: Paginated teams list content: application/json: schema: - type: array - items: {type: object} + type: object + properties: + teams: + type: array + items: + $ref: "#/components/schemas/AdminTeam" + total: + type: integer + page: + type: integer + per_page: + type: integer + total_pages: + type: integer + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /v1/admin/teams/{id}/byoc: put: @@ -2743,12 +2768,20 @@ paths: application/json: schema: type: object - required: [byoc] + required: [enabled] properties: - byoc: {type: boolean} + enabled: + type: boolean + description: true to enable BYOC, false to disable. responses: "204": description: Updated + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /v1/admin/teams/{id}: delete: @@ -2765,6 +2798,38 @@ paths: responses: "204": description: Deleted + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + + /v1/admin/hosts: + get: + summary: List all hosts (admin) + operationId: adminListHosts + tags: [admin] + security: + - sessionAuth: [] + description: | + Returns all hosts across all teams with per-host resource consumption. + Includes team name for hosts associated with a team. + responses: + "200": + description: Hosts list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Host" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /v1/admin/users: get: @@ -3581,10 +3646,6 @@ components: type: integer memory_mb_reserved: type: integer - sampled_at: - type: string - format: date-time - nullable: true peaks: type: object description: Maximum values over the last 30 days. @@ -3633,10 +3694,6 @@ components: type: integer timeout_sec: type: integer - guest_ip: - type: string - host_ip: - type: string created_at: type: string format: date-time @@ -3661,7 +3718,11 @@ components: agent_version, envd_version) when running. disk_size_mb: type: integer - nullable: true + description: Maximum disk capacity in MiB. + disk_used_mb: + type: integer + format: int64 + description: Current disk usage in MiB. Only populated on individual capsule GET; omitted in list responses. CreateSnapshotRequest: type: object @@ -4013,6 +4074,25 @@ components: updated_at: type: string format: date-time + team_name: + type: string + nullable: true + description: Team name (included when listing hosts as an admin). + running_vcpus: + type: integer + description: Total vCPUs allocated to running capsules on this host. + running_memory_mb: + type: integer + description: Total memory in MB allocated to running capsules on this host. + running_disk_mb: + type: integer + description: Total disk in MB allocated to running capsules on this host. + paused_memory_mb: + type: integer + description: Total memory in MB allocated to paused capsules on this host. + paused_disk_mb: + type: integer + description: Total disk in MB allocated to paused capsules on this host. RefreshHostTokenRequest: type: object @@ -4124,6 +4204,39 @@ components: items: $ref: "#/components/schemas/TeamMember" + AdminTeam: + type: object + properties: + id: + type: string + name: + type: string + slug: + type: string + is_byoc: + type: boolean + created_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + member_count: + type: integer + owner_name: + type: string + owner_email: + type: string + active_sandbox_count: + type: integer + channel_count: + type: integer + running_vcpus: + type: integer + running_memory_mb: + type: integer + CapsuleMetrics: type: object properties: diff --git a/pyproject.toml b/pyproject.toml index 6ee4a11..b67c7a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wrenn" -version = "0.1.5" +version = "0.2.0" description = "Python SDK for Wrenn" readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index bc19b60..0dc0352 100644 --- a/uv.lock +++ b/uv.lock @@ -1166,7 +1166,7 @@ wheels = [ [[package]] name = "wrenn" -version = "0.1.5" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "certifi" },