Skip to content

Commit 7da136f

Browse files
committed
[python] adds download/upload and clean disconnection
1 parent e62ba2b commit 7da136f

File tree

7 files changed

+157
-6
lines changed

7 files changed

+157
-6
lines changed

python/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Gama client
2-
Gama-client is a python wrapper for interacting with the headless mode (called gama-server) of the modeling and simulation platform [gama](https://gama-platform.org/). The latest release is compatible with gama 1.9.1.
2+
Gama-client is a python wrapper for interacting with the headless mode (called gama-server) of the modeling and simulation platform [gama](https://gama-platform.org/). The latest release is compatible with gama 1.9.2.
33
This wrapper will take care of the connection with gama-server and of sending properly formatted requests to gama-server. It is made to fit the asynchronous nature of gama-server and thus makes it possible to handle multiple simulations at the same time, but the counterpart is that the users will still have to manage what to do with the received messages (command confirmation, simulation output, errors etc.) by themselves. We provide a working example that shows the architecture you can deploy if you still want to have a sequential execution.
44

55
# Installation
@@ -70,6 +70,12 @@ await client.connect()
7070
```
7171
And the client will take care of handling this first message and setting the `socket_id` by itself.
7272

73+
To explicitly disconnect from gama-server, simply use the `close_connection` function:
74+
```python
75+
await client.close_connection()
76+
```
77+
you can later reconnect with the same `client` object the same way you did the first time, with the `connect` function.
78+
7379
### Running commands
7480
Once connected you will want to run commands, the principle is pretty simple: all commands can be run your `client` variable through functions. For example if you want to run the [load command](https://gama-platform.org/wiki/next/HeadlessServer#the-load-command) you just have to call the `load` function with the proper parameters.
7581
```python
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
some random text
2+
multi-line of course
3+
4+
look at me I'm coding:
5+
print("hello world")
6+
7+
TADA !
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
model snap
3+
4+
global {
5+
6+
init{
7+
create dummy number:10;
8+
}
9+
reflex f {
10+
save snapshot("d") to:"test.png";
11+
}
12+
}
13+
14+
species dummy {
15+
16+
}
17+
18+
19+
experiment name type: gui {
20+
21+
output {
22+
display "d" type:3d{
23+
species dummy;
24+
}
25+
26+
}
27+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# tests the new functionalities of uploading/downloading and closing the connection
2+
3+
import asyncio
4+
from asyncio import Future
5+
from pathlib import Path
6+
from typing import Dict
7+
8+
from gama_client.base_client import GamaBaseClient
9+
from gama_client.command_types import CommandTypes
10+
from gama_client.message_types import MessageTypes
11+
12+
download_future: Future
13+
upload_future: Future
14+
load_future: Future
15+
16+
17+
async def message_handler(message: Dict):
18+
print("received", message)
19+
if "command" in message:
20+
if message["command"]["type"] == CommandTypes.Download.value:
21+
download_future.set_result(message)
22+
elif message["command"]["type"] == CommandTypes.Upload.value:
23+
upload_future.set_result(message)
24+
elif message["command"]["type"] == CommandTypes.Load.value:
25+
load_future.set_result(message)
26+
27+
28+
async def main():
29+
global download_future, upload_future, load_future
30+
31+
client = GamaBaseClient("localhost", 6868, message_handler)
32+
await client.connect()
33+
34+
file_to_download = str(Path(__file__).parents[0] / "file_to_download.txt")
35+
file_to_upload = str(Path(__file__).parents[0] / "uploaded.txt")
36+
gaml_model = str(Path(__file__).parents[0] / "test_gaml_file.gaml")
37+
38+
download_future = asyncio.get_running_loop().create_future()
39+
upload_future = asyncio.get_running_loop().create_future()
40+
load_future = asyncio.get_running_loop().create_future()
41+
42+
await client.download(file_to_download)
43+
ftd_content = await download_future
44+
if ftd_content["type"] != MessageTypes.CommandExecutedSuccessfully.value:
45+
print("error while trying to download a file", ftd_content)
46+
return
47+
print("file downloaded successfully from gama-server, here is its content:", ftd_content["content"])
48+
49+
await client.upload(file_to_upload, "here is my secret:\n\n\n\n\n\n\n\n\n\nGOTCHA")
50+
up_result = await upload_future
51+
if up_result["type"] != MessageTypes.CommandExecutedSuccessfully.value:
52+
print("error while trying to upload a file", up_result)
53+
return
54+
# after that, a new file should have appeared next to this code file, containing our text
55+
print("upload successful, about to close connection and do other stuff now")
56+
57+
await client.close_connection()
58+
59+
# leave some time to check on gs if correctly disconnected
60+
await asyncio.sleep(15)
61+
62+
# reconnect and execute a random command to check that everything is fine
63+
await client.connect()
64+
await client.load(gaml_model, "name")
65+
load_result = await upload_future
66+
if load_result["type"] != MessageTypes.CommandExecutedSuccessfully.value:
67+
print("error while trying to load the gaml model", load_result)
68+
return
69+
70+
print("everything worked as expected, exiting the program")
71+
72+
if __name__ == "__main__":
73+
asyncio.run(main())

python/gama_client/base_client.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def start_listening_loop(self, handle_connection_message: bool, timeout: f
6464
:param timeout: timeout for reading the next message
6565
:return: Never returns
6666
"""
67-
while True:
67+
while self.socket.open:
6868
try:
6969
mess = await asyncio.wait_for(self.socket.recv(), timeout=timeout)
7070
try:
@@ -80,8 +80,9 @@ async def start_listening_loop(self, handle_connection_message: bool, timeout: f
8080
except Exception as js_ex:
8181
print("Unable to unpack gama-server messages as a json. Error:", js_ex, "Message received:", mess)
8282
except Exception as sock_ex:
83-
print("Error while waiting for a message from gama-server. Exiting", sock_ex)
84-
sys.exit(-1)
83+
if self.socket.open:
84+
print("Error while waiting for a message from gama-server. Exiting", sock_ex)
85+
sys.exit(-1)
8586

8687
async def load(self, file_path: str, experiment_name: str, console: bool = None, status: bool = None,
8788
dialog: bool = None, runtime: bool = None, parameters: List[Dict] = None, until: str = "", socket_id: str = "",
@@ -152,6 +153,41 @@ async def exit(self):
152153
}
153154
await self.socket.send(json.dumps(cmd))
154155

156+
async def download(self, file_path: str):
157+
"""
158+
Downloads a file from gama server file system
159+
:type file_path: the path of the file to download on gama-server's file system
160+
:return: if everything goes well, gama-server will send back an object containing the entirety
161+
of the file as a string
162+
"""
163+
cmd = {
164+
"type": CommandTypes.Download.value,
165+
"file": file_path,
166+
}
167+
await self.socket.send(json.dumps(cmd))
168+
169+
async def upload(self, file_path: str, content: str):
170+
"""
171+
Uploads a file to gama-server's file-system
172+
:param file_path: the path on gama-server file-system where the content is going to be saved
173+
:param content: the content of the file to be uploaded
174+
"""
175+
cmd = {
176+
"type": CommandTypes.Upload.value,
177+
"file": file_path,
178+
"content": content
179+
}
180+
await self.socket.send(json.dumps(cmd))
181+
182+
async def close_connection(self, close_code=1000, reason=""):
183+
"""
184+
Closes the connection
185+
:param close_code: the close code, 1000 by default
186+
:param reason: a human-readable reason for closing.
187+
:return:
188+
"""
189+
await self.socket.close(close_code, reason)
190+
155191
async def play(self, exp_id: str, sync: bool = None, socket_id: str = "", additional_data: Dict = None):
156192
"""
157193
Sends a command to run the experiment **exp_id**

python/gama_client/command_types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ class CommandTypes(Enum):
1111
StepBack = "stepBack"
1212
Reload = "reload"
1313
Stop = "stop"
14-
Expression = "expression"
14+
Expression = "expression"
15+
Download = "download"
16+
Upload = "upload"

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["hatchling"]
33
build-backend = "hatchling.build"
44
[project]
55
name = "gama_client"
6-
version = "1.1.3"
6+
version = "1.1.4"
77
authors = [
88
{ name="Baptiste Lesquoy", email="[email protected]" },
99
]

0 commit comments

Comments
 (0)