pie-client 0.1.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.
- pie_client-0.1.0/PKG-INFO +29 -0
- pie_client-0.1.0/README.md +16 -0
- pie_client-0.1.0/pie/__init__.py +2 -0
- pie_client-0.1.0/pie/client.py +365 -0
- pie_client-0.1.0/pie_client.egg-info/PKG-INFO +29 -0
- pie_client-0.1.0/pie_client.egg-info/SOURCES.txt +9 -0
- pie_client-0.1.0/pie_client.egg-info/dependency_links.txt +1 -0
- pie_client-0.1.0/pie_client.egg-info/requires.txt +3 -0
- pie_client-0.1.0/pie_client.egg-info/top_level.txt +1 -0
- pie_client-0.1.0/pyproject.toml +26 -0
- pie_client-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pie-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pie Client
|
|
5
|
+
Author-email: In Gim <in.gim@yale.edu>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/pie-project/pie
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: websockets==11.0.3
|
|
11
|
+
Requires-Dist: msgpack==0.5.6
|
|
12
|
+
Requires-Dist: blake3==1.0.4
|
|
13
|
+
|
|
14
|
+
# PIE Python Client
|
|
15
|
+
|
|
16
|
+
A concise Python toolkit to interact with the Symphony server:
|
|
17
|
+
|
|
18
|
+
- Upload LIP (\*.wasm) binaries
|
|
19
|
+
- Check if a LIP is already uploaded
|
|
20
|
+
- Launch a LIP on Symphony
|
|
21
|
+
- Interact with running LIP instances in real-time (send/receive messages, terminate, etc.)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
```bash
|
|
25
|
+
pip install -r requirements.txt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
Refer to [main.py](./main.py) for a usage example.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# PIE Python Client
|
|
2
|
+
|
|
3
|
+
A concise Python toolkit to interact with the Symphony server:
|
|
4
|
+
|
|
5
|
+
- Upload LIP (\*.wasm) binaries
|
|
6
|
+
- Check if a LIP is already uploaded
|
|
7
|
+
- Launch a LIP on Symphony
|
|
8
|
+
- Interact with running LIP instances in real-time (send/receive messages, terminate, etc.)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
```bash
|
|
12
|
+
pip install -r requirements.txt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
Refer to [main.py](./main.py) for a usage example.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import msgpack
|
|
3
|
+
import websockets
|
|
4
|
+
import blake3
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import uuid
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Event(Enum):
|
|
13
|
+
"""Enumeration for events received from an instance."""
|
|
14
|
+
Message = 0
|
|
15
|
+
Completed = 1
|
|
16
|
+
Aborted = 2
|
|
17
|
+
Exception = 3
|
|
18
|
+
ServerError = 4
|
|
19
|
+
OutOfResources = 5
|
|
20
|
+
Blob = 6 # Represents a binary data blob
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Instance:
|
|
24
|
+
"""Represents a running instance of a program on the server."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client, instance_id: str):
|
|
27
|
+
self.client = client
|
|
28
|
+
self.instance_id = instance_id
|
|
29
|
+
self.event_queue = self.client.inst_event_queues.get(instance_id)
|
|
30
|
+
if self.event_queue is None:
|
|
31
|
+
raise Exception(f"Internal error: No event queue for instance {instance_id}")
|
|
32
|
+
|
|
33
|
+
async def send(self, message: str):
|
|
34
|
+
"""Send a string message to the instance."""
|
|
35
|
+
await self.client.signal_instance(self.instance_id, message)
|
|
36
|
+
|
|
37
|
+
async def upload_blob(self, blob_bytes: bytes):
|
|
38
|
+
"""Upload a blob of binary data to the instance."""
|
|
39
|
+
await self.client.upload_blob(self.instance_id, blob_bytes)
|
|
40
|
+
|
|
41
|
+
async def recv(self) -> tuple[Event, str | bytes]:
|
|
42
|
+
"""
|
|
43
|
+
Receive an event from the instance. Blocks until an event is available.
|
|
44
|
+
Returns a tuple of (Event, message), where message can be a string or bytes.
|
|
45
|
+
"""
|
|
46
|
+
if self.event_queue is None:
|
|
47
|
+
raise Exception("Event queue is not available for this instance.")
|
|
48
|
+
event_code, msg = await self.event_queue.get()
|
|
49
|
+
|
|
50
|
+
event = Event(event_code)
|
|
51
|
+
return event, msg
|
|
52
|
+
|
|
53
|
+
async def terminate(self):
|
|
54
|
+
"""Request termination of the instance."""
|
|
55
|
+
await self.client.terminate_instance(self.instance_id)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PieClient:
|
|
59
|
+
"""
|
|
60
|
+
An asynchronous client for interacting with the Pie WebSocket server.
|
|
61
|
+
This client is designed to be used as an async context manager.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, server_uri: str):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the client.
|
|
67
|
+
:param server_uri: The WebSocket server URI (e.g., "ws://127.0.0.1:8080").
|
|
68
|
+
"""
|
|
69
|
+
self.server_uri = server_uri
|
|
70
|
+
self.ws = None
|
|
71
|
+
self.listener_task = None
|
|
72
|
+
self.corr_id_counter = 0
|
|
73
|
+
self.pending_requests = {}
|
|
74
|
+
self.inst_event_queues = {}
|
|
75
|
+
self.pending_downloads = {} # For reassembling blob chunks
|
|
76
|
+
|
|
77
|
+
# 🔻 FIX 1/3: Buffer for early events to prevent race conditions.
|
|
78
|
+
self.orphan_events = {}
|
|
79
|
+
|
|
80
|
+
async def __aenter__(self):
|
|
81
|
+
"""Enter the async context, establishing the connection."""
|
|
82
|
+
await self.connect()
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
86
|
+
"""Exit the async context, closing the connection cleanly."""
|
|
87
|
+
await self.close()
|
|
88
|
+
|
|
89
|
+
async def connect(self):
|
|
90
|
+
"""Establish a WebSocket connection and start the background listener."""
|
|
91
|
+
self.ws = await websockets.connect(self.server_uri)
|
|
92
|
+
print(f"[PieClient] Connected to {self.server_uri}")
|
|
93
|
+
self.listener_task = asyncio.create_task(self._listen_to_server())
|
|
94
|
+
|
|
95
|
+
async def _listen_to_server(self):
|
|
96
|
+
"""Background task to receive and process all incoming server messages."""
|
|
97
|
+
try:
|
|
98
|
+
async for raw_msg in self.ws:
|
|
99
|
+
if isinstance(raw_msg, bytes):
|
|
100
|
+
try:
|
|
101
|
+
message = msgpack.unpackb(raw_msg, raw=False)
|
|
102
|
+
await self._process_server_message(message)
|
|
103
|
+
except msgpack.UnpackException as e:
|
|
104
|
+
print(f"[PieClient] Failed to decode messagepack: {e}")
|
|
105
|
+
else:
|
|
106
|
+
print(f"[PieClient] Received unexpected non-binary message: {raw_msg}")
|
|
107
|
+
except websockets.ConnectionClosedOK:
|
|
108
|
+
print("[PieClient] Connection closed normally.")
|
|
109
|
+
except websockets.ConnectionClosedError as e:
|
|
110
|
+
print(f"[PieClient] Connection closed with error: {e}")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"[PieClient] Listener task encountered an unexpected error: {e}")
|
|
113
|
+
|
|
114
|
+
async def _process_server_message(self, message: dict):
|
|
115
|
+
"""Route incoming server messages based on their type."""
|
|
116
|
+
msg_type = message.get("type")
|
|
117
|
+
if msg_type == "response":
|
|
118
|
+
corr_id = message.get("corr_id")
|
|
119
|
+
if corr_id in self.pending_requests:
|
|
120
|
+
future = self.pending_requests.pop(corr_id)
|
|
121
|
+
future.set_result((message.get("successful"), message.get("result")))
|
|
122
|
+
|
|
123
|
+
elif msg_type == "instance_event":
|
|
124
|
+
# 🔻 FIX 2/3: Buffer orphan events instead of discarding them.
|
|
125
|
+
instance_id = message.get("instance_id")
|
|
126
|
+
event_tuple = (message.get("event"), message.get("message"))
|
|
127
|
+
|
|
128
|
+
if instance_id in self.inst_event_queues:
|
|
129
|
+
# Queue exists, proceed as normal
|
|
130
|
+
await self.inst_event_queues[instance_id].put(event_tuple)
|
|
131
|
+
else:
|
|
132
|
+
# Queue doesn't exist yet, buffer the event
|
|
133
|
+
if instance_id not in self.orphan_events:
|
|
134
|
+
self.orphan_events[instance_id] = []
|
|
135
|
+
self.orphan_events[instance_id].append(event_tuple)
|
|
136
|
+
|
|
137
|
+
elif msg_type == "download_blob":
|
|
138
|
+
await self._handle_blob_chunk(message)
|
|
139
|
+
elif msg_type == "server_event":
|
|
140
|
+
print(f"[PieClient] Received server event: {message.get('message')}")
|
|
141
|
+
else:
|
|
142
|
+
print(f"[PieClient] Received unknown message type: {msg_type}")
|
|
143
|
+
|
|
144
|
+
async def _handle_blob_chunk(self, message: dict):
|
|
145
|
+
"""Processes a chunk of a blob sent from the server, ensuring sequential order."""
|
|
146
|
+
blob_hash = message.get("blob_hash")
|
|
147
|
+
instance_id = message.get("instance_id")
|
|
148
|
+
chunk_index = message.get("chunk_index")
|
|
149
|
+
total_chunks = message.get("total_chunks")
|
|
150
|
+
|
|
151
|
+
if instance_id not in self.inst_event_queues:
|
|
152
|
+
return # Ignore blobs for unknown/terminated instances
|
|
153
|
+
|
|
154
|
+
# Initialize download on the first chunk (index 0)
|
|
155
|
+
if blob_hash not in self.pending_downloads:
|
|
156
|
+
if chunk_index != 0:
|
|
157
|
+
print(f"[PieClient] Received non-zero first chunk for blob {blob_hash}. Discarding.")
|
|
158
|
+
return
|
|
159
|
+
self.pending_downloads[blob_hash] = {
|
|
160
|
+
"buffer": bytearray(),
|
|
161
|
+
"total_chunks": total_chunks,
|
|
162
|
+
"next_chunk_index": 1,
|
|
163
|
+
"instance_id": instance_id,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
download = self.pending_downloads[blob_hash]
|
|
167
|
+
|
|
168
|
+
# Validate chunk consistency and order
|
|
169
|
+
if total_chunks != download["total_chunks"] or chunk_index != download["next_chunk_index"] - 1:
|
|
170
|
+
error_msg = "Chunk count mismatch" if total_chunks != download["total_chunks"] else "Out-of-order chunk"
|
|
171
|
+
print(f"[PieClient] {error_msg} for blob {blob_hash}. Aborting download.")
|
|
172
|
+
del self.pending_downloads[blob_hash]
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
download["buffer"].extend(message.get("chunk_data"))
|
|
176
|
+
download["next_chunk_index"] += 1
|
|
177
|
+
|
|
178
|
+
# If all chunks are received, finalize the download
|
|
179
|
+
if download["next_chunk_index"] == download["total_chunks"]:
|
|
180
|
+
completed_blob = bytes(download["buffer"])
|
|
181
|
+
computed_hash = blake3.blake3(completed_blob).hexdigest()
|
|
182
|
+
if computed_hash == blob_hash:
|
|
183
|
+
await self.inst_event_queues[instance_id].put((Event.Blob.value, completed_blob))
|
|
184
|
+
else:
|
|
185
|
+
print(f"[PieClient] Blob hash mismatch for instance {instance_id}. Expected {blob_hash}, got {computed_hash}. Discarding.")
|
|
186
|
+
|
|
187
|
+
del self.pending_downloads[blob_hash]
|
|
188
|
+
|
|
189
|
+
async def close(self):
|
|
190
|
+
"""Gracefully close the WebSocket connection and shut down background tasks."""
|
|
191
|
+
if self.ws and not self.ws.closed:
|
|
192
|
+
await self.ws.close()
|
|
193
|
+
if self.listener_task:
|
|
194
|
+
try:
|
|
195
|
+
self.listener_task.cancel()
|
|
196
|
+
await self.listener_task
|
|
197
|
+
except asyncio.CancelledError:
|
|
198
|
+
pass # Expected on cancellation
|
|
199
|
+
print("[PieClient] Client has been shut down.")
|
|
200
|
+
|
|
201
|
+
def _get_next_corr_id(self):
|
|
202
|
+
"""Generate a unique correlation ID for a request."""
|
|
203
|
+
self.corr_id_counter += 1
|
|
204
|
+
return self.corr_id_counter
|
|
205
|
+
|
|
206
|
+
async def _send_msg_and_wait(self, msg: dict) -> tuple[bool, str]:
|
|
207
|
+
"""Send a message that expects a response and wait for it."""
|
|
208
|
+
corr_id = self._get_next_corr_id()
|
|
209
|
+
msg["corr_id"] = corr_id
|
|
210
|
+
future = asyncio.get_event_loop().create_future()
|
|
211
|
+
self.pending_requests[corr_id] = future
|
|
212
|
+
encoded = msgpack.packb(msg, use_bin_type=True)
|
|
213
|
+
await self.ws.send(encoded)
|
|
214
|
+
return await future
|
|
215
|
+
|
|
216
|
+
async def authenticate(self, token: str) -> tuple[bool, str]:
|
|
217
|
+
"""Authenticate the client with the server using a token."""
|
|
218
|
+
msg = {"type": "authenticate", "token": token}
|
|
219
|
+
successful, result = await self._send_msg_and_wait(msg)
|
|
220
|
+
if successful:
|
|
221
|
+
print("[PieClient] Authenticated successfully.")
|
|
222
|
+
else:
|
|
223
|
+
print(f"[PieClient] Authentication failed: {result}")
|
|
224
|
+
return successful, result
|
|
225
|
+
|
|
226
|
+
async def query(self, subject: str, record: str) -> tuple[bool, str]:
|
|
227
|
+
"""Send a generic query to the server."""
|
|
228
|
+
msg = {"type": "query", "subject": subject, "record": record}
|
|
229
|
+
return await self._send_msg_and_wait(msg)
|
|
230
|
+
|
|
231
|
+
async def program_exists(self, program_hash: str) -> bool:
|
|
232
|
+
"""Check if a program with the given hash exists on the server."""
|
|
233
|
+
successful, result = await self.query("program_exists", program_hash)
|
|
234
|
+
if successful:
|
|
235
|
+
return result == "true"
|
|
236
|
+
raise Exception(f"Query for program_exists failed: {result}")
|
|
237
|
+
|
|
238
|
+
async def _upload_chunked(self, data_bytes: bytes, msg_template: dict):
|
|
239
|
+
"""Internal helper to handle generic chunked uploads."""
|
|
240
|
+
data_hash = msg_template.get("program_hash") or msg_template.get("blob_hash")
|
|
241
|
+
upload_type = msg_template["type"]
|
|
242
|
+
|
|
243
|
+
chunk_size = 256 * 1024
|
|
244
|
+
total_size = len(data_bytes)
|
|
245
|
+
# An empty upload is still one chunk of zero bytes
|
|
246
|
+
total_chunks = (total_size + chunk_size - 1) // chunk_size if total_size > 0 else 1
|
|
247
|
+
|
|
248
|
+
corr_id = self._get_next_corr_id()
|
|
249
|
+
msg_template["corr_id"] = corr_id
|
|
250
|
+
msg_template["total_chunks"] = total_chunks
|
|
251
|
+
|
|
252
|
+
if total_size == 0:
|
|
253
|
+
msg = msg_template.copy()
|
|
254
|
+
msg.update({"chunk_index": 0, "chunk_data": b''})
|
|
255
|
+
await self.ws.send(msgpack.packb(msg, use_bin_type=True))
|
|
256
|
+
else:
|
|
257
|
+
for chunk_index in range(total_chunks):
|
|
258
|
+
start = chunk_index * chunk_size
|
|
259
|
+
end = min(start + chunk_size, total_size)
|
|
260
|
+
msg = msg_template.copy()
|
|
261
|
+
msg.update({"chunk_index": chunk_index, "chunk_data": data_bytes[start:end]})
|
|
262
|
+
await self.ws.send(msgpack.packb(msg, use_bin_type=True))
|
|
263
|
+
|
|
264
|
+
future = asyncio.get_event_loop().create_future()
|
|
265
|
+
self.pending_requests[corr_id] = future
|
|
266
|
+
successful, result = await future
|
|
267
|
+
|
|
268
|
+
if not successful:
|
|
269
|
+
raise Exception(f"{upload_type.replace('_', ' ').title()} failed: {result}")
|
|
270
|
+
|
|
271
|
+
print(f"[PieClient] {upload_type.replace('_', ' ').title()} successful for hash: {data_hash}")
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
async def upload_program(self, program_bytes: bytes):
|
|
275
|
+
"""Upload a program to the server in chunks."""
|
|
276
|
+
program_hash = blake3.blake3(program_bytes).hexdigest()
|
|
277
|
+
template = {"type": "upload_program", "program_hash": program_hash}
|
|
278
|
+
await self._upload_chunked(program_bytes, template)
|
|
279
|
+
|
|
280
|
+
async def upload_blob(self, instance_id: str, blob_bytes: bytes):
|
|
281
|
+
"""Upload a blob of data to a specific instance in chunks."""
|
|
282
|
+
blob_hash = blake3.blake3(blob_bytes).hexdigest()
|
|
283
|
+
template = {"type": "upload_blob", "instance_id": instance_id, "blob_hash": blob_hash}
|
|
284
|
+
await self._upload_chunked(blob_bytes, template)
|
|
285
|
+
|
|
286
|
+
async def launch_instance(self, program_hash: str, arguments: list[str] = None) -> Instance:
|
|
287
|
+
"""Launch an instance of a program."""
|
|
288
|
+
msg = {"type": "launch_instance", "program_hash": program_hash, "arguments": arguments or []}
|
|
289
|
+
successful, result = await self._send_msg_and_wait(msg)
|
|
290
|
+
if successful:
|
|
291
|
+
instance_id = result
|
|
292
|
+
# Create the queue as before
|
|
293
|
+
queue = asyncio.Queue()
|
|
294
|
+
self.inst_event_queues[instance_id] = queue
|
|
295
|
+
|
|
296
|
+
# 🔻 FIX 3/3: Check for and replay any events that arrived early.
|
|
297
|
+
if instance_id in self.orphan_events:
|
|
298
|
+
early_events = self.orphan_events.pop(instance_id)
|
|
299
|
+
for event_tuple in early_events:
|
|
300
|
+
await queue.put(event_tuple)
|
|
301
|
+
|
|
302
|
+
return Instance(self, instance_id)
|
|
303
|
+
raise Exception(f"Failed to launch instance: {result}")
|
|
304
|
+
|
|
305
|
+
async def launch_server_instance(self, program_hash: str, port: int, arguments: list[str] = None):
|
|
306
|
+
"""Launch a server instance of a program on a specific port."""
|
|
307
|
+
msg = {"type": "launch_server_instance", "port": port, "program_hash": program_hash, "arguments": arguments or []}
|
|
308
|
+
successful, result = await self._send_msg_and_wait(msg)
|
|
309
|
+
if not successful:
|
|
310
|
+
raise Exception(f"Failed to launch server instance: {result}")
|
|
311
|
+
|
|
312
|
+
async def signal_instance(self, instance_id: str, message: str):
|
|
313
|
+
"""Send a signal/message to a running instance (fire-and-forget)."""
|
|
314
|
+
msg = {"type": "signal_instance", "instance_id": instance_id, "message": message}
|
|
315
|
+
await self.ws.send(msgpack.packb(msg, use_bin_type=True))
|
|
316
|
+
|
|
317
|
+
async def terminate_instance(self, instance_id: str):
|
|
318
|
+
"""Request the server to terminate a running instance (fire-and-forget)."""
|
|
319
|
+
msg = {"type": "terminate_instance", "instance_id": instance_id}
|
|
320
|
+
await self.ws.send(msgpack.packb(msg, use_bin_type=True))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _compile_rust_sync(rust_code: str, cargo_toml_content: str, package_name: str) -> bytes:
|
|
324
|
+
"""[Internal Synchronous Helper] Compiles rust code in a temporary directory."""
|
|
325
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
326
|
+
project_path = Path(temp_dir)
|
|
327
|
+
(project_path / "src").mkdir()
|
|
328
|
+
(project_path / "Cargo.toml").write_text(cargo_toml_content)
|
|
329
|
+
(project_path / "src" / "lib.rs").write_text(rust_code)
|
|
330
|
+
command = ["cargo", "build", "--target", "wasm32-wasip2", "--release"]
|
|
331
|
+
try:
|
|
332
|
+
print(f"🚀 Compiling crate '{package_name}'...")
|
|
333
|
+
subprocess.run(command, cwd=project_path, check=True, capture_output=True, text=True)
|
|
334
|
+
except FileNotFoundError:
|
|
335
|
+
raise RuntimeError("Error: `cargo` not found. Is Rust installed? Try: `rustup target add wasm32-wasip2`")
|
|
336
|
+
except subprocess.CalledProcessError as e:
|
|
337
|
+
raise RuntimeError(f"❌ Rust compilation failed.\n--- COMPILER OUTPUT ---\n{e.stderr}")
|
|
338
|
+
wasm_file_name = f"{package_name.replace('-', '_')}.wasm"
|
|
339
|
+
wasm_path = project_path / "target" / "wasm32-wasip2" / "release" / wasm_file_name
|
|
340
|
+
if not wasm_path.exists():
|
|
341
|
+
raise RuntimeError(f"Build succeeded but could not find WASM file at {wasm_path}")
|
|
342
|
+
print("✅ Compilation successful! Reading WASM binary.")
|
|
343
|
+
return wasm_path.read_bytes()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
async def compile_program(source: str | Path, dependencies: list[str]) -> bytes:
|
|
347
|
+
"""Compiles Rust source into a WASM binary and returns the bytes."""
|
|
348
|
+
if isinstance(source, Path) or (isinstance(source, str) and source.endswith('.rs')):
|
|
349
|
+
rust_code = Path(source).read_text()
|
|
350
|
+
else:
|
|
351
|
+
rust_code = source
|
|
352
|
+
package_name = f"pie-temp-crate-{uuid.uuid4().hex[:8]}"
|
|
353
|
+
deps_str = "\n".join(dependencies)
|
|
354
|
+
cargo_toml_content = f"""
|
|
355
|
+
[package]
|
|
356
|
+
name = "{package_name}"
|
|
357
|
+
version = "0.1.0"
|
|
358
|
+
edition = "2021"
|
|
359
|
+
[lib]
|
|
360
|
+
crate-type = ["cdylib"]
|
|
361
|
+
[dependencies]
|
|
362
|
+
{deps_str}
|
|
363
|
+
"""
|
|
364
|
+
loop = asyncio.get_running_loop()
|
|
365
|
+
return await loop.run_in_executor(None, _compile_rust_sync, rust_code, cargo_toml_content, package_name)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pie-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pie Client
|
|
5
|
+
Author-email: In Gim <in.gim@yale.edu>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/pie-project/pie
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: websockets==11.0.3
|
|
11
|
+
Requires-Dist: msgpack==0.5.6
|
|
12
|
+
Requires-Dist: blake3==1.0.4
|
|
13
|
+
|
|
14
|
+
# PIE Python Client
|
|
15
|
+
|
|
16
|
+
A concise Python toolkit to interact with the Symphony server:
|
|
17
|
+
|
|
18
|
+
- Upload LIP (\*.wasm) binaries
|
|
19
|
+
- Check if a LIP is already uploaded
|
|
20
|
+
- Launch a LIP on Symphony
|
|
21
|
+
- Interact with running LIP instances in real-time (send/receive messages, terminate, etc.)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
```bash
|
|
25
|
+
pip install -r requirements.txt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
Refer to [main.py](./main.py) for a usage example.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pie
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pie-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pie Client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "In Gim", email = "in.gim@yale.edu" },
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"websockets==11.0.3",
|
|
17
|
+
"msgpack==0.5.6",
|
|
18
|
+
"blake3==1.0.4",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/pie-project/pie"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ["pie"]
|
|
26
|
+
license-files = []
|