sprites-py 0.0.1a1__py3-none-any.whl

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.
sprites/__init__.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ Sprites SDK for Python
3
+
4
+ A Python SDK for interacting with the Sprites API, providing filesystem,
5
+ checkpoint, services, and network policy management.
6
+
7
+ Usage:
8
+ from sprites import SpritesClient
9
+
10
+ client = SpritesClient(token="your-token")
11
+ sprite = client.sprite("my-sprite")
12
+
13
+ # Filesystem operations (pathlib.Path-like API)
14
+ fs = sprite.filesystem("/app")
15
+ config = (fs / "config.json").read_text()
16
+ (fs / "output.txt").write_text("Hello, World!")
17
+
18
+ # List directory contents
19
+ for entry in (fs / "data").iterdir():
20
+ print(entry.name)
21
+ """
22
+
23
+ from .client import SpritesClient
24
+ from .sprite import Sprite
25
+ from .filesystem import SpriteFilesystem, SpritePath
26
+ from .exceptions import (
27
+ SpriteError,
28
+ NetworkError,
29
+ AuthenticationError,
30
+ NotFoundError,
31
+ ExecError,
32
+ FilesystemError,
33
+ FileNotFoundError_,
34
+ IsADirectoryError_,
35
+ NotADirectoryError_,
36
+ PermissionError_,
37
+ DirectoryNotEmptyError,
38
+ )
39
+ from .types import (
40
+ ClientOptions,
41
+ URLSettings,
42
+ SpriteConfig,
43
+ SpawnOptions,
44
+ ExecOptions,
45
+ ExecResult,
46
+ SpriteInfo,
47
+ ListOptions,
48
+ SpriteList,
49
+ Session,
50
+ Checkpoint,
51
+ StreamMessage,
52
+ PortMapping,
53
+ Service,
54
+ ServiceState,
55
+ ServiceWithState,
56
+ ServiceRequest,
57
+ ServiceLogEvent,
58
+ PolicyRule,
59
+ NetworkPolicy,
60
+ FileStat,
61
+ DirEntry,
62
+ )
63
+
64
+ __version__ = "0.0.1-alpha.1"
65
+
66
+ __all__ = [
67
+ # Main classes
68
+ "SpritesClient",
69
+ "Sprite",
70
+ "SpriteFilesystem",
71
+ "SpritePath",
72
+ # Exceptions
73
+ "SpriteError",
74
+ "NetworkError",
75
+ "AuthenticationError",
76
+ "NotFoundError",
77
+ "ExecError",
78
+ "FilesystemError",
79
+ "FileNotFoundError_",
80
+ "IsADirectoryError_",
81
+ "NotADirectoryError_",
82
+ "PermissionError_",
83
+ "DirectoryNotEmptyError",
84
+ # Types
85
+ "ClientOptions",
86
+ "URLSettings",
87
+ "SpriteConfig",
88
+ "SpawnOptions",
89
+ "ExecOptions",
90
+ "ExecResult",
91
+ "SpriteInfo",
92
+ "ListOptions",
93
+ "SpriteList",
94
+ "Session",
95
+ "Checkpoint",
96
+ "StreamMessage",
97
+ "PortMapping",
98
+ "Service",
99
+ "ServiceState",
100
+ "ServiceWithState",
101
+ "ServiceRequest",
102
+ "ServiceLogEvent",
103
+ "PolicyRule",
104
+ "NetworkPolicy",
105
+ "FileStat",
106
+ "DirEntry",
107
+ # Version
108
+ "__version__",
109
+ ]
sprites/checkpoint.py ADDED
@@ -0,0 +1,333 @@
1
+ """Checkpoint operations for Sprites."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Iterator
8
+
9
+ import httpx
10
+
11
+ from sprites.exceptions import APIError
12
+ from sprites.types import Checkpoint, StreamMessage
13
+
14
+ if TYPE_CHECKING:
15
+ from sprites.sprite import Sprite
16
+
17
+
18
+ class CheckpointStream:
19
+ """A stream of checkpoint creation messages."""
20
+
21
+ def __init__(self, response: httpx.Response):
22
+ """Initialize the checkpoint stream.
23
+
24
+ Args:
25
+ response: The HTTP response with streaming body.
26
+ """
27
+ self._response = response
28
+ self._lines = response.iter_lines()
29
+ self._done = False
30
+
31
+ def __iter__(self) -> Iterator[StreamMessage]:
32
+ """Iterate over stream messages."""
33
+ return self
34
+
35
+ def __next__(self) -> StreamMessage:
36
+ """Get the next message from the stream."""
37
+ if self._done:
38
+ raise StopIteration
39
+
40
+ try:
41
+ for line in self._lines:
42
+ if not line:
43
+ continue
44
+ try:
45
+ data = json.loads(line)
46
+ return StreamMessage(
47
+ type=data.get("type", ""),
48
+ data=data.get("data"),
49
+ error=data.get("error"),
50
+ )
51
+ except json.JSONDecodeError:
52
+ continue
53
+ except StopIteration:
54
+ pass
55
+
56
+ self._done = True
57
+ raise StopIteration
58
+
59
+ def close(self) -> None:
60
+ """Close the stream."""
61
+ self._response.close()
62
+
63
+ def __enter__(self) -> CheckpointStream:
64
+ return self
65
+
66
+ def __exit__(self, *args: object) -> None:
67
+ self.close()
68
+
69
+
70
+ class RestoreStream:
71
+ """A stream of checkpoint restore messages."""
72
+
73
+ def __init__(self, response: httpx.Response):
74
+ """Initialize the restore stream.
75
+
76
+ Args:
77
+ response: The HTTP response with streaming body.
78
+ """
79
+ self._response = response
80
+ self._lines = response.iter_lines()
81
+ self._done = False
82
+
83
+ def __iter__(self) -> Iterator[StreamMessage]:
84
+ """Iterate over stream messages."""
85
+ return self
86
+
87
+ def __next__(self) -> StreamMessage:
88
+ """Get the next message from the stream."""
89
+ if self._done:
90
+ raise StopIteration
91
+
92
+ try:
93
+ for line in self._lines:
94
+ if not line:
95
+ continue
96
+ try:
97
+ data = json.loads(line)
98
+ return StreamMessage(
99
+ type=data.get("type", ""),
100
+ data=data.get("data"),
101
+ error=data.get("error"),
102
+ )
103
+ except json.JSONDecodeError:
104
+ continue
105
+ except StopIteration:
106
+ pass
107
+
108
+ self._done = True
109
+ raise StopIteration
110
+
111
+ def close(self) -> None:
112
+ """Close the stream."""
113
+ self._response.close()
114
+
115
+ def __enter__(self) -> RestoreStream:
116
+ return self
117
+
118
+ def __exit__(self, *args: object) -> None:
119
+ self.close()
120
+
121
+
122
+ def list_checkpoints(sprite: Sprite, history_filter: str = "") -> list[Checkpoint]:
123
+ """List all checkpoints for a sprite.
124
+
125
+ Args:
126
+ sprite: The sprite to list checkpoints for.
127
+ history_filter: Optional filter for checkpoint history.
128
+
129
+ Returns:
130
+ List of checkpoint objects.
131
+
132
+ Raises:
133
+ APIError: If the API call fails.
134
+ """
135
+ url = f"{sprite.client.base_url}/v1/sprites/{sprite.name}/checkpoints"
136
+ if history_filter:
137
+ url += f"?history={history_filter}"
138
+
139
+ try:
140
+ response = sprite.client.http_client.get(url)
141
+ except httpx.RequestError as e:
142
+ raise APIError(f"Failed to list checkpoints: {e}") from e
143
+
144
+ if response.status_code != 200:
145
+ raise APIError(
146
+ f"Failed to list checkpoints (status {response.status_code})",
147
+ status_code=response.status_code,
148
+ response=response.text,
149
+ )
150
+
151
+ data = response.json()
152
+ checkpoints = []
153
+ for item in data:
154
+ checkpoint = Checkpoint(
155
+ id=item.get("id", ""),
156
+ create_time=datetime.fromisoformat(item.get("create_time", "").replace("Z", "+00:00")),
157
+ comment=item.get("comment"),
158
+ history=item.get("history"),
159
+ )
160
+ checkpoints.append(checkpoint)
161
+
162
+ return checkpoints
163
+
164
+
165
+ def get_checkpoint(sprite: Sprite, checkpoint_id: str) -> Checkpoint:
166
+ """Get a specific checkpoint.
167
+
168
+ Args:
169
+ sprite: The sprite.
170
+ checkpoint_id: The ID of the checkpoint.
171
+
172
+ Returns:
173
+ The checkpoint object.
174
+
175
+ Raises:
176
+ APIError: If the API call fails.
177
+ """
178
+ url = f"{sprite.client.base_url}/v1/sprites/{sprite.name}/checkpoints/{checkpoint_id}"
179
+
180
+ try:
181
+ response = sprite.client.http_client.get(url)
182
+ except httpx.RequestError as e:
183
+ raise APIError(f"Failed to get checkpoint: {e}") from e
184
+
185
+ if response.status_code != 200:
186
+ raise APIError(
187
+ f"Failed to get checkpoint (status {response.status_code})",
188
+ status_code=response.status_code,
189
+ response=response.text,
190
+ )
191
+
192
+ data = response.json()
193
+ return Checkpoint(
194
+ id=data.get("id", ""),
195
+ create_time=datetime.fromisoformat(data.get("create_time", "").replace("Z", "+00:00")),
196
+ comment=data.get("comment"),
197
+ history=data.get("history"),
198
+ )
199
+
200
+
201
+ def create_checkpoint(sprite: Sprite, comment: str = "") -> CheckpointStream:
202
+ """Create a new checkpoint.
203
+
204
+ Args:
205
+ sprite: The sprite to checkpoint.
206
+ comment: Optional comment for the checkpoint.
207
+
208
+ Returns:
209
+ A stream of checkpoint creation messages.
210
+
211
+ Raises:
212
+ APIError: If the API call fails.
213
+ """
214
+ url = f"{sprite.client.base_url}/v1/sprites/{sprite.name}/checkpoint"
215
+
216
+ payload = {}
217
+ if comment:
218
+ payload["comment"] = comment
219
+
220
+ # Use a separate client for streaming with no timeout
221
+ with httpx.Client(
222
+ timeout=None,
223
+ headers={"Authorization": f"Bearer {sprite.client.token}"},
224
+ ) as client:
225
+ try:
226
+ response = client.post(url, json=payload, headers={"Content-Type": "application/json"})
227
+ except httpx.RequestError as e:
228
+ raise APIError(f"Failed to create checkpoint: {e}") from e
229
+
230
+ if response.status_code != 200:
231
+ raise APIError(
232
+ f"Failed to create checkpoint (status {response.status_code})",
233
+ status_code=response.status_code,
234
+ response=response.text,
235
+ )
236
+
237
+ # For non-streaming response, parse as NDJSON
238
+ messages = []
239
+ for line in response.text.split("\n"):
240
+ if line.strip():
241
+ try:
242
+ data = json.loads(line)
243
+ messages.append(
244
+ StreamMessage(
245
+ type=data.get("type", ""),
246
+ data=data.get("data"),
247
+ error=data.get("error"),
248
+ )
249
+ )
250
+ except json.JSONDecodeError:
251
+ pass
252
+
253
+ # Return a simple iterator wrapper
254
+ return _MessageIterator(messages)
255
+
256
+
257
+ def restore_checkpoint(sprite: Sprite, checkpoint_id: str) -> RestoreStream:
258
+ """Restore a checkpoint.
259
+
260
+ Args:
261
+ sprite: The sprite to restore.
262
+ checkpoint_id: The ID of the checkpoint to restore.
263
+
264
+ Returns:
265
+ A stream of restore messages.
266
+
267
+ Raises:
268
+ APIError: If the API call fails.
269
+ """
270
+ url = f"{sprite.client.base_url}/v1/sprites/{sprite.name}/checkpoints/{checkpoint_id}/restore"
271
+
272
+ # Use a separate client for streaming with no timeout
273
+ with httpx.Client(
274
+ timeout=None,
275
+ headers={"Authorization": f"Bearer {sprite.client.token}"},
276
+ ) as client:
277
+ try:
278
+ response = client.post(url)
279
+ except httpx.RequestError as e:
280
+ raise APIError(f"Failed to restore checkpoint: {e}") from e
281
+
282
+ if response.status_code != 200:
283
+ raise APIError(
284
+ f"Failed to restore checkpoint (status {response.status_code})",
285
+ status_code=response.status_code,
286
+ response=response.text,
287
+ )
288
+
289
+ # For non-streaming response, parse as NDJSON
290
+ messages = []
291
+ for line in response.text.split("\n"):
292
+ if line.strip():
293
+ try:
294
+ data = json.loads(line)
295
+ messages.append(
296
+ StreamMessage(
297
+ type=data.get("type", ""),
298
+ data=data.get("data"),
299
+ error=data.get("error"),
300
+ )
301
+ )
302
+ except json.JSONDecodeError:
303
+ pass
304
+
305
+ # Return a simple iterator wrapper
306
+ return _MessageIterator(messages)
307
+
308
+
309
+ class _MessageIterator:
310
+ """Simple iterator over pre-fetched messages."""
311
+
312
+ def __init__(self, messages: list[StreamMessage]):
313
+ self._messages = messages
314
+ self._index = 0
315
+
316
+ def __iter__(self) -> Iterator[StreamMessage]:
317
+ return self
318
+
319
+ def __next__(self) -> StreamMessage:
320
+ if self._index >= len(self._messages):
321
+ raise StopIteration
322
+ msg = self._messages[self._index]
323
+ self._index += 1
324
+ return msg
325
+
326
+ def close(self) -> None:
327
+ pass
328
+
329
+ def __enter__(self) -> _MessageIterator:
330
+ return self
331
+
332
+ def __exit__(self, *args: object) -> None:
333
+ pass