tyto.run 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .env
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: tyto.run
3
+ Version: 0.11.0
4
+ Summary: Official Python SDK for the Tyto API
5
+ Project-URL: Homepage, https://tyto.run
6
+ Author: Bonya
7
+ License: MIT
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: websockets>=13.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # tyto
14
+
15
+ Python SDK for the [Tyto API](https://api.tyto.run) — manage nests, sessions, files, previews, snapshots, and keepalive holds.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install tyto
21
+ ```
22
+
23
+ ## Auth
24
+
25
+ Export your API key:
26
+
27
+ ```bash
28
+ export TYTO_API_KEY=your_key_here
29
+ ```
30
+
31
+ Or pass it directly:
32
+
33
+ ```python
34
+ from tyto import Tyto
35
+ tyto = Tyto(api_key="your_key_here")
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import os
42
+ from tyto import Tyto
43
+
44
+ tyto = Tyto(api_key=os.environ["TYTO_API_KEY"])
45
+
46
+ # Who am I?
47
+ me = tyto.me()
48
+ print(me.email)
49
+
50
+ # Create a nest
51
+ nest = tyto.create(name="my-nest", template="ubuntu-24-dev")
52
+
53
+ # Upload / download a file or directory
54
+ nest.put("./hello.txt", "hello.txt")
55
+ nest.get("hello.txt", "./hello.downloaded.txt")
56
+
57
+ # Create a session and attach over WebSocket
58
+ session = nest.create_session(argv=["bash", "-lc", "echo hi"], tty=False)
59
+ with session.attach() as ws:
60
+ for message in ws:
61
+ print(message)
62
+
63
+ # Open raw WebSocket connections
64
+ with nest.exec() as ws: ...
65
+ with nest.console() as ws: ...
66
+
67
+ # Create a preview
68
+ preview = nest.create_preview(port=3000, auth="private")
69
+ print(preview.url)
70
+
71
+ # Snapshot, fork, restore
72
+ snap = nest.create_snapshot(name="v1")
73
+ fork = nest.fork(name="my-fork")
74
+ nest.delete_snapshot(snap.id)
75
+ # nest.restore(snap.id)
76
+
77
+ # Keepalive hold
78
+ nest.holds.put("ci", ttl="30m", reason="CI job")
79
+ nest.holds.heartbeat("ci", ttl="30m")
80
+ nest.holds.delete("ci")
81
+
82
+ # Lifecycle
83
+ nest.stop()
84
+ nest.start()
85
+ nest.wake(reason="wakeup")
86
+ nest.delete()
87
+ ```
88
+
89
+ ## Resources
90
+
91
+ | Resource | Description |
92
+ |---|---|
93
+ | `tyto.create()` | Create a nest |
94
+ | `tyto.nests` | Create / list / get nests |
95
+ | `tyto.previews` | Inspect / revoke previews by ID |
96
+ | `tyto.auth` | CLI browser auth flow |
97
+ | `nest.put(local, remote)` | Upload a file or directory |
98
+ | `nest.get(remote, local)` | Download a file or directory |
99
+ | `nest.create_session()` | Create a managed session |
100
+ | `nest.create_preview()` | Create a preview URL |
101
+ | `nest.create_snapshot()` | Create a snapshot |
102
+ | `nest.delete_snapshot()` | Delete a snapshot |
103
+ | `nest.fs` | Low-level file upload / download |
104
+ | `nest.sessions` | Managed sessions (create / list) |
105
+ | `nest.previews` | Previews scoped to the nest |
106
+ | `nest.snapshots` | Snapshots + fork/restore |
107
+ | `nest.holds` | Keepalive holds |
108
+ | `session.attach()` | WebSocket stream for a session |
109
+ | `nest.console()` | Interactive shell WebSocket |
110
+ | `nest.exec()` | Command WebSocket |
111
+
112
+ ## Configuration
113
+
114
+ | Parameter | Env var | Default |
115
+ |---|---|---|
116
+ | `api_key` | `TYTO_API_KEY` | — (required) |
117
+ | `api_url` | `TYTO_API_URL` | `https://api.tyto.run` |
118
+
119
+ ## Examples
120
+
121
+ ```bash
122
+ python examples/quickstart.py
123
+ python examples/files.py
124
+ python examples/preview.py
125
+ python examples/snapshot_fork.py
126
+ python examples/websocket_exec.py
127
+ ```
128
+
129
+ ## Context manager
130
+
131
+ The `Tyto` client can be used as a context manager to ensure the underlying HTTP connection pool is closed:
132
+
133
+ ```python
134
+ with Tyto() as tyto:
135
+ nests = tyto.nests.list()
136
+ ```
@@ -0,0 +1,124 @@
1
+ # tyto
2
+
3
+ Python SDK for the [Tyto API](https://api.tyto.run) — manage nests, sessions, files, previews, snapshots, and keepalive holds.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install tyto
9
+ ```
10
+
11
+ ## Auth
12
+
13
+ Export your API key:
14
+
15
+ ```bash
16
+ export TYTO_API_KEY=your_key_here
17
+ ```
18
+
19
+ Or pass it directly:
20
+
21
+ ```python
22
+ from tyto import Tyto
23
+ tyto = Tyto(api_key="your_key_here")
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ import os
30
+ from tyto import Tyto
31
+
32
+ tyto = Tyto(api_key=os.environ["TYTO_API_KEY"])
33
+
34
+ # Who am I?
35
+ me = tyto.me()
36
+ print(me.email)
37
+
38
+ # Create a nest
39
+ nest = tyto.create(name="my-nest", template="ubuntu-24-dev")
40
+
41
+ # Upload / download a file or directory
42
+ nest.put("./hello.txt", "hello.txt")
43
+ nest.get("hello.txt", "./hello.downloaded.txt")
44
+
45
+ # Create a session and attach over WebSocket
46
+ session = nest.create_session(argv=["bash", "-lc", "echo hi"], tty=False)
47
+ with session.attach() as ws:
48
+ for message in ws:
49
+ print(message)
50
+
51
+ # Open raw WebSocket connections
52
+ with nest.exec() as ws: ...
53
+ with nest.console() as ws: ...
54
+
55
+ # Create a preview
56
+ preview = nest.create_preview(port=3000, auth="private")
57
+ print(preview.url)
58
+
59
+ # Snapshot, fork, restore
60
+ snap = nest.create_snapshot(name="v1")
61
+ fork = nest.fork(name="my-fork")
62
+ nest.delete_snapshot(snap.id)
63
+ # nest.restore(snap.id)
64
+
65
+ # Keepalive hold
66
+ nest.holds.put("ci", ttl="30m", reason="CI job")
67
+ nest.holds.heartbeat("ci", ttl="30m")
68
+ nest.holds.delete("ci")
69
+
70
+ # Lifecycle
71
+ nest.stop()
72
+ nest.start()
73
+ nest.wake(reason="wakeup")
74
+ nest.delete()
75
+ ```
76
+
77
+ ## Resources
78
+
79
+ | Resource | Description |
80
+ |---|---|
81
+ | `tyto.create()` | Create a nest |
82
+ | `tyto.nests` | Create / list / get nests |
83
+ | `tyto.previews` | Inspect / revoke previews by ID |
84
+ | `tyto.auth` | CLI browser auth flow |
85
+ | `nest.put(local, remote)` | Upload a file or directory |
86
+ | `nest.get(remote, local)` | Download a file or directory |
87
+ | `nest.create_session()` | Create a managed session |
88
+ | `nest.create_preview()` | Create a preview URL |
89
+ | `nest.create_snapshot()` | Create a snapshot |
90
+ | `nest.delete_snapshot()` | Delete a snapshot |
91
+ | `nest.fs` | Low-level file upload / download |
92
+ | `nest.sessions` | Managed sessions (create / list) |
93
+ | `nest.previews` | Previews scoped to the nest |
94
+ | `nest.snapshots` | Snapshots + fork/restore |
95
+ | `nest.holds` | Keepalive holds |
96
+ | `session.attach()` | WebSocket stream for a session |
97
+ | `nest.console()` | Interactive shell WebSocket |
98
+ | `nest.exec()` | Command WebSocket |
99
+
100
+ ## Configuration
101
+
102
+ | Parameter | Env var | Default |
103
+ |---|---|---|
104
+ | `api_key` | `TYTO_API_KEY` | — (required) |
105
+ | `api_url` | `TYTO_API_URL` | `https://api.tyto.run` |
106
+
107
+ ## Examples
108
+
109
+ ```bash
110
+ python examples/quickstart.py
111
+ python examples/files.py
112
+ python examples/preview.py
113
+ python examples/snapshot_fork.py
114
+ python examples/websocket_exec.py
115
+ ```
116
+
117
+ ## Context manager
118
+
119
+ The `Tyto` client can be used as a context manager to ensure the underlying HTTP connection pool is closed:
120
+
121
+ ```python
122
+ with Tyto() as tyto:
123
+ nests = tyto.nests.list()
124
+ ```
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tyto.run"
7
+ version = "0.11.0"
8
+ description = "Official Python SDK for the Tyto API"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Bonya" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = [
14
+ "httpx>=0.27.0",
15
+ "websockets>=13.0",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://tyto.run"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/tyto"]
23
+
24
+ [tool.hatch.build.targets.sdist]
25
+ include = ["src/tyto", "README.md", "pyproject.toml"]
@@ -0,0 +1,62 @@
1
+ from .client import Tyto
2
+ from .config import TytoConfig
3
+ from .errors import TytoAPIError, TytoError
4
+ from .models import (
5
+ AuthPollResponse,
6
+ AuthStartResponse,
7
+ DeleteSnapshotResponse,
8
+ ForkResponse,
9
+ ForkStorage,
10
+ KeepaliveHoldData,
11
+ NestData,
12
+ NestLifecycle,
13
+ PreviewData,
14
+ ReadFileResult,
15
+ RestoreResponse,
16
+ SessionData,
17
+ SnapshotData,
18
+ SnapshotList,
19
+ User,
20
+ WakeResponse,
21
+ )
22
+ from .resources.auth import AuthResource
23
+ from .resources.files import FileSystem
24
+ from .resources.holds import HoldsResource
25
+ from .resources.nests import Nest, NestsResource
26
+ from .resources.previews import PreviewsResource, TopLevelPreviewsResource
27
+ from .resources.sessions import Session, SessionsResource
28
+ from .resources.snapshots import SnapshotsResource, TopLevelSnapshotsResource
29
+
30
+ __all__ = [
31
+ "Tyto",
32
+ "TytoConfig",
33
+ "TytoError",
34
+ "TytoAPIError",
35
+ "User",
36
+ "AuthStartResponse",
37
+ "AuthPollResponse",
38
+ "NestData",
39
+ "NestLifecycle",
40
+ "WakeResponse",
41
+ "SessionData",
42
+ "PreviewData",
43
+ "SnapshotData",
44
+ "SnapshotList",
45
+ "RestoreResponse",
46
+ "ForkResponse",
47
+ "ForkStorage",
48
+ "DeleteSnapshotResponse",
49
+ "KeepaliveHoldData",
50
+ "ReadFileResult",
51
+ "Nest",
52
+ "NestsResource",
53
+ "Session",
54
+ "SessionsResource",
55
+ "FileSystem",
56
+ "PreviewsResource",
57
+ "TopLevelPreviewsResource",
58
+ "SnapshotsResource",
59
+ "TopLevelSnapshotsResource",
60
+ "HoldsResource",
61
+ "AuthResource",
62
+ ]
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from .config import TytoConfig
8
+ from .errors import TytoAPIError
9
+
10
+
11
+ class HttpClient:
12
+ def __init__(self, config: TytoConfig) -> None:
13
+ self._config = config
14
+ self._client = httpx.Client(
15
+ base_url=config.api_url,
16
+ headers={"Authorization": f"Bearer {config.api_key}"},
17
+ timeout=30.0,
18
+ )
19
+
20
+ def _raise_for_status(self, response: httpx.Response) -> None:
21
+ if response.is_success:
22
+ return
23
+ try:
24
+ body = response.json()
25
+ code = body.get("error")
26
+ message = body.get("message") or f"HTTP {response.status_code}"
27
+ except Exception:
28
+ code = None
29
+ message = f"HTTP {response.status_code}"
30
+ raise TytoAPIError(response.status_code, code, message)
31
+
32
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
33
+ resp = self._client.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
34
+ self._raise_for_status(resp)
35
+ if resp.status_code == 204:
36
+ return None
37
+ return resp.json()
38
+
39
+ def post(self, path: str, json: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
40
+ resp = self._client.post(path, json=json, params={k: v for k, v in (params or {}).items() if v is not None})
41
+ self._raise_for_status(resp)
42
+ if resp.status_code == 204:
43
+ return None
44
+ return resp.json()
45
+
46
+ def put(self, path: str, json: Any = None) -> Any:
47
+ resp = self._client.put(path, json=json)
48
+ self._raise_for_status(resp)
49
+ if resp.status_code == 204:
50
+ return None
51
+ return resp.json()
52
+
53
+ def delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
54
+ resp = self._client.delete(path, params={k: v for k, v in (params or {}).items() if v is not None})
55
+ self._raise_for_status(resp)
56
+ if resp.status_code == 204:
57
+ return None
58
+ return resp.json()
59
+
60
+ def put_binary(self, path: str, data: bytes, content_type: str, params: Dict[str, Any]) -> None:
61
+ resp = self._client.put(
62
+ path,
63
+ content=data,
64
+ headers={"Content-Type": content_type},
65
+ params={k: v for k, v in params.items() if v is not None},
66
+ )
67
+ self._raise_for_status(resp)
68
+
69
+ def get_binary(self, path: str, params: Optional[Dict[str, Any]] = None) -> tuple[bytes, str]:
70
+ resp = self._client.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
71
+ self._raise_for_status(resp)
72
+ kind = resp.headers.get("X-Tyto-FS-Kind", "file")
73
+ return resp.content, kind
74
+
75
+ def close(self) -> None:
76
+ self._client.close()
77
+
78
+ def __enter__(self) -> "HttpClient":
79
+ return self
80
+
81
+ def __exit__(self, *args: Any) -> None:
82
+ self.close()
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from ._http import HttpClient
6
+ from .config import TytoConfig, resolve_config
7
+ from .models import User
8
+ from .resources.auth import AuthResource
9
+ from .resources.nests import Nest, NestsResource
10
+ from .resources.previews import TopLevelPreviewsResource
11
+ from .resources.snapshots import TopLevelSnapshotsResource
12
+
13
+
14
+ class Tyto:
15
+ def __init__(
16
+ self,
17
+ api_key: Optional[str] = None,
18
+ api_url: Optional[str] = None,
19
+ ) -> None:
20
+ self._config = resolve_config(api_key=api_key, api_url=api_url)
21
+ self._http = HttpClient(self._config)
22
+ self.auth = AuthResource(self._http)
23
+ self.nests = NestsResource(self._http, self._config)
24
+ self.previews = TopLevelPreviewsResource(self._http)
25
+ self.snapshots = TopLevelSnapshotsResource(self._http)
26
+
27
+ def create(
28
+ self,
29
+ name: str,
30
+ template: str = "ubuntu-24-dev",
31
+ repo_url: Optional[str] = None,
32
+ ) -> Nest:
33
+ return self.nests.create(name=name, template=template, repo_url=repo_url)
34
+
35
+ def health(self) -> dict:
36
+ return self._http.get("/healthz")
37
+
38
+ def ready(self) -> dict:
39
+ return self._http.get("/readyz")
40
+
41
+ def me(self) -> User:
42
+ return User.from_dict(self._http.get("/me"))
43
+
44
+ def close(self) -> None:
45
+ self._http.close()
46
+
47
+ def __enter__(self) -> "Tyto":
48
+ return self
49
+
50
+ def __exit__(self, *args) -> None:
51
+ self.close()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ from .errors import TytoError
7
+
8
+
9
+ @dataclass
10
+ class TytoConfig:
11
+ api_key: str = ""
12
+ api_url: str = "https://api.tyto.run"
13
+
14
+
15
+ def resolve_config(
16
+ api_key: str | None = None,
17
+ api_url: str | None = None,
18
+ ) -> TytoConfig:
19
+ key = api_key or os.environ.get("TYTO_API_KEY", "")
20
+ if not key:
21
+ raise TytoError(
22
+ "api_key is required. Pass it as an argument or set the TYTO_API_KEY environment variable."
23
+ )
24
+ url = (api_url or os.environ.get("TYTO_API_URL", "https://api.tyto.run")).rstrip("/")
25
+ return TytoConfig(api_key=key, api_url=url)
@@ -0,0 +1,13 @@
1
+ class TytoError(Exception):
2
+ pass
3
+
4
+
5
+ class TytoAPIError(TytoError):
6
+ def __init__(self, status: int, code: str | None, message: str) -> None:
7
+ super().__init__(message)
8
+ self.status = status
9
+ self.code = code
10
+ self.message = message
11
+
12
+ def __repr__(self) -> str:
13
+ return f"TytoAPIError(status={self.status}, code={self.code!r}, message={self.message!r})"