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 +109 -0
- sprites/checkpoint.py +333 -0
- sprites/client.py +377 -0
- sprites/exceptions.py +131 -0
- sprites/exec.py +270 -0
- sprites/filesystem.py +780 -0
- sprites/policy.py +104 -0
- sprites/py.typed +0 -0
- sprites/services.py +429 -0
- sprites/session.py +186 -0
- sprites/sprite.py +545 -0
- sprites/types.py +268 -0
- sprites/websocket.py +320 -0
- sprites_py-0.0.1a1.dist-info/METADATA +137 -0
- sprites_py-0.0.1a1.dist-info/RECORD +18 -0
- sprites_py-0.0.1a1.dist-info/WHEEL +5 -0
- sprites_py-0.0.1a1.dist-info/entry_points.txt +2 -0
- sprites_py-0.0.1a1.dist-info/top_level.txt +1 -0
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
|