experiment_server 0.3.7__tar.gz → 0.3.8__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.
- {experiment_server-0.3.7 → experiment_server-0.3.8}/PKG-INFO +62 -10
- {experiment_server-0.3.7 → experiment_server-0.3.8}/README.md +61 -9
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_api.py +4 -4
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_client.py +2 -2
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_participant_ordering.py +13 -13
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_process_config.py +52 -30
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_server.py +3 -3
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_ui.py +496 -105
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/cli.py +28 -10
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/css/app.tcss +89 -3
- experiment_server-0.3.8/experiment_server/temp.py +13 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/utils.py +20 -7
- {experiment_server-0.3.7 → experiment_server-0.3.8}/pyproject.toml +1 -1
- {experiment_server-0.3.7 → experiment_server-0.3.8}/LICENSE.md +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/__init__.py +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/css/bootstrap-5.2.3.min.css +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/index.html +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/initconfig.html +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/alpinejs3.min.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/bootstrap-4.5.0.min.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/fontawesome-1e694dd391.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/htmx-1.9.10.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/jquery-3.5.1.min.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/popper-1.16.0.min.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/sweetalert2-11.js +0 -0
- {experiment_server-0.3.7 → experiment_server-0.3.8}/sample_config.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: experiment_server
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Server for experiments to get configuarations from
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: experiment,study-design
|
|
@@ -77,7 +77,7 @@ $ poetry add experiment-server
|
|
|
77
77
|
|
|
78
78
|
A terminal-based user interface is provided for interactive management and monitoring of an experiment. It is useful when you want a lightweight, keyboard-driven interface to load configs, inspect/generate per‑participant configs, edit files, and control participant flow without using the web UI.
|
|
79
79
|
|
|
80
|
-
Launch the TUI with:
|
|
80
|
+
### Launch the TUI with:
|
|
81
81
|
|
|
82
82
|
```sh
|
|
83
83
|
# Start TUI (optionally provide a config file)
|
|
@@ -88,35 +88,77 @@ $ experiment-server ui -c sample_config.toml
|
|
|
88
88
|
$ experiment-server ui -c sample_config.toml -i 1 -h 127.0.0.1 -p 5000
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Options:
|
|
91
|
+
### Options:
|
|
92
|
+
|
|
92
93
|
- -c, --config-file : path to a .toml config to load on start (optional).
|
|
93
|
-
- -i, --default-participant-index : default participant index to use when no explicit participant id is provided (default 1).
|
|
94
|
-
- -h, --host : host/interface for the server (default 127.0.0.1).
|
|
95
|
-
- -p, --port : port for the server (default 5000).
|
|
96
94
|
|
|
97
|
-
|
|
95
|
+
- -i, --default-participant-index : default participant index to use when no explicit participant id is provided (default 1). (optional)
|
|
96
|
+
|
|
97
|
+
- -h, --host : host/interface for the server (default 127.0.0.1). (optional)
|
|
98
|
+
|
|
99
|
+
- -p, --port : port for the server (default 5000). (optional)
|
|
100
|
+
|
|
101
|
+
- --editor-only : Launch the editor only. "-i", "-h", and "-p" will be ignored. When launching ui with "--editor-only", the config file ("-c") must be passed as well. (optional)
|
|
102
|
+
|
|
103
|
+
### Behavior and features
|
|
104
|
+
|
|
105
|
+
- Startup config validation - When launching the full TUI (without --editor-only), if a config file is provided it is validated at startup. If validation fails, the TUI will refuse to start the full UI/server and will exit with an error message suggesting use of verify-config-file or the editor-only mode to edit the file.
|
|
106
|
+
|
|
98
107
|
- Can be started without a config file; load a config later from the built-in file browser (filters to directories and .toml files).
|
|
108
|
+
|
|
99
109
|
- When a config is loaded the package starts the server in the same event loop, exposing the same REST API described elsewhere in this README.
|
|
110
|
+
|
|
100
111
|
- Participant management:
|
|
112
|
+
|
|
101
113
|
- View all participants and their status, block id and block name.
|
|
114
|
+
|
|
102
115
|
- Create new participants (auto id or a specific id).
|
|
116
|
+
|
|
103
117
|
- Move the default or a specific participant to the next block or to a specific block id.
|
|
118
|
+
|
|
104
119
|
- Move all active participants to a block.
|
|
120
|
+
|
|
105
121
|
- Reset a participant.
|
|
122
|
+
|
|
106
123
|
- Choose a monitored participant to view its current config and status.
|
|
124
|
+
|
|
107
125
|
- Config management:
|
|
126
|
+
|
|
108
127
|
- View the currently loaded .toml contents in a code editor pane.
|
|
128
|
+
|
|
109
129
|
- Edit the config in place and save; saving writes the file and the app will attempt to reload the config.
|
|
130
|
+
|
|
110
131
|
- Generate new config files using simple or advanced helpers (write a minimal example or build from JSON-specified order/blocks/parameters).
|
|
132
|
+
|
|
111
133
|
- Generate expanded JSON configs for a range of participant indices (write output to a directory).
|
|
134
|
+
|
|
112
135
|
- Live logging pane shows application logs for actions and errors.
|
|
113
|
-
- A simple edit UI is available to tweak the in-memory per-participant config (submit or cancel edits).
|
|
114
136
|
|
|
115
|
-
|
|
137
|
+
- A simple edit UI is available to tweak the in-memory per-participant config (submit or cancel edits). The embedded editor provides:
|
|
138
|
+
- An "Edit config" flow (Edit / Insert snippet / Save / Cancel).
|
|
139
|
+
|
|
140
|
+
- "Insert snippet" to add common TOML fragments (blocks, strategy lines, init/final blocks, new variables).
|
|
141
|
+
|
|
142
|
+
- Save-time validation: when you save from the TUI, the editor runs verification (same checks used by [verify-config-file](#verify-config)). If verification fails the UI shows the failure reason and asks you to confirm whether to write the file anyway.
|
|
143
|
+
|
|
144
|
+
- editor-only TUI - A compact editor-only TUI is available via --editor-only. This launches a simple code editor for editing a config file without starting the HTTP server. Example:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
# editor-only mode (requires a config file)
|
|
148
|
+
$ experiment-server ui --editor-only -c sample_config.toml
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Note: --editor-only requires -c/--config-file and will not start the server.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### Notes
|
|
155
|
+
|
|
116
156
|
- Editing and saving the config file from the TUI writes to the same config path the server is using; the TUI will attempt to reload the new configuration but will preserve participant state (see server behavior note in the [Loading experiment through server](#loading-experiment-through-server) section).
|
|
157
|
+
|
|
117
158
|
- The TUI requires the textual/Rich environment provided by the package (normally installed with the package). Use `Ctrl+q` to exit or the TUI's quit action.
|
|
118
159
|
|
|
119
160
|
This TUI is intended as a compact, interactive alternative to the web interface for quick testing, config edits, and participant control.
|
|
161
|
+
|
|
120
162
|
<p style="display:flex; gap:8px; align-items:flex-start;">
|
|
121
163
|
<img src="https://raw.githubusercontent.com/ahmed-shariff/experiment_server/master/media/screenshot_tui1.png" alt="web UI screenshot 1" style="width:49%;">
|
|
122
164
|
<img src="https://raw.githubusercontent.com/ahmed-shariff/experiment_server/master/media/screenshot_tui2.png" alt="web UI screenshot 2" style="width:49%;">
|
|
@@ -368,7 +410,13 @@ A config file can be validated by running:
|
|
|
368
410
|
```sh
|
|
369
411
|
$ experiment-server verify-config-file sample_config.toml
|
|
370
412
|
```
|
|
371
|
-
This will show how the expanded config looks like for the first 5 participants.
|
|
413
|
+
This will show how the expanded config looks like for the first 5 participants and will return a clear failure reason if the config is invalid.
|
|
414
|
+
|
|
415
|
+
You can launch the editor-only [TUI](#textual-ui-tui) to edit the file in-place. This editor also has save time validation which can be used to verify the config before saving.
|
|
416
|
+
|
|
417
|
+
```sh
|
|
418
|
+
$ experiment-server ui --editor-only -c sample_config.toml
|
|
419
|
+
```
|
|
372
420
|
|
|
373
421
|
See also [verify_config][experiment_server._process_config.verify_config]
|
|
374
422
|
|
|
@@ -438,9 +486,13 @@ See also [_generate_config_json][experiment_server._api._generate_config_json]
|
|
|
438
486
|
|
|
439
487
|
## Function calls in config
|
|
440
488
|
A function call in the config is represented by a table, with the following keys
|
|
489
|
+
|
|
441
490
|
- `function_name`: This should be one of the names in the supported functions list below.
|
|
491
|
+
|
|
442
492
|
- `args`: The arguments to be passed to the function represented by `function_name`. This can be a list or a table/dict. They should unpack with `*` or `**` respectively when called with the corresponding function.
|
|
493
|
+
|
|
443
494
|
- (optional) `params`: function-specific configurations to apply with the function calls.
|
|
495
|
+
|
|
444
496
|
- (optional) `id`: A unique identifier to group function calls.
|
|
445
497
|
|
|
446
498
|
A table that has keys other than the above keys would not be treated as a function call. Any function calls in different places of the config with the same `id` would be treated as a single group. Tables without an `id` are grouped based on their key-value pairs. Groups are used to identify how some parameters affect the results (e.g., `unique` for `choices`). Function calls can also be in `configurations.variabels`. Note that all function calls are made after the `extends` are resolved and variables from `configurations.variabels` are replaced.
|
|
@@ -38,7 +38,7 @@ $ poetry add experiment-server
|
|
|
38
38
|
|
|
39
39
|
A terminal-based user interface is provided for interactive management and monitoring of an experiment. It is useful when you want a lightweight, keyboard-driven interface to load configs, inspect/generate per‑participant configs, edit files, and control participant flow without using the web UI.
|
|
40
40
|
|
|
41
|
-
Launch the TUI with:
|
|
41
|
+
### Launch the TUI with:
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
44
|
# Start TUI (optionally provide a config file)
|
|
@@ -49,35 +49,77 @@ $ experiment-server ui -c sample_config.toml
|
|
|
49
49
|
$ experiment-server ui -c sample_config.toml -i 1 -h 127.0.0.1 -p 5000
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Options:
|
|
52
|
+
### Options:
|
|
53
|
+
|
|
53
54
|
- -c, --config-file : path to a .toml config to load on start (optional).
|
|
54
|
-
- -i, --default-participant-index : default participant index to use when no explicit participant id is provided (default 1).
|
|
55
|
-
- -h, --host : host/interface for the server (default 127.0.0.1).
|
|
56
|
-
- -p, --port : port for the server (default 5000).
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
- -i, --default-participant-index : default participant index to use when no explicit participant id is provided (default 1). (optional)
|
|
57
|
+
|
|
58
|
+
- -h, --host : host/interface for the server (default 127.0.0.1). (optional)
|
|
59
|
+
|
|
60
|
+
- -p, --port : port for the server (default 5000). (optional)
|
|
61
|
+
|
|
62
|
+
- --editor-only : Launch the editor only. "-i", "-h", and "-p" will be ignored. When launching ui with "--editor-only", the config file ("-c") must be passed as well. (optional)
|
|
63
|
+
|
|
64
|
+
### Behavior and features
|
|
65
|
+
|
|
66
|
+
- Startup config validation - When launching the full TUI (without --editor-only), if a config file is provided it is validated at startup. If validation fails, the TUI will refuse to start the full UI/server and will exit with an error message suggesting use of verify-config-file or the editor-only mode to edit the file.
|
|
67
|
+
|
|
59
68
|
- Can be started without a config file; load a config later from the built-in file browser (filters to directories and .toml files).
|
|
69
|
+
|
|
60
70
|
- When a config is loaded the package starts the server in the same event loop, exposing the same REST API described elsewhere in this README.
|
|
71
|
+
|
|
61
72
|
- Participant management:
|
|
73
|
+
|
|
62
74
|
- View all participants and their status, block id and block name.
|
|
75
|
+
|
|
63
76
|
- Create new participants (auto id or a specific id).
|
|
77
|
+
|
|
64
78
|
- Move the default or a specific participant to the next block or to a specific block id.
|
|
79
|
+
|
|
65
80
|
- Move all active participants to a block.
|
|
81
|
+
|
|
66
82
|
- Reset a participant.
|
|
83
|
+
|
|
67
84
|
- Choose a monitored participant to view its current config and status.
|
|
85
|
+
|
|
68
86
|
- Config management:
|
|
87
|
+
|
|
69
88
|
- View the currently loaded .toml contents in a code editor pane.
|
|
89
|
+
|
|
70
90
|
- Edit the config in place and save; saving writes the file and the app will attempt to reload the config.
|
|
91
|
+
|
|
71
92
|
- Generate new config files using simple or advanced helpers (write a minimal example or build from JSON-specified order/blocks/parameters).
|
|
93
|
+
|
|
72
94
|
- Generate expanded JSON configs for a range of participant indices (write output to a directory).
|
|
95
|
+
|
|
73
96
|
- Live logging pane shows application logs for actions and errors.
|
|
74
|
-
- A simple edit UI is available to tweak the in-memory per-participant config (submit or cancel edits).
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
- A simple edit UI is available to tweak the in-memory per-participant config (submit or cancel edits). The embedded editor provides:
|
|
99
|
+
- An "Edit config" flow (Edit / Insert snippet / Save / Cancel).
|
|
100
|
+
|
|
101
|
+
- "Insert snippet" to add common TOML fragments (blocks, strategy lines, init/final blocks, new variables).
|
|
102
|
+
|
|
103
|
+
- Save-time validation: when you save from the TUI, the editor runs verification (same checks used by [verify-config-file](#verify-config)). If verification fails the UI shows the failure reason and asks you to confirm whether to write the file anyway.
|
|
104
|
+
|
|
105
|
+
- editor-only TUI - A compact editor-only TUI is available via --editor-only. This launches a simple code editor for editing a config file without starting the HTTP server. Example:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
# editor-only mode (requires a config file)
|
|
109
|
+
$ experiment-server ui --editor-only -c sample_config.toml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- Note: --editor-only requires -c/--config-file and will not start the server.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
### Notes
|
|
116
|
+
|
|
77
117
|
- Editing and saving the config file from the TUI writes to the same config path the server is using; the TUI will attempt to reload the new configuration but will preserve participant state (see server behavior note in the [Loading experiment through server](#loading-experiment-through-server) section).
|
|
118
|
+
|
|
78
119
|
- The TUI requires the textual/Rich environment provided by the package (normally installed with the package). Use `Ctrl+q` to exit or the TUI's quit action.
|
|
79
120
|
|
|
80
121
|
This TUI is intended as a compact, interactive alternative to the web interface for quick testing, config edits, and participant control.
|
|
122
|
+
|
|
81
123
|
<p style="display:flex; gap:8px; align-items:flex-start;">
|
|
82
124
|
<img src="https://raw.githubusercontent.com/ahmed-shariff/experiment_server/master/media/screenshot_tui1.png" alt="web UI screenshot 1" style="width:49%;">
|
|
83
125
|
<img src="https://raw.githubusercontent.com/ahmed-shariff/experiment_server/master/media/screenshot_tui2.png" alt="web UI screenshot 2" style="width:49%;">
|
|
@@ -329,7 +371,13 @@ A config file can be validated by running:
|
|
|
329
371
|
```sh
|
|
330
372
|
$ experiment-server verify-config-file sample_config.toml
|
|
331
373
|
```
|
|
332
|
-
This will show how the expanded config looks like for the first 5 participants.
|
|
374
|
+
This will show how the expanded config looks like for the first 5 participants and will return a clear failure reason if the config is invalid.
|
|
375
|
+
|
|
376
|
+
You can launch the editor-only [TUI](#textual-ui-tui) to edit the file in-place. This editor also has save time validation which can be used to verify the config before saving.
|
|
377
|
+
|
|
378
|
+
```sh
|
|
379
|
+
$ experiment-server ui --editor-only -c sample_config.toml
|
|
380
|
+
```
|
|
333
381
|
|
|
334
382
|
See also [verify_config][experiment_server._process_config.verify_config]
|
|
335
383
|
|
|
@@ -399,9 +447,13 @@ See also [_generate_config_json][experiment_server._api._generate_config_json]
|
|
|
399
447
|
|
|
400
448
|
## Function calls in config
|
|
401
449
|
A function call in the config is represented by a table, with the following keys
|
|
450
|
+
|
|
402
451
|
- `function_name`: This should be one of the names in the supported functions list below.
|
|
452
|
+
|
|
403
453
|
- `args`: The arguments to be passed to the function represented by `function_name`. This can be a list or a table/dict. They should unpack with `*` or `**` respectively when called with the corresponding function.
|
|
454
|
+
|
|
404
455
|
- (optional) `params`: function-specific configurations to apply with the function calls.
|
|
456
|
+
|
|
405
457
|
- (optional) `id`: A unique identifier to group function calls.
|
|
406
458
|
|
|
407
459
|
A table that has keys other than the above keys would not be treated as a function call. Any function calls in different places of the config with the same `id` would be treated as a single group. Tables without an `id` are grouped based on their key-value pairs. Groups are used to identify how some parameters affect the results (e.g., `unique` for `choices`). Function calls can also be in `configurations.variabels`. Note that all function calls are made after the `extends` are resolved and variables from `configurations.variabels` are replaced.
|
|
@@ -6,7 +6,7 @@ from experiment_server._process_config import process_config_file
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import json
|
|
8
8
|
|
|
9
|
-
from experiment_server.utils import
|
|
9
|
+
from experiment_server.utils import ExperimentServerException, FileModifiedWatcher
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ParticipantState:
|
|
@@ -199,7 +199,7 @@ class Experiment:
|
|
|
199
199
|
if participant_index is None:
|
|
200
200
|
participant_index = self.default_participant_index
|
|
201
201
|
if participant_index not in self.global_state:
|
|
202
|
-
raise
|
|
202
|
+
raise ExperimentServerException(f"participant with index {participant_index} is not set. Consider using `add_participant_index`")
|
|
203
203
|
return self.global_state[participant_index]
|
|
204
204
|
|
|
205
205
|
def get_state(self, participant_index:int|None=None) -> bool:
|
|
@@ -286,11 +286,11 @@ def _generate_config_json(config_file: Union[str, Path], participant_indices: It
|
|
|
286
286
|
logger.info(f"Creating direcotry {out_dir}")
|
|
287
287
|
out_dir.mkdir(parents=True)
|
|
288
288
|
elif not out_dir.is_dir():
|
|
289
|
-
raise
|
|
289
|
+
raise ExperimentServerException(f"`out_file_location` should be a directory. Got {out_dir}")
|
|
290
290
|
|
|
291
291
|
out_files = []
|
|
292
292
|
for participant_index in participant_indices:
|
|
293
|
-
config = process_config_file(config_file, participant_index,
|
|
293
|
+
config = process_config_file(config_file, participant_index, suppress_message=True)
|
|
294
294
|
if out_dir is not None:
|
|
295
295
|
out_file = Path(out_dir) / f"{Path(config_file).stem}-participant_{participant_index}.json"
|
|
296
296
|
out_files.append(out_file)
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
from typing import Tuple, Union
|
|
3
3
|
import requests
|
|
4
4
|
|
|
5
|
-
from experiment_server.utils import
|
|
5
|
+
from experiment_server.utils import ExperimentServerException
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Client:
|
|
@@ -49,7 +49,7 @@ class Client:
|
|
|
49
49
|
elif verb == "PUT":
|
|
50
50
|
r = requests.put(url)
|
|
51
51
|
else:
|
|
52
|
-
raise
|
|
52
|
+
raise ExperimentServerException("huh?")
|
|
53
53
|
|
|
54
54
|
if r.status_code != 200:
|
|
55
55
|
return False, {"message": f"status {r.status_code} with text: {r.text}"}
|
{experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_participant_ordering.py
RENAMED
|
@@ -3,7 +3,7 @@ import itertools
|
|
|
3
3
|
from typing import Dict, List, Union
|
|
4
4
|
from easydict import EasyDict as edict
|
|
5
5
|
|
|
6
|
-
from experiment_server.utils import
|
|
6
|
+
from experiment_server.utils import ExperimentServerConfigurationException, balanced_latin_square
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
ORDERING_STRATEGY = edict({v:v for v in ["randomize", "latin_square", "as_is"]})
|
|
@@ -122,29 +122,29 @@ def construct_participant_condition(config: List[Dict],
|
|
|
122
122
|
if within_groups_strategy is None:
|
|
123
123
|
within_groups_strategy = ORDERING_STRATEGY.as_is
|
|
124
124
|
elif within_groups_strategy not in list(ORDERING_STRATEGY.values()):
|
|
125
|
-
raise
|
|
125
|
+
raise ExperimentServerConfigurationException(f"Allowed values for `within_groups` are {ORDERING_STRATEGY.values()}, for {within_groups_strategy}")
|
|
126
126
|
|
|
127
127
|
if groups_strategy is None:
|
|
128
128
|
groups_strategy = ORDERING_STRATEGY.as_is
|
|
129
129
|
elif groups_strategy not in list(ORDERING_STRATEGY.values()):
|
|
130
|
-
raise
|
|
130
|
+
raise ExperimentServerConfigurationException(f"Allowed values for `groups` are {ORDERING_STRATEGY.values()}, for {groups_strategy}")
|
|
131
131
|
|
|
132
132
|
if init_blocks_strategy is None:
|
|
133
133
|
init_blocks_strategy = INIT_FINAL_ORDERING_STRATEGY.as_is
|
|
134
134
|
elif init_blocks_strategy not in list(INIT_FINAL_ORDERING_STRATEGY.values()):
|
|
135
|
-
raise
|
|
135
|
+
raise ExperimentServerConfigurationException(f"Allowed values for `init_blocks_strategy` are {INIT_FINAL_ORDERING_STRATEGY.values()}, for {init_blocks_strategy}")
|
|
136
136
|
|
|
137
137
|
if final_blocks_strategy is None:
|
|
138
138
|
final_blocks_strategy = INIT_FINAL_ORDERING_STRATEGY.as_is
|
|
139
139
|
elif final_blocks_strategy not in list(INIT_FINAL_ORDERING_STRATEGY.values()):
|
|
140
|
-
raise
|
|
140
|
+
raise ExperimentServerConfigurationException(f"Allowed values for `final_blocks_strategy` are {INIT_FINAL_ORDERING_STRATEGY.values()}, for {final_blocks_strategy}")
|
|
141
141
|
|
|
142
142
|
name_to_config_mapping = {}
|
|
143
143
|
|
|
144
144
|
for c in config:
|
|
145
145
|
name = c["name"] = str(c["name"])
|
|
146
146
|
if name in name_to_config_mapping:
|
|
147
|
-
raise
|
|
147
|
+
raise ExperimentServerConfigurationException(f"Duplicate block name: {name}")
|
|
148
148
|
name_to_config_mapping[c["name"]] = c
|
|
149
149
|
|
|
150
150
|
if isinstance(order, list):
|
|
@@ -153,7 +153,7 @@ def construct_participant_condition(config: List[Dict],
|
|
|
153
153
|
# Making sure the stratergy set for groups is used for within groups
|
|
154
154
|
within_groups_strategy = groups_strategy
|
|
155
155
|
if not all([isinstance(g, str) for group in order for g in group]):
|
|
156
|
-
raise
|
|
156
|
+
raise ExperimentServerConfigurationException(f"All conditions in order were expected to be strings, got {order}")
|
|
157
157
|
|
|
158
158
|
_filtered_order = order
|
|
159
159
|
|
|
@@ -175,7 +175,7 @@ def construct_participant_condition(config: List[Dict],
|
|
|
175
175
|
elif within_groups_strategy == ORDERING_STRATEGY.latin_square:
|
|
176
176
|
elements_in_group = set([len(_g) for _g in _filtered_order])
|
|
177
177
|
if len(elements_in_group) != 1:
|
|
178
|
-
raise
|
|
178
|
+
raise ExperimentServerConfigurationException(f"Currently {ORDERING_STRATEGY.latin_square} not supported for `within_groups` when the number of elements in all groups are not the same")
|
|
179
179
|
else:
|
|
180
180
|
_elements_count = elements_in_group.pop()
|
|
181
181
|
_latin_square = [el for l in balanced_latin_square(_elements_count) for el in [l,] * len(_filtered_order)]
|
|
@@ -205,7 +205,7 @@ def _process_init_final_block(participant_index:int, block_names: Union[list, di
|
|
|
205
205
|
_filtered_block_names = block_names
|
|
206
206
|
if isinstance(block_names, list):
|
|
207
207
|
if not all([isinstance(n, str) for n in block_names]):
|
|
208
|
-
raise
|
|
208
|
+
raise ExperimentServerConfigurationException(f"All conditions in order were expected to be strings, got {block_names}")
|
|
209
209
|
|
|
210
210
|
elif isinstance(block_names, dict):
|
|
211
211
|
_filtered_block_names = _process_dict_orders(participant_index, block_names, blocks_strategy, var_name, strategy_enum)
|
|
@@ -218,15 +218,15 @@ def _process_dict_orders(participant_index:int, block_names: dict, blocks_strate
|
|
|
218
218
|
"""Helper to process dictionary patterns. See `construct_participant_condition` for more details."""
|
|
219
219
|
block_names = {int(k):v for k, v in block_names.items()}
|
|
220
220
|
if not all([isinstance(_order, list) for _order in block_names.values()]):
|
|
221
|
-
raise
|
|
221
|
+
raise ExperimentServerConfigurationException(f"Each group in {var_name} for all participants needs to be list, got {block_names}")
|
|
222
222
|
if not all([isinstance(val, str) for _order in block_names.values() for val in _order]):
|
|
223
|
-
raise
|
|
223
|
+
raise ExperimentServerConfigurationException(f"All conditions in {var_name} for all participants needs to be a strings, got {block_names}")
|
|
224
224
|
|
|
225
225
|
if not all([idx+1 in block_names.keys() for idx in range(len(block_names))]):
|
|
226
|
-
raise
|
|
226
|
+
raise ExperimentServerConfigurationException(f"Keys order in {var_name} should match the consecutive indices starting from 1. Got keys {list(block_names.keys())}, expected keys {[idx + 1 for idx in list(range(len(block_names)))]}")
|
|
227
227
|
|
|
228
228
|
if blocks_strategy != strategy_enum.as_is:
|
|
229
|
-
raise
|
|
229
|
+
raise ExperimentServerConfigurationException(f"Ordering strategy for {var_name} should be {strategy_enum.as_is} when {var_name} is a dictionary. Got {blocks_strategy}")
|
|
230
230
|
_key = ((participant_index - 1) % len(block_names)) + 1
|
|
231
231
|
_filtered_block_names = block_names[_key]
|
|
232
232
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import random
|
|
3
3
|
import warnings
|
|
4
|
-
from typing import Any, Callable, Dict, List, Tuple, Union
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
6
|
from experiment_server._participant_ordering import construct_participant_condition, ORDERING_STRATEGY
|
|
7
|
-
from experiment_server.utils import
|
|
7
|
+
from experiment_server.utils import ExperimentServerConfigurationException, ExperimentServerException, merge_dicts
|
|
8
8
|
from loguru import logger
|
|
9
9
|
from tabulate import tabulate
|
|
10
10
|
import json
|
|
11
11
|
import toml
|
|
12
|
+
from traceback import format_exception_only
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
TOP_LEVEL_RESERVED_KEYS = ["name", "config", "extends"]
|
|
@@ -16,7 +17,7 @@ SECTIONS = ["main_configuration", "init_configuration", "final_configuration", "
|
|
|
16
17
|
ALLOWED_SETTINGS = ["randomize_within_groups", "randomize_groups"]
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def process_config_file(f: Union[str, Path], participant_index: int,
|
|
20
|
+
def process_config_file(f: Union[str, Path], participant_index: int, suppress_message:bool=False) -> List[Dict[str, Any]]:
|
|
20
21
|
"""
|
|
21
22
|
Load and resolve an experiment configuration file for a single participant.
|
|
22
23
|
|
|
@@ -35,7 +36,7 @@ def process_config_file(f: Union[str, Path], participant_index: int, supress_mes
|
|
|
35
36
|
This index is used where deterministic rotations (e.g. latin-square or per-participant
|
|
36
37
|
assignment) are requested and is added to any configured random seed to provide
|
|
37
38
|
reproducible per-participant randomness.
|
|
38
|
-
|
|
39
|
+
suppress_message (bool, optional):
|
|
39
40
|
If False (default), a JSON summary of the resolved configuration for the participant
|
|
40
41
|
is logged. If True, the info log is suppressed.
|
|
41
42
|
|
|
@@ -65,21 +66,24 @@ def process_config_file(f: Union[str, Path], participant_index: int, supress_mes
|
|
|
65
66
|
See also `experiment_server._participant_ordering.construct_participant_condition`.
|
|
66
67
|
"""
|
|
67
68
|
if participant_index < 1:
|
|
68
|
-
raise
|
|
69
|
+
raise ExperimentServerConfigurationException(f"Participant index needs to be greater than 0, got {participant_index}")
|
|
69
70
|
|
|
70
71
|
if Path(f).suffix == ".toml":
|
|
71
|
-
return _process_toml(f, participant_index,
|
|
72
|
+
return _process_toml(f, participant_index, suppress_message)
|
|
72
73
|
else:
|
|
73
|
-
raise
|
|
74
|
+
raise ExperimentServerException("Invalid file type. Expected `.toml`")
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
def _process_toml(f: Union[str, Path], participant_index:int,
|
|
77
|
+
def _process_toml(f: Union[str, Path], participant_index:int, suppress_message:bool=False) -> List[Dict[str, Any]]:
|
|
77
78
|
loaded_configuration = toml.load(f)
|
|
78
|
-
return _process_config(loaded_configuration, participant_index,
|
|
79
|
+
return _process_config(loaded_configuration, participant_index, suppress_message)
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
def _process_config(configuration: dict[Any, Any], participant_index:int,
|
|
82
|
-
|
|
82
|
+
def _process_config(configuration: dict[Any, Any], participant_index:int, suppress_message:bool=False) -> List[Dict[str, Any]]:
|
|
83
|
+
try:
|
|
84
|
+
configurations = configuration["configuration"]
|
|
85
|
+
except KeyError:
|
|
86
|
+
raise ExperimentServerConfigurationException("Missing `configuration` section.")
|
|
83
87
|
variables = configurations.get("variables", {})
|
|
84
88
|
|
|
85
89
|
order_groups_strategy = ORDERING_STRATEGY.as_is
|
|
@@ -113,11 +117,19 @@ def _process_config(configuration: dict[Any, Any], participant_index:int, supres
|
|
|
113
117
|
random.seed(random_seed + participant_index)
|
|
114
118
|
|
|
115
119
|
all_blocks = _replace_variables(configuration["blocks"], variables)
|
|
116
|
-
|
|
120
|
+
if not isinstance(all_blocks, list):
|
|
121
|
+
raise ExperimentServerConfigurationException(f"`blocks` is not a list.")
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
try:
|
|
124
|
+
order = configurations["order"]
|
|
125
|
+
except KeyError:
|
|
126
|
+
raise ExperimentServerConfigurationException(f"Missing `order` under `configuration`.")
|
|
119
127
|
|
|
120
128
|
for c in all_blocks:
|
|
129
|
+
if "name" not in c:
|
|
130
|
+
raise ExperimentServerConfigurationException(f"One or more block(s) missing `name`.")
|
|
131
|
+
if "config" not in c and "extends" not in c:
|
|
132
|
+
raise ExperimentServerConfigurationException(f"One or more block(s) missing `config`.")
|
|
121
133
|
c["name"] = str(c["name"])
|
|
122
134
|
|
|
123
135
|
blocks = construct_participant_condition(all_blocks, participant_index, order=order,
|
|
@@ -142,7 +154,7 @@ def _process_config(configuration: dict[Any, Any], participant_index:int, supres
|
|
|
142
154
|
c["config"]["name"] = c["name"]
|
|
143
155
|
c["config"]["block_id"] = idx
|
|
144
156
|
|
|
145
|
-
if not
|
|
157
|
+
if not suppress_message:
|
|
146
158
|
logger.info("Configuration loaded: \n" + json.dumps(blocks, indent=2))
|
|
147
159
|
return blocks
|
|
148
160
|
|
|
@@ -159,7 +171,7 @@ def _resolve_extends(c, configs, seen_configs):
|
|
|
159
171
|
try:
|
|
160
172
|
dict_b = [_c for _c in configs if _c["name"] == c["extends"]][0]
|
|
161
173
|
except IndexError:
|
|
162
|
-
raise
|
|
174
|
+
raise ExperimentServerConfigurationException("`{}` is not a valid name. It must be a `name`.".format(c["extends"]))
|
|
163
175
|
|
|
164
176
|
dict_b, configs, seen_configs = _resolve_extends(dict_b, configs, seen_configs + [dict_b["name"]])
|
|
165
177
|
configs[dict_a["block_idx"]] = merge_dicts(dict_a, dict_b)
|
|
@@ -221,7 +233,7 @@ def _replace_variables(config: Union[Dict[str, Any], List[Any]], variabels: Dict
|
|
|
221
233
|
try:
|
|
222
234
|
resolved_config[k] = variabels[v[1:]]
|
|
223
235
|
except KeyError:
|
|
224
|
-
raise
|
|
236
|
+
raise ExperimentServerConfigurationException(f"The variable `{v}` does not exsist in `configuration.variables`")
|
|
225
237
|
elif isinstance(v, dict):
|
|
226
238
|
resolved_config[k] = _replace_variables(v, variabels)
|
|
227
239
|
else:
|
|
@@ -233,7 +245,7 @@ def _replace_variables(config: Union[Dict[str, Any], List[Any]], variabels: Dict
|
|
|
233
245
|
try:
|
|
234
246
|
resolved_config.append(variabels[v[1:]])
|
|
235
247
|
except KeyError:
|
|
236
|
-
raise
|
|
248
|
+
raise ExperimentServerConfigurationException(f"The variable `{v}` does not exsist in `configuration.variables`")
|
|
237
249
|
elif isinstance(v, dict):
|
|
238
250
|
resolved_config.append(_replace_variables(v, variabels))
|
|
239
251
|
else:
|
|
@@ -269,7 +281,7 @@ def _unpack_args(args) -> Tuple[list, dict]:
|
|
|
269
281
|
elif isinstance(args, dict):
|
|
270
282
|
kwargs = args
|
|
271
283
|
else:
|
|
272
|
-
raise
|
|
284
|
+
raise ExperimentServerConfigurationException(f"`args` should be a list or a dict. Got {args}")
|
|
273
285
|
return largs, kwargs
|
|
274
286
|
|
|
275
287
|
|
|
@@ -287,7 +299,7 @@ def _resolve_function(function_name:str, args: Union[List,Dict], function_calls:
|
|
|
287
299
|
function_call_group = function_calls[call_signature] = ChoicesFunction(args, params)
|
|
288
300
|
return function_call_group(args, params)
|
|
289
301
|
else:
|
|
290
|
-
raise
|
|
302
|
+
raise ExperimentServerConfigurationException(f"Unknown function {function_name}")
|
|
291
303
|
|
|
292
304
|
|
|
293
305
|
class ChoicesFunction:
|
|
@@ -301,11 +313,11 @@ class ChoicesFunction:
|
|
|
301
313
|
self.params = params
|
|
302
314
|
if params is not None:
|
|
303
315
|
if not isinstance(params, dict):
|
|
304
|
-
raise
|
|
316
|
+
raise ExperimentServerConfigurationException(f"`params` for `choices` should be a dict.")
|
|
305
317
|
if len(params) not in (0, 1):
|
|
306
|
-
raise
|
|
318
|
+
raise ExperimentServerConfigurationException(f"Function `choices` expected 0 or 1 keys in params, got {len(params)}")
|
|
307
319
|
if len(params) == 1 and "unique" not in params:
|
|
308
|
-
raise
|
|
320
|
+
raise ExperimentServerConfigurationException(f"Unexpected key in `params` of `choices`. Allowed keys: [`unique`]")
|
|
309
321
|
if "unique" in params:
|
|
310
322
|
self.unique = params.get("unique")
|
|
311
323
|
self.previous_choices = []
|
|
@@ -329,11 +341,11 @@ class ChoicesFunction:
|
|
|
329
341
|
|
|
330
342
|
# Check if it is possible to get unique values.
|
|
331
343
|
if len(self.previous_choices) != len(set(self.previous_choices)):
|
|
332
|
-
raise
|
|
344
|
+
raise ExperimentServerConfigurationException("There are more calls to `choices` than number of elements in `args`")
|
|
333
345
|
return choice
|
|
334
346
|
|
|
335
347
|
|
|
336
|
-
def verify_config(f: Union[str, Path], test_func:Callable[[List[Dict[str, Any]]], Tuple[bool, str]]=None) -> bool:
|
|
348
|
+
def verify_config(f: Union[str, Path], test_func:Optional[Callable[[List[Dict[str, Any]]], Tuple[bool, str]]]=None, raise_on_error:bool=False) -> Tuple[bool, Optional[str]]:
|
|
337
349
|
"""
|
|
338
350
|
Verify an experiment TOML config by constructing participant orders for participants 1–5.
|
|
339
351
|
|
|
@@ -346,23 +358,33 @@ def verify_config(f: Union[str, Path], test_func:Callable[[List[Dict[str, Any]]]
|
|
|
346
358
|
f: Path or filename of the TOML configuration file.
|
|
347
359
|
test_func: Optional callable receiving the resolved blocks and returning
|
|
348
360
|
a (bool, str) tuple indicating success and an optional reason.
|
|
361
|
+
raise_on_error: If True, will raise the exception that caused the verification to fail.
|
|
349
362
|
|
|
350
363
|
Returns:
|
|
351
|
-
|
|
364
|
+
Tuple (bool success, str reason):
|
|
365
|
+
`success` will be True if verification completed successfully for all checked
|
|
366
|
+
participants. If verification failed, success will be false, and the reason will
|
|
367
|
+
be the message from the resulting exception.
|
|
352
368
|
"""
|
|
353
|
-
with logger.catch(reraise=
|
|
369
|
+
# with logger.catch(reraise=raise_on_error, message="Config verification failed"):
|
|
370
|
+
|
|
371
|
+
try:
|
|
354
372
|
df = _get_table_for_participants(f, test_func)
|
|
355
373
|
logger.info(f"Ordering for 5 participants: \n\n{tabulate(df, headers='firstrow', tablefmt='fancy_grid')}\n")
|
|
356
374
|
logger.info(f"Config file verification successful for {f}")
|
|
357
|
-
return True
|
|
358
|
-
|
|
375
|
+
return (True, None)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logger.exception(e)
|
|
378
|
+
if raise_on_error:
|
|
379
|
+
raise
|
|
380
|
+
return (False, "\n".join(format_exception_only(e)))
|
|
359
381
|
|
|
360
382
|
|
|
361
|
-
def _get_table_for_participants(f: Union[str, Path], test_func:Callable[[List[Dict[str, Any]]], Tuple[bool, str]]=None) -> list[list[Any]]:
|
|
383
|
+
def _get_table_for_participants(f: Union[str, Path], test_func:Optional[Callable[[List[Dict[str, Any]]], Tuple[bool, str]]]=None) -> list[list[Any]]:
|
|
362
384
|
config_blocks: Dict[int, Dict[str, Any]] = {}
|
|
363
385
|
# collect configs for participants 1..5 (same range as original)
|
|
364
386
|
for participant_index in range(1, 6):
|
|
365
|
-
config = process_config_file(f, participant_index=participant_index)
|
|
387
|
+
config = process_config_file(f, participant_index=participant_index, suppress_message=True)
|
|
366
388
|
config_blocks[participant_index] = {f"block_{idx + 1}": c["name"] for idx, c in enumerate(config)}
|
|
367
389
|
if test_func is not None:
|
|
368
390
|
test_result, reason = test_func(config)
|
|
@@ -12,7 +12,7 @@ import asyncio
|
|
|
12
12
|
import json
|
|
13
13
|
|
|
14
14
|
from experiment_server._api import Experiment
|
|
15
|
-
from experiment_server.utils import
|
|
15
|
+
from experiment_server.utils import ExperimentServerConfigurationException, ExperimentServerException
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _create_app(experiment:Experiment):
|
|
@@ -397,9 +397,9 @@ class ExperimentHandler(RequestHandler):
|
|
|
397
397
|
if not added_participant:
|
|
398
398
|
self.set_status(406)
|
|
399
399
|
self.write(json.dumps(added_participant))
|
|
400
|
-
except
|
|
400
|
+
except ExperimentServerConfigurationException as e:
|
|
401
401
|
self.set_status(406)
|
|
402
|
-
self.write(e.args
|
|
402
|
+
self.write(e.message if e.message is not None else str(e.args))
|
|
403
403
|
|
|
404
404
|
def _get_int_from_param(self, param):
|
|
405
405
|
try:
|