experiment_server 0.3.0__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.
- experiment_server/__init__.py +31 -0
- experiment_server/_api.py +200 -0
- experiment_server/_client.py +76 -0
- experiment_server/_participant_ordering.py +127 -0
- experiment_server/_process_config.py +348 -0
- experiment_server/_server.py +403 -0
- experiment_server/cli.py +72 -0
- experiment_server/static/css/bootstrap-5.2.3.min.css +7 -0
- experiment_server/static/index.html +217 -0
- experiment_server/static/initconfig.html +143 -0
- experiment_server/static/js/alpinejs3.min.js +5 -0
- experiment_server/static/js/bootstrap-4.5.0.min.js +7 -0
- experiment_server/static/js/fontawesome-1e694dd391.js +2 -0
- experiment_server/static/js/htmx-1.9.10.js +1 -0
- experiment_server/static/js/jquery-3.5.1.min.js +2 -0
- experiment_server/static/js/popper-1.16.0.min.js +5 -0
- experiment_server/static/js/sweetalert2-11.js +6 -0
- experiment_server/utils.py +99 -0
- experiment_server-0.3.0.dist-info/LICENSE.md +21 -0
- experiment_server-0.3.0.dist-info/METADATA +295 -0
- experiment_server-0.3.0.dist-info/RECORD +24 -0
- experiment_server-0.3.0.dist-info/WHEEL +4 -0
- experiment_server-0.3.0.dist-info/entry_points.txt +3 -0
- sample_config.toml +57 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Main script."""
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from multiprocessing import Process
|
|
7
|
+
|
|
8
|
+
from tornado.web import RequestHandler, Application, StaticFileHandler
|
|
9
|
+
import tornado.ioloop
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from experiment_server._api import Experiment
|
|
14
|
+
from experiment_server.utils import ExperimentServerConfigurationExcetion, ExperimentServerExcetion
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _create_app(default_participant_index, config_file):
|
|
18
|
+
resource_parameters = {"experiment": Experiment(config_file, default_participant_index)}
|
|
19
|
+
|
|
20
|
+
static_location = (Path(__file__).parent / "static" ).absolute()
|
|
21
|
+
|
|
22
|
+
application = Application([
|
|
23
|
+
(r"/()",StaticFileHandler, {'path': str(static_location / "index.html")}),
|
|
24
|
+
(r"/index()",StaticFileHandler, {'path': str(static_location / "index.html")}),
|
|
25
|
+
(r"/web/([^/]+)", WebHandler, resource_parameters),
|
|
26
|
+
(r"/web/([^/]+)/([0-9]+)", WebHandler, resource_parameters),
|
|
27
|
+
(r"/api/([^/]+)", ExperimentHandler, resource_parameters),
|
|
28
|
+
(r"/api/([^/]+)/([0-9]+)", ExperimentHandler, resource_parameters),
|
|
29
|
+
(r"/api/([^/]+)/([0-9]+)/([0-9]+)", ExperimentHandler, resource_parameters),
|
|
30
|
+
(r"/(.*)",StaticFileHandler, {'path': static_location, 'default_filename': "index.html"})
|
|
31
|
+
])
|
|
32
|
+
return application
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _init_api(config_file, default_participant_index, host="127.0.0.1", port=5000):
|
|
36
|
+
application = _create_app(default_participant_index, config_file)
|
|
37
|
+
application.listen(port=port, address=host)
|
|
38
|
+
await asyncio.Event().wait()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _server(config_file, default_participant_index, host="127.0.0.1", port=5000):
|
|
42
|
+
asyncio.run(_init_api(config_file, default_participant_index, host, port))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def server_process(config_file, default_participant_index=None, host="127.0.0.1", port="5000"):
|
|
46
|
+
p = Process(target=_server,
|
|
47
|
+
kwargs={
|
|
48
|
+
"default_participant_index":default_participant_index,
|
|
49
|
+
"host":host, "port":port, "config_file":config_file
|
|
50
|
+
})
|
|
51
|
+
return p
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WebHandler(RequestHandler):
|
|
55
|
+
def initialize(self, experiment:Experiment):
|
|
56
|
+
self.experiment = experiment
|
|
57
|
+
self.output_written: bool = False
|
|
58
|
+
|
|
59
|
+
def write_to_output(self, message):
|
|
60
|
+
self.write(f"<div hx-swap-oob=\"innerHTML:#output\">{message}</div>")
|
|
61
|
+
|
|
62
|
+
def write_alert(self, alert_type, message):
|
|
63
|
+
self.write_to_output(f"<div class=\"alert alert-{alert_type}\">{message}</div>")
|
|
64
|
+
|
|
65
|
+
def write_info(self, message):
|
|
66
|
+
self.write_alert("info", message)
|
|
67
|
+
|
|
68
|
+
def write_warn(self, message):
|
|
69
|
+
self.write_alert("warning", message)
|
|
70
|
+
|
|
71
|
+
def write_danger(self, message):
|
|
72
|
+
self.write_alert("danger", message)
|
|
73
|
+
|
|
74
|
+
def write_empty_status(self):
|
|
75
|
+
self.write_status_string(None, True)
|
|
76
|
+
|
|
77
|
+
def write_status_string(self, participant_id=None, no_message=False):
|
|
78
|
+
if not no_message:
|
|
79
|
+
message = self.experiment.get_participant_state(participant_id).status_string().replace("\n", " ")
|
|
80
|
+
else:
|
|
81
|
+
message = ""
|
|
82
|
+
self.write(f"<div hx-swap-oob=\"innerHTML:#status\">{message}</div>")
|
|
83
|
+
|
|
84
|
+
def _process_participant_id(self):
|
|
85
|
+
participant_id = self.get_argument("txtPPID", self.experiment.default_participant_index, True)
|
|
86
|
+
use_default = self.get_argument("checkUseDefult", "off", True)
|
|
87
|
+
|
|
88
|
+
if use_default == "on":
|
|
89
|
+
participant_id = None
|
|
90
|
+
else:
|
|
91
|
+
try:
|
|
92
|
+
participant_id = int(participant_id)
|
|
93
|
+
except ValueError:
|
|
94
|
+
participant_id = self.experiment.default_participant_index
|
|
95
|
+
|
|
96
|
+
if participant_id is not None and participant_id not in self.experiment.global_state:
|
|
97
|
+
self.write_danger(f"Participant with ID {participant_id} not known. Consider initializing new participant.")
|
|
98
|
+
self.write_empty_status()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
return participant_id
|
|
102
|
+
|
|
103
|
+
def _get_config_table(self, config) -> str:
|
|
104
|
+
table_output = "<table class=\"table\"><tr><th>key</th><th>value</th></tr>"
|
|
105
|
+
for k,v in config.items():
|
|
106
|
+
table_output += f"<tr><td>{k}</td><td>{v}</td></tr>"
|
|
107
|
+
btn = """<button type="button" class="btn btn-primary"
|
|
108
|
+
hx-get="/web/config-editable"
|
|
109
|
+
hx-trigger="click"
|
|
110
|
+
hx-include="#checkUseDefault,#txtPPID"
|
|
111
|
+
hx-swap="none"/>Edit</button>"""
|
|
112
|
+
|
|
113
|
+
table_output += f"<tr><td></td><td>{btn}</td></tr>"
|
|
114
|
+
table_output += "</table>"
|
|
115
|
+
return table_output
|
|
116
|
+
|
|
117
|
+
def _get_editable_config_table(self, config) -> str:
|
|
118
|
+
table_output = """<form hx-post="/web/update-config" hx-include="#checkUseDefault,#txtPPID"><table class="table"><tr><th>key</th><th>value</th></tr>"""
|
|
119
|
+
for k,v in config.items():
|
|
120
|
+
if k in ["participant_index", "block_id", "name"]:
|
|
121
|
+
table_output += f"<tr><td>{k}</td><td>{v}</td></tr>"
|
|
122
|
+
else:
|
|
123
|
+
_name = f"_c_{k}"
|
|
124
|
+
table_output += f"<tr><td><label for={_name}>{k}</label></td><td><input type=\"text\" id={_name} name={_name} placeholder=\"{v}\"></td></tr>"
|
|
125
|
+
|
|
126
|
+
btn = """<button type="submit" class="btn btn-primary" />Submit</button>
|
|
127
|
+
<button type="button" class="btn btn-secondary"
|
|
128
|
+
hx-get="/web/config"
|
|
129
|
+
hx-trigger="click"
|
|
130
|
+
hx-include="#checkUseDefault,#txtPPID"
|
|
131
|
+
hx-swap="none"/>Cancel</button>"""
|
|
132
|
+
|
|
133
|
+
table_output += f"<tr><td></td><td>{btn}</td></tr>"
|
|
134
|
+
table_output += "</table></form>"
|
|
135
|
+
return table_output
|
|
136
|
+
|
|
137
|
+
# NOTE: I am abusing the GET here!
|
|
138
|
+
def get(self, action=None):
|
|
139
|
+
if action in ["status-string", "acive-participant-change", "config", "config-editable", "reset-participant", "move-to-block", "move-to-next"]:
|
|
140
|
+
participant_id = self._process_participant_id()
|
|
141
|
+
|
|
142
|
+
if action == "status-string":
|
|
143
|
+
self.write_status_string(participant_id)
|
|
144
|
+
|
|
145
|
+
elif action == "acive-participant-change":
|
|
146
|
+
self.write_status_string(participant_id)
|
|
147
|
+
self.write_to_output("")
|
|
148
|
+
|
|
149
|
+
elif action == "config":
|
|
150
|
+
config = self.experiment.get_config(participant_id)
|
|
151
|
+
if config is not None:
|
|
152
|
+
self.write_info(self._get_config_table(config))
|
|
153
|
+
else:
|
|
154
|
+
self.write_warn(f"participant {participant_id} not active. A call to `/move-to-next` must be made before calling `/config`")
|
|
155
|
+
|
|
156
|
+
elif action == "config-editable":
|
|
157
|
+
config = self.experiment.get_config(participant_id)
|
|
158
|
+
if config is not None:
|
|
159
|
+
self.write_info(self._get_editable_config_table(config))
|
|
160
|
+
else:
|
|
161
|
+
self.write_warn(f"participant {participant_id} not active. A call to `/move-to-next` must be made before calling `/config`")
|
|
162
|
+
|
|
163
|
+
elif action == "reset-participant":
|
|
164
|
+
self.experiment.reset_participant(participant_id)
|
|
165
|
+
_str = f"index {participant_id}" if participant_id is not None else "default index"
|
|
166
|
+
self.write_info(f"Reset config for all blocks for participant with {_str}")
|
|
167
|
+
|
|
168
|
+
elif action == "move-to-block":
|
|
169
|
+
new_block_id = self.get_argument("txtBlockID", "-", True)
|
|
170
|
+
try:
|
|
171
|
+
new_block_id = int(new_block_id)
|
|
172
|
+
except ValueError:
|
|
173
|
+
self.write_danger("Invaid input")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
new_block_name = self.experiment.move_to_block(new_block_id, participant_id)
|
|
178
|
+
self.write_info(f"Moved to block: {new_block_name}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.write_danger(e)
|
|
181
|
+
self.write_status_string(participant_id)
|
|
182
|
+
|
|
183
|
+
elif action == "move-to-next":
|
|
184
|
+
try:
|
|
185
|
+
new_block_name = self.experiment.move_to_next(participant_id)
|
|
186
|
+
self.write_info(f"Moved to block: {new_block_name}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
self.write_danger(e)
|
|
189
|
+
self.write_status_string(participant_id)
|
|
190
|
+
|
|
191
|
+
elif action == "move-all-to-block":
|
|
192
|
+
new_block_id = self.get_argument("txtAllBlockID", "-", True)
|
|
193
|
+
try:
|
|
194
|
+
new_block_id = int(new_block_id)
|
|
195
|
+
except ValueError:
|
|
196
|
+
self.write_danger("Invaid input")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
new_block_name = self.experiment.move_all_to_block(new_block_id)
|
|
201
|
+
self.write_info(f"Moved all to block: {new_block_name}")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
self.write_danger(e)
|
|
204
|
+
|
|
205
|
+
elif action == "new-participant":
|
|
206
|
+
self.write_info("New participant id added: " + str(self.experiment.get_next_participant()))
|
|
207
|
+
|
|
208
|
+
elif action == "add-participant":
|
|
209
|
+
new_participant_id = self.get_argument("newPPID", "-", True)
|
|
210
|
+
try:
|
|
211
|
+
new_participant_id = int(new_participant_id)
|
|
212
|
+
except ValueError:
|
|
213
|
+
self.write_danger("Invaid input")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
added_participant = self.experiment.add_participant_index(new_participant_id)
|
|
217
|
+
if not added_participant:
|
|
218
|
+
self.write_warn(f"Participant id {new_participant_id} already exists.")
|
|
219
|
+
else:
|
|
220
|
+
self.write_info(f"Added new participant with id: {new_participant_id}")
|
|
221
|
+
|
|
222
|
+
elif action == "list-participants":
|
|
223
|
+
table_output = "<table class=\"table\"><tr><th>participant ID</th><th>Block ID</th><th>Block Name</th></tr>"
|
|
224
|
+
for idx, state in self.experiment.global_state.items():
|
|
225
|
+
table_output += f"<tr><td>{idx}</td><td>{state.block_id}</td><td>{state.block_name}</td></tr>"
|
|
226
|
+
table_output += "</table>"
|
|
227
|
+
self.write_info(table_output)
|
|
228
|
+
|
|
229
|
+
def post(self, action=None):
|
|
230
|
+
participant_id = self._process_participant_id()
|
|
231
|
+
|
|
232
|
+
if action == "update-config":
|
|
233
|
+
valid_submission = True
|
|
234
|
+
config = self.experiment.get_config(participant_id)
|
|
235
|
+
if config is not None:
|
|
236
|
+
for key in config.keys():
|
|
237
|
+
if key in ["participant_index", "block_id", "name"]:
|
|
238
|
+
continue
|
|
239
|
+
try:
|
|
240
|
+
param_key = f"_c_{key}"
|
|
241
|
+
new_value = self.get_argument(param_key)
|
|
242
|
+
if len(new_value) == 0:
|
|
243
|
+
continue # value was not set!
|
|
244
|
+
new_value = json.loads(new_value)
|
|
245
|
+
config[key] = new_value # its a dict! it's a reference value!
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.exception(f"Failed to process key {key} with {e}")
|
|
248
|
+
logger.error(f"Failed to process key {key} with {e}")
|
|
249
|
+
valid_submission = False
|
|
250
|
+
|
|
251
|
+
if valid_submission:
|
|
252
|
+
output = "<b>Update Successful</b></br>"
|
|
253
|
+
output += self._get_config_table(config)
|
|
254
|
+
self.write_info(output)
|
|
255
|
+
else:
|
|
256
|
+
output = "<b>Update Failed</b></br>"
|
|
257
|
+
output += self._get_editable_config_table(config)
|
|
258
|
+
self.write_warn(output)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ExperimentHandler(RequestHandler):
|
|
262
|
+
def initialize(self, experiment:Experiment):
|
|
263
|
+
self.experiment = experiment
|
|
264
|
+
|
|
265
|
+
def get(self, action=None, param=None):
|
|
266
|
+
# The experiment methods treat None as default pp
|
|
267
|
+
if param is not None:
|
|
268
|
+
participant_id = self._get_int_from_param(param)
|
|
269
|
+
else:
|
|
270
|
+
participant_id = None
|
|
271
|
+
|
|
272
|
+
if participant_id is not None and participant_id not in self.experiment.global_state:
|
|
273
|
+
self.write(f"Participant with ID {participant_id} not known. Consider initializing new participant.")
|
|
274
|
+
self.set_status(406)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if action == "blocks-count":
|
|
278
|
+
self.write(json.dumps(self.experiment.get_blocks_count()))
|
|
279
|
+
elif action == "block-id":
|
|
280
|
+
self.write(json.dumps(self.experiment.get_participant_state(participant_id).block_id))
|
|
281
|
+
elif action == "active":
|
|
282
|
+
self.write(json.dumps(self.experiment.get_state(participant_id)))
|
|
283
|
+
elif action == "config":
|
|
284
|
+
config = self.experiment.get_config(participant_id)
|
|
285
|
+
if config is not None:
|
|
286
|
+
logger.info(f"Config returned: {config}")
|
|
287
|
+
self.write(json.dumps(config, indent=4))
|
|
288
|
+
else:
|
|
289
|
+
self.set_status(406)
|
|
290
|
+
self.write(f"participant {participant_id} not active. A call to `/move-to-next` must be made before calling `/config`")
|
|
291
|
+
elif action == "summary-data":
|
|
292
|
+
self.write({
|
|
293
|
+
"participant_index": participant_id if participant_id is not None else self.experiment.default_participant_index,
|
|
294
|
+
"configs_length": self.experiment.get_blocks_count(participant_id)
|
|
295
|
+
})
|
|
296
|
+
elif action == "all-configs":
|
|
297
|
+
self.write(json.dumps(self.experiment.get_all_configs(participant_id), indent=4))
|
|
298
|
+
elif action == "status-string":
|
|
299
|
+
self.write(self.experiment.get_participant_state(participant_id).status_string().replace("\n", " "))
|
|
300
|
+
else:
|
|
301
|
+
self.set_status(404)
|
|
302
|
+
self.write("N/A")
|
|
303
|
+
|
|
304
|
+
def post(self, action=None, param1=None, param2=None):
|
|
305
|
+
if action == "move-to-next":
|
|
306
|
+
if param2 is not None:
|
|
307
|
+
self.set_status(404)
|
|
308
|
+
self.write(f"unknown second parameter {param2}")
|
|
309
|
+
if param1 is not None:
|
|
310
|
+
participant_id = self._get_int_from_param(param1)
|
|
311
|
+
else:
|
|
312
|
+
participant_id = None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
block_name = self.experiment.move_to_next(participant_id)
|
|
316
|
+
logger.info(f"Loading block: {self.experiment.get_participant_state(participant_id).block}\n")
|
|
317
|
+
self.write({"name": block_name})
|
|
318
|
+
except KeyError:
|
|
319
|
+
self.write(f"Participant with ID {participant_id} not known. Consider initializing new participant.")
|
|
320
|
+
self.set_status(406)
|
|
321
|
+
|
|
322
|
+
elif action == "move-to-block":
|
|
323
|
+
if param1 is None and param2 is None:
|
|
324
|
+
self.set_status(404)
|
|
325
|
+
self.write("Need atleast one paramter.")
|
|
326
|
+
return
|
|
327
|
+
elif param2 is None:
|
|
328
|
+
participant_id = None
|
|
329
|
+
new_block_id = self._get_int_from_param(param1)
|
|
330
|
+
else:
|
|
331
|
+
participant_id = self._get_int_from_param(param1)
|
|
332
|
+
new_block_id = self._get_int_from_param(param2)
|
|
333
|
+
if new_block_id is not None:
|
|
334
|
+
try:
|
|
335
|
+
if new_block_id >= self.experiment.get_blocks_count(participant_id) or new_block_id < 0:
|
|
336
|
+
self.set_status(404)
|
|
337
|
+
self.write("param should be >= 0 and < " + str(self.experiment.get_blocks_count(participant_id)))
|
|
338
|
+
else:
|
|
339
|
+
self.experiment.move_to_block(new_block_id, participant_id)
|
|
340
|
+
self.write(str(new_block_id))
|
|
341
|
+
except KeyError:
|
|
342
|
+
self.write(f"Participant with ID {participant_id} not known. Consider initializing new participant.")
|
|
343
|
+
self.set_status(406)
|
|
344
|
+
elif action == "move-all-to-block":
|
|
345
|
+
if param2 is not None:
|
|
346
|
+
self.set_status(404)
|
|
347
|
+
self.write(f"unknown second parameter {param2}")
|
|
348
|
+
return
|
|
349
|
+
new_block_id = self._get_int_from_param(param1)
|
|
350
|
+
if new_block_id is not None:
|
|
351
|
+
if new_block_id >= self.experiment.get_blocks_count() or new_block_id < 0:
|
|
352
|
+
self.set_status(404)
|
|
353
|
+
self.write("param should be >= 0 and < " + str(self.experiment.get_blocks_count()))
|
|
354
|
+
else:
|
|
355
|
+
self.experiment.move_all_to_block(new_block_id)
|
|
356
|
+
self.write(str(new_block_id))
|
|
357
|
+
elif action == "shutdown":
|
|
358
|
+
self.experiment.watchdog.end_watch()
|
|
359
|
+
shutdown_server()
|
|
360
|
+
else:
|
|
361
|
+
self.set_status(404)
|
|
362
|
+
self.write("n/a")
|
|
363
|
+
|
|
364
|
+
def put(self, action=None, param=None):
|
|
365
|
+
if action == "new-participant":
|
|
366
|
+
if param != None:
|
|
367
|
+
self.set_status(406)
|
|
368
|
+
self.write("`new-participant` doesn't take params")
|
|
369
|
+
else:
|
|
370
|
+
self.write(str(self.experiment.get_next_participant()))
|
|
371
|
+
elif action == "add-participant":
|
|
372
|
+
participant_id = self._get_int_from_param(param)
|
|
373
|
+
if participant_id is not None:
|
|
374
|
+
try:
|
|
375
|
+
added_participant = self.experiment.add_participant_index(participant_id)
|
|
376
|
+
if not added_participant:
|
|
377
|
+
self.set_status(406)
|
|
378
|
+
self.write(json.dumps(added_participant))
|
|
379
|
+
except ExperimentServerConfigurationExcetion as e:
|
|
380
|
+
self.set_status(406)
|
|
381
|
+
self.write(e.args[0][0])
|
|
382
|
+
|
|
383
|
+
def _get_int_from_param(self, param):
|
|
384
|
+
try:
|
|
385
|
+
param = int(param)
|
|
386
|
+
return param
|
|
387
|
+
except ValueError:
|
|
388
|
+
self.set_status(404)
|
|
389
|
+
self.write(f"param should be an integer, got {param}, processing as None")
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# # From: https://stackoverflow.com/questions/15562446/how-to-stop-flask-application-without-using-ctrl-c
|
|
394
|
+
# def shutdown_server():
|
|
395
|
+
# func = request.environ.get('werkzeug.server.shutdown')
|
|
396
|
+
# if func is None:
|
|
397
|
+
# raise RuntimeError('Not running with the Werkzeug Server')
|
|
398
|
+
# func()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# from https://stackoverflow.com/questions/5375220/how-do-i-stop-tornado-web-server
|
|
402
|
+
def shutdown_server():
|
|
403
|
+
tornado.ioloop.IOLoop.instance().stop()
|
experiment_server/cli.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""CLI."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from click_aliases import ClickAliasedGroup
|
|
7
|
+
|
|
8
|
+
from experiment_server._server import _server
|
|
9
|
+
from experiment_server._process_config import verify_config
|
|
10
|
+
from experiment_server._api import _generate_config_json
|
|
11
|
+
from experiment_server.utils import ExperimentServerExcetion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(cls=ClickAliasedGroup)
|
|
15
|
+
def cli():
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cli.command(aliases=["r"])
|
|
20
|
+
@click.argument("config-file")
|
|
21
|
+
@click.option("-i", "--default-participant-index", default=1, type=click.IntRange(min=1, max_open=True))
|
|
22
|
+
@click.option("-h", "--host", default='127.0.0.1')
|
|
23
|
+
@click.option("-p", "--port", default='5000')
|
|
24
|
+
def run(default_participant_index, config_file, host, port):
|
|
25
|
+
"""Launch server with the `config-file` used to setup the configurations"""
|
|
26
|
+
_server(default_participant_index=default_participant_index if default_participant_index > 0 else None, host=host, port=port, config_file=config_file)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command(aliases=["v", "verify"])
|
|
30
|
+
@click.argument("config-file", type=click.Path())
|
|
31
|
+
def verify_config_file(config_file):
|
|
32
|
+
"""Verify if the config-file provided is valid"""
|
|
33
|
+
verify_config(f=config_file)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command(aliases=["g", "generate"])
|
|
37
|
+
@click.argument("config-file", type=click.Path())
|
|
38
|
+
@click.option("-i", "--participant-index", default=None, type=int)
|
|
39
|
+
@click.option("-r", "--participant-range", default=None, type=int)
|
|
40
|
+
@click.option("-d", "--out-dir", default=None, type=click.Path(file_okay=False))
|
|
41
|
+
def generate_config_json(config_file, participant_index, participant_range, out_dir):
|
|
42
|
+
"""Generate json config files after processing config-file for participant_index or
|
|
43
|
+
till participant_range. If `out_location` is passed, it is expected to be a directory.
|
|
44
|
+
If not passed will write out the config's to stdout, one line per participant.
|
|
45
|
+
"""
|
|
46
|
+
if participant_index is None and participant_range is None:
|
|
47
|
+
logger.error("Both `participant-index` and `participant-range` cannot be empty.")
|
|
48
|
+
return
|
|
49
|
+
elif participant_index is not None and participant_range is not None:
|
|
50
|
+
logger.error("Both `participant-index` and `participant-range` provided. Ignoring `participant-index`.")
|
|
51
|
+
|
|
52
|
+
with logger.catch(ExperimentServerExcetion, reraise=False):
|
|
53
|
+
_generate_config_json(config_file=config_file, participant_indices=range(1, participant_range + 1) if participant_range is not None else [participant_index, ], out_dir=out_dir)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@cli.command(aliases=["n", "new"])
|
|
57
|
+
@click.argument("new-file-location")
|
|
58
|
+
def new_config_file(new_file_location):
|
|
59
|
+
"""Create a new config file."""
|
|
60
|
+
out_location = Path(new_file_location)
|
|
61
|
+
if out_location.is_dir():
|
|
62
|
+
out_location = out_location / "new_config.toml"
|
|
63
|
+
|
|
64
|
+
if out_location.exists():
|
|
65
|
+
logger.error(f"{out_location} already exists!")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
with open(Path(__file__).parent.parent / "sample_config.toml", "r") as in_f:
|
|
69
|
+
with open(out_location, "w") as out_f:
|
|
70
|
+
out_f.writelines(in_f.readlines())
|
|
71
|
+
|
|
72
|
+
logger.info(f"New config at: {out_location}")
|