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.
Files changed (26) hide show
  1. {experiment_server-0.3.7 → experiment_server-0.3.8}/PKG-INFO +62 -10
  2. {experiment_server-0.3.7 → experiment_server-0.3.8}/README.md +61 -9
  3. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_api.py +4 -4
  4. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_client.py +2 -2
  5. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_participant_ordering.py +13 -13
  6. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_process_config.py +52 -30
  7. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_server.py +3 -3
  8. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/_ui.py +496 -105
  9. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/cli.py +28 -10
  10. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/css/app.tcss +89 -3
  11. experiment_server-0.3.8/experiment_server/temp.py +13 -0
  12. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/utils.py +20 -7
  13. {experiment_server-0.3.7 → experiment_server-0.3.8}/pyproject.toml +1 -1
  14. {experiment_server-0.3.7 → experiment_server-0.3.8}/LICENSE.md +0 -0
  15. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/__init__.py +0 -0
  16. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/css/bootstrap-5.2.3.min.css +0 -0
  17. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/index.html +0 -0
  18. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/initconfig.html +0 -0
  19. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/alpinejs3.min.js +0 -0
  20. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/bootstrap-4.5.0.min.js +0 -0
  21. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/fontawesome-1e694dd391.js +0 -0
  22. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/htmx-1.9.10.js +0 -0
  23. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/jquery-3.5.1.min.js +0 -0
  24. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/popper-1.16.0.min.js +0 -0
  25. {experiment_server-0.3.7 → experiment_server-0.3.8}/experiment_server/static/js/sweetalert2-11.js +0 -0
  26. {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.7
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
- Behavior and features
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
- Notes
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
- Behavior and features
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
- Notes
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 ExperimentServerExcetion, FileModifiedWatcher
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 ExperimentServerExcetion(f"participant with index {participant_index} is not set. Consider using `add_participant_index`")
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 ExperimentServerExcetion(f"`out_file_location` should be a directory. Got {out_dir}")
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, supress_message=True)
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 ExperimentServerExcetion
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 ExperimentServerExcetion("huh?")
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}"}
@@ -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 ExperimentServerConfigurationExcetion, balanced_latin_square
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 ExperimentServerConfigurationExcetion(f"Allowed values for `within_groups` are {ORDERING_STRATEGY.values()}, for {within_groups_strategy}")
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 ExperimentServerConfigurationExcetion(f"Allowed values for `groups` are {ORDERING_STRATEGY.values()}, for {groups_strategy}")
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 ExperimentServerConfigurationExcetion(f"Allowed values for `init_blocks_strategy` are {INIT_FINAL_ORDERING_STRATEGY.values()}, for {init_blocks_strategy}")
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 ExperimentServerConfigurationExcetion(f"Allowed values for `final_blocks_strategy` are {INIT_FINAL_ORDERING_STRATEGY.values()}, for {final_blocks_strategy}")
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 ExperimentServerConfigurationExcetion(f"Duplicate block name: {name}")
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 ExperimentServerConfigurationExcetion(f"All conditions in order were expected to be strings, got {order}")
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 ExperimentServerConfigurationExcetion(f"Currently {ORDERING_STRATEGY.latin_square} not supported for `within_groups` when the number of elements in all groups are not the same")
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 ExperimentServerConfigurationExcetion(f"All conditions in order were expected to be strings, got {block_names}")
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 ExperimentServerConfigurationExcetion(f"Each group in {var_name} for all participants needs to be list, got {block_names}")
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 ExperimentServerConfigurationExcetion(f"All conditions in {var_name} for all participants needs to be a strings, got {block_names}")
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 ExperimentServerConfigurationExcetion(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)))]}")
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 ExperimentServerConfigurationExcetion(f"Ordering strategy for {var_name} should be {strategy_enum.as_is} when {var_name} is a dictionary. Got {blocks_strategy}")
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 ExperimentServerConfigurationExcetion, ExperimentServerExcetion, merge_dicts
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, supress_message:bool=False) -> List[Dict[str, Any]]:
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
- supress_message (bool, optional):
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 ExperimentServerConfigurationExcetion(f"Participant index needs to be greater than 0, got {participant_index}")
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, supress_message)
72
+ return _process_toml(f, participant_index, suppress_message)
72
73
  else:
73
- raise ExperimentServerExcetion("Invalid file type. Expected `.toml`")
74
+ raise ExperimentServerException("Invalid file type. Expected `.toml`")
74
75
 
75
76
 
76
- def _process_toml(f: Union[str, Path], participant_index:int, supress_message:bool=False) -> List[Dict[str, Any]]:
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, supress_message)
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, supress_message:bool=False) -> List[Dict[str, Any]]:
82
- configurations = configuration.get("configuration", {})
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
- assert isinstance(all_blocks, list)
120
+ if not isinstance(all_blocks, list):
121
+ raise ExperimentServerConfigurationException(f"`blocks` is not a list.")
117
122
 
118
- order = configurations.get("order", [str(i) for i in list(range(len(all_blocks)))])
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 supress_message:
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 ExperimentServerConfigurationExcetion("`{}` is not a valid name. It must be a `name`.".format(c["extends"]))
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 ExperimentServerConfigurationExcetion(f"The variable `{v}` does not exsist in `configuration.variables`")
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 ExperimentServerConfigurationExcetion(f"The variable `{v}` does not exsist in `configuration.variables`")
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 ExperimentServerConfigurationExcetion(f"`args` should be a list or a dict. Got {args}")
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 ExperimentServerConfigurationExcetion(f"Unknown function {function_name}")
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 ExperimentServerConfigurationExcetion(f"`params` for `choices` should be a dict.")
316
+ raise ExperimentServerConfigurationException(f"`params` for `choices` should be a dict.")
305
317
  if len(params) not in (0, 1):
306
- raise ExperimentServerConfigurationExcetion(f"Function `choices` expected 0 or 1 keys in params, got {len(params)}")
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 ExperimentServerConfigurationExcetion(f"Unexpected key in `params` of `choices`. Allowed keys: [`unique`]")
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 ExperimentServerConfigurationExcetion("There are more calls to `choices` than number of elements in `args`")
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
- True if verification completed successfully for all checked participants.
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=False, message="Config verification failed"):
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
- return False
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 ExperimentServerConfigurationExcetion, ExperimentServerExcetion
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 ExperimentServerConfigurationExcetion as e:
400
+ except ExperimentServerConfigurationException as e:
401
401
  self.set_status(406)
402
- self.write(e.args[0][0])
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: