experiment_server 0.3.4__tar.gz → 0.3.5__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.4 → experiment_server-0.3.5}/PKG-INFO +71 -20
- {experiment_server-0.3.4 → experiment_server-0.3.5}/README.md +67 -17
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/_api.py +86 -42
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/_client.py +28 -0
- experiment_server-0.3.5/experiment_server/_participant_ordering.py +229 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/_process_config.py +134 -118
- {experiment_server-0.3.4 → experiment_server-0.3.5}/pyproject.toml +2 -4
- {experiment_server-0.3.4 → experiment_server-0.3.5}/sample_config.toml +50 -8
- experiment_server-0.3.4/experiment_server/_participant_ordering.py +0 -126
- {experiment_server-0.3.4 → experiment_server-0.3.5}/LICENSE.md +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/__init__.py +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/_server.py +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/cli.py +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/css/bootstrap-5.2.3.min.css +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/index.html +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/initconfig.html +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/alpinejs3.min.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/bootstrap-4.5.0.min.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/fontawesome-1e694dd391.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/htmx-1.9.10.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/jquery-3.5.1.min.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/popper-1.16.0.min.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/static/js/sweetalert2-11.js +0 -0
- {experiment_server-0.3.4 → experiment_server-0.3.5}/experiment_server/utils.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: experiment_server
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Server for experiments to get configuarations from
|
|
5
|
-
Home-page: https://shariff-faleel.com/experiment_server/
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: experiment,study-design
|
|
8
7
|
Author: Ahmed Shariff
|
|
@@ -19,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Requires-Dist: Jinja2 (>=3.1.2)
|
|
23
23
|
Requires-Dist: asyncio (>=3.4.3)
|
|
24
24
|
Requires-Dist: click (>=6.0)
|
|
@@ -33,6 +33,7 @@ Requires-Dist: toml (>=0.10.2)
|
|
|
33
33
|
Requires-Dist: tornado (>=6.2)
|
|
34
34
|
Requires-Dist: watchdog (>=2.1.9)
|
|
35
35
|
Project-URL: Documentation, https://shariff-faleel.com/experiment_server/documentation/
|
|
36
|
+
Project-URL: Homepage, https://shariff-faleel.com/experiment_server/
|
|
36
37
|
Project-URL: Repository, https://github.com/ahmed-shariff/experiment_server
|
|
37
38
|
Description-Content-Type: text/markdown
|
|
38
39
|
|
|
@@ -85,18 +86,41 @@ See example `.toml` below for how the configuration can be defined.
|
|
|
85
86
|
# The `configuration` table contains the settings of the study/experiment itself
|
|
86
87
|
[configuration]
|
|
87
88
|
# The `order` is an array of block names or an array of array of block names.
|
|
89
|
+
# The `order` also can be a dictionary/table. If it is a dictionary/table, the keys should
|
|
90
|
+
# be participant indices starting from 1. The values should be a list of block names.
|
|
91
|
+
# The keys are the participant indices - when experiment manager is queries with a given
|
|
92
|
+
# participant index, the dictionary/table will be used as a lookup table and the
|
|
93
|
+
# corresponding list of blocks will be used. When a participant index larger than the
|
|
94
|
+
# highest participant index is queries through experiment manager, the modulus of the
|
|
95
|
+
# index will be used to determine which list of conditions should be used.
|
|
96
|
+
# In the following example order with a table/dictionary, participant index 4 would get
|
|
97
|
+
# the blocks assigned to participant index 1:
|
|
98
|
+
# order = {1= ["conditionA", "conditionB", "conditionA", "conditionB"], 2= ["conditionB", "conditionA", "conditionB", "conditionA"], 3= ["conditionB", "conditionB", "conditionA", "conditionA"]}
|
|
88
99
|
order = [["conditionA", "conditionB", "conditionA", "conditionB"]]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# The `groups_strategy` and `within_groups_strategy` are optional keys that allows you to
|
|
103
|
+
# define how the conditions specified in `order` will be managed. `groups_strategy` would
|
|
104
|
+
# dictate how the top level array of `order` will be handled. `within_groups_strategy`
|
|
105
|
+
# would dictate how the conditions in the nested arrays (if specified) would be
|
|
106
|
+
# managed. These keys can have one of the following values.
|
|
94
107
|
# - "latin_square": Apply latin square to balance the values.
|
|
95
108
|
# - "randomize": For each participant randomize the order of the values in the array.
|
|
96
109
|
# - "as_is": Use the order of the values as specified.
|
|
97
110
|
# When not specified, the default value is "as_is" for both keys.
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
groups_strategy = "latin_square"
|
|
112
|
+
within_groups_strategy = "randomize"
|
|
113
|
+
|
|
114
|
+
# The following are optional keys. `init_blocks` is a list of blocks which will be
|
|
115
|
+
# appended to the start and `final_blocks` are a list of blocks that would be appended to
|
|
116
|
+
# the end. The respective strategies dictate the ordering withing the list of init or
|
|
117
|
+
# final blocks. For init and final blocks only the "randomize" and "as_is" are
|
|
118
|
+
# supported. By default, their values are "as_is".
|
|
119
|
+
init_blocks = ["initialBlock"]
|
|
120
|
+
final_blocks = ["finalBlockA", "finalBlockB"]
|
|
121
|
+
init_blocks_strategy = "as_is"
|
|
122
|
+
final_blocks_strategy = "randomize"
|
|
123
|
+
|
|
100
124
|
# The random seed to use for any randomization. Default seed is 0. The seed will be
|
|
101
125
|
# the value of random_seed + participant_index
|
|
102
126
|
random_seed = 0
|
|
@@ -146,6 +170,15 @@ See [toml spec](https://toml.io/en/v1.0.0) for more information on the format of
|
|
|
146
170
|
The above config file, after being processed, would result in the following list of blocks for participant number 1:
|
|
147
171
|
```json
|
|
148
172
|
[
|
|
173
|
+
{
|
|
174
|
+
"name": "initialBlock",
|
|
175
|
+
"config": {
|
|
176
|
+
"param1": 1,
|
|
177
|
+
"participant_index": 1,
|
|
178
|
+
"name": "initialBlock",
|
|
179
|
+
"block_id": 0
|
|
180
|
+
}
|
|
181
|
+
},
|
|
149
182
|
{
|
|
150
183
|
"name": "conditionB",
|
|
151
184
|
"extends": "conditionA",
|
|
@@ -157,11 +190,11 @@ The above config file, after being processed, would result in the following list
|
|
|
157
190
|
"param1": 1,
|
|
158
191
|
"param2": [
|
|
159
192
|
1,
|
|
160
|
-
|
|
193
|
+
3
|
|
161
194
|
],
|
|
162
195
|
"participant_index": 1,
|
|
163
196
|
"name": "conditionB",
|
|
164
|
-
"block_id":
|
|
197
|
+
"block_id": 1
|
|
165
198
|
}
|
|
166
199
|
},
|
|
167
200
|
{
|
|
@@ -174,11 +207,11 @@ The above config file, after being processed, would result in the following list
|
|
|
174
207
|
2
|
|
175
208
|
],
|
|
176
209
|
"param3": [
|
|
177
|
-
|
|
210
|
+
1
|
|
178
211
|
],
|
|
179
212
|
"participant_index": 1,
|
|
180
213
|
"name": "conditionA",
|
|
181
|
-
"block_id":
|
|
214
|
+
"block_id": 2
|
|
182
215
|
}
|
|
183
216
|
},
|
|
184
217
|
{
|
|
@@ -187,15 +220,15 @@ The above config file, after being processed, would result in the following list
|
|
|
187
220
|
"trialsPerItem": 3,
|
|
188
221
|
"param1": 1,
|
|
189
222
|
"param2": [
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
2,
|
|
224
|
+
3
|
|
192
225
|
],
|
|
193
226
|
"param3": [
|
|
194
227
|
2
|
|
195
228
|
],
|
|
196
229
|
"participant_index": 1,
|
|
197
230
|
"name": "conditionA",
|
|
198
|
-
"block_id":
|
|
231
|
+
"block_id": 3
|
|
199
232
|
}
|
|
200
233
|
},
|
|
201
234
|
{
|
|
@@ -208,12 +241,30 @@ The above config file, after being processed, would result in the following list
|
|
|
208
241
|
"trialsPerItem": 3,
|
|
209
242
|
"param1": 1,
|
|
210
243
|
"param2": [
|
|
211
|
-
|
|
212
|
-
|
|
244
|
+
2,
|
|
245
|
+
3
|
|
213
246
|
],
|
|
214
247
|
"participant_index": 1,
|
|
215
248
|
"name": "conditionB",
|
|
216
|
-
"block_id":
|
|
249
|
+
"block_id": 4
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "finalBlockA",
|
|
254
|
+
"config": {
|
|
255
|
+
"param1": 1,
|
|
256
|
+
"participant_index": 1,
|
|
257
|
+
"name": "finalBlockA",
|
|
258
|
+
"block_id": 5
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"name": "finalBlockB",
|
|
263
|
+
"config": {
|
|
264
|
+
"param1": 1,
|
|
265
|
+
"participant_index": 1,
|
|
266
|
+
"name": "finalBlockB",
|
|
267
|
+
"block_id": 6
|
|
217
268
|
}
|
|
218
269
|
}
|
|
219
270
|
]
|
|
@@ -47,18 +47,41 @@ See example `.toml` below for how the configuration can be defined.
|
|
|
47
47
|
# The `configuration` table contains the settings of the study/experiment itself
|
|
48
48
|
[configuration]
|
|
49
49
|
# The `order` is an array of block names or an array of array of block names.
|
|
50
|
+
# The `order` also can be a dictionary/table. If it is a dictionary/table, the keys should
|
|
51
|
+
# be participant indices starting from 1. The values should be a list of block names.
|
|
52
|
+
# The keys are the participant indices - when experiment manager is queries with a given
|
|
53
|
+
# participant index, the dictionary/table will be used as a lookup table and the
|
|
54
|
+
# corresponding list of blocks will be used. When a participant index larger than the
|
|
55
|
+
# highest participant index is queries through experiment manager, the modulus of the
|
|
56
|
+
# index will be used to determine which list of conditions should be used.
|
|
57
|
+
# In the following example order with a table/dictionary, participant index 4 would get
|
|
58
|
+
# the blocks assigned to participant index 1:
|
|
59
|
+
# order = {1= ["conditionA", "conditionB", "conditionA", "conditionB"], 2= ["conditionB", "conditionA", "conditionB", "conditionA"], 3= ["conditionB", "conditionB", "conditionA", "conditionA"]}
|
|
50
60
|
order = [["conditionA", "conditionB", "conditionA", "conditionB"]]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# The `groups_strategy` and `within_groups_strategy` are optional keys that allows you to
|
|
64
|
+
# define how the conditions specified in `order` will be managed. `groups_strategy` would
|
|
65
|
+
# dictate how the top level array of `order` will be handled. `within_groups_strategy`
|
|
66
|
+
# would dictate how the conditions in the nested arrays (if specified) would be
|
|
67
|
+
# managed. These keys can have one of the following values.
|
|
56
68
|
# - "latin_square": Apply latin square to balance the values.
|
|
57
69
|
# - "randomize": For each participant randomize the order of the values in the array.
|
|
58
70
|
# - "as_is": Use the order of the values as specified.
|
|
59
71
|
# When not specified, the default value is "as_is" for both keys.
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
groups_strategy = "latin_square"
|
|
73
|
+
within_groups_strategy = "randomize"
|
|
74
|
+
|
|
75
|
+
# The following are optional keys. `init_blocks` is a list of blocks which will be
|
|
76
|
+
# appended to the start and `final_blocks` are a list of blocks that would be appended to
|
|
77
|
+
# the end. The respective strategies dictate the ordering withing the list of init or
|
|
78
|
+
# final blocks. For init and final blocks only the "randomize" and "as_is" are
|
|
79
|
+
# supported. By default, their values are "as_is".
|
|
80
|
+
init_blocks = ["initialBlock"]
|
|
81
|
+
final_blocks = ["finalBlockA", "finalBlockB"]
|
|
82
|
+
init_blocks_strategy = "as_is"
|
|
83
|
+
final_blocks_strategy = "randomize"
|
|
84
|
+
|
|
62
85
|
# The random seed to use for any randomization. Default seed is 0. The seed will be
|
|
63
86
|
# the value of random_seed + participant_index
|
|
64
87
|
random_seed = 0
|
|
@@ -108,6 +131,15 @@ See [toml spec](https://toml.io/en/v1.0.0) for more information on the format of
|
|
|
108
131
|
The above config file, after being processed, would result in the following list of blocks for participant number 1:
|
|
109
132
|
```json
|
|
110
133
|
[
|
|
134
|
+
{
|
|
135
|
+
"name": "initialBlock",
|
|
136
|
+
"config": {
|
|
137
|
+
"param1": 1,
|
|
138
|
+
"participant_index": 1,
|
|
139
|
+
"name": "initialBlock",
|
|
140
|
+
"block_id": 0
|
|
141
|
+
}
|
|
142
|
+
},
|
|
111
143
|
{
|
|
112
144
|
"name": "conditionB",
|
|
113
145
|
"extends": "conditionA",
|
|
@@ -119,11 +151,11 @@ The above config file, after being processed, would result in the following list
|
|
|
119
151
|
"param1": 1,
|
|
120
152
|
"param2": [
|
|
121
153
|
1,
|
|
122
|
-
|
|
154
|
+
3
|
|
123
155
|
],
|
|
124
156
|
"participant_index": 1,
|
|
125
157
|
"name": "conditionB",
|
|
126
|
-
"block_id":
|
|
158
|
+
"block_id": 1
|
|
127
159
|
}
|
|
128
160
|
},
|
|
129
161
|
{
|
|
@@ -136,11 +168,11 @@ The above config file, after being processed, would result in the following list
|
|
|
136
168
|
2
|
|
137
169
|
],
|
|
138
170
|
"param3": [
|
|
139
|
-
|
|
171
|
+
1
|
|
140
172
|
],
|
|
141
173
|
"participant_index": 1,
|
|
142
174
|
"name": "conditionA",
|
|
143
|
-
"block_id":
|
|
175
|
+
"block_id": 2
|
|
144
176
|
}
|
|
145
177
|
},
|
|
146
178
|
{
|
|
@@ -149,15 +181,15 @@ The above config file, after being processed, would result in the following list
|
|
|
149
181
|
"trialsPerItem": 3,
|
|
150
182
|
"param1": 1,
|
|
151
183
|
"param2": [
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
2,
|
|
185
|
+
3
|
|
154
186
|
],
|
|
155
187
|
"param3": [
|
|
156
188
|
2
|
|
157
189
|
],
|
|
158
190
|
"participant_index": 1,
|
|
159
191
|
"name": "conditionA",
|
|
160
|
-
"block_id":
|
|
192
|
+
"block_id": 3
|
|
161
193
|
}
|
|
162
194
|
},
|
|
163
195
|
{
|
|
@@ -170,12 +202,30 @@ The above config file, after being processed, would result in the following list
|
|
|
170
202
|
"trialsPerItem": 3,
|
|
171
203
|
"param1": 1,
|
|
172
204
|
"param2": [
|
|
173
|
-
|
|
174
|
-
|
|
205
|
+
2,
|
|
206
|
+
3
|
|
175
207
|
],
|
|
176
208
|
"participant_index": 1,
|
|
177
209
|
"name": "conditionB",
|
|
178
|
-
"block_id":
|
|
210
|
+
"block_id": 4
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "finalBlockA",
|
|
215
|
+
"config": {
|
|
216
|
+
"param1": 1,
|
|
217
|
+
"participant_index": 1,
|
|
218
|
+
"name": "finalBlockA",
|
|
219
|
+
"block_id": 5
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "finalBlockB",
|
|
224
|
+
"config": {
|
|
225
|
+
"param1": 1,
|
|
226
|
+
"participant_index": 1,
|
|
227
|
+
"name": "finalBlockB",
|
|
228
|
+
"block_id": 6
|
|
179
229
|
}
|
|
180
230
|
}
|
|
181
231
|
]
|
|
@@ -11,6 +11,14 @@ from experiment_server.utils import ExperimentServerExcetion, FileModifiedWatche
|
|
|
11
11
|
|
|
12
12
|
class ParticipantState:
|
|
13
13
|
def __init__(self, config, participant_index, active):
|
|
14
|
+
"""
|
|
15
|
+
Track a single participant's progress through an experiment configuration.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
config (List[Dict[str, Any]]): Ordered list of block dictionaries for this participant.
|
|
19
|
+
participant_index (int): 1-based participant index.
|
|
20
|
+
active (bool): True if participant is currently between START and END (inclusive of blocks).
|
|
21
|
+
"""
|
|
14
22
|
self.participant_index = participant_index
|
|
15
23
|
self._block_id: int = -1
|
|
16
24
|
self.config = config
|
|
@@ -18,10 +26,19 @@ class ParticipantState:
|
|
|
18
26
|
|
|
19
27
|
@property
|
|
20
28
|
def block_id(self) -> int:
|
|
29
|
+
"""Current zero-based index of the active block, -1 if before START, len(config) if after END."""
|
|
21
30
|
return self._block_id
|
|
22
31
|
|
|
23
32
|
@block_id.setter
|
|
24
33
|
def block_id(self, block_id: int):
|
|
34
|
+
"""
|
|
35
|
+
Set the current block index and update the active flag.
|
|
36
|
+
|
|
37
|
+
If block_id < 0 -> mark as not active (before start).
|
|
38
|
+
If block_id >= len(config) -> mark as not active (after end).
|
|
39
|
+
Otherwise active = True.
|
|
40
|
+
An assertion ensures consistency between block_id and the block's own 'config.block_id'.
|
|
41
|
+
"""
|
|
25
42
|
if block_id < 0:
|
|
26
43
|
self._block_id = -1
|
|
27
44
|
self.active = False
|
|
@@ -36,30 +53,42 @@ class ParticipantState:
|
|
|
36
53
|
|
|
37
54
|
@property
|
|
38
55
|
def block(self) -> Dict[str, Any]|None:
|
|
56
|
+
"""Return the current block dict or None if before START or after END."""
|
|
39
57
|
if self.block_id >= len(self.config) or self.block_id < 0:
|
|
40
58
|
return None
|
|
41
|
-
|
|
42
|
-
return self.config[self.block_id]
|
|
59
|
+
return self.config[self.block_id]
|
|
43
60
|
|
|
44
61
|
@property
|
|
45
62
|
def block_name(self) -> str:
|
|
63
|
+
"""Human-readable block position: 'START', 'END' or the current block name."""
|
|
46
64
|
if self.block_id >= len(self.config):
|
|
47
65
|
return "END"
|
|
48
|
-
|
|
66
|
+
if self.block_id < 0:
|
|
49
67
|
return "START"
|
|
50
68
|
return self.config[self.block_id]["name"]
|
|
51
69
|
|
|
52
70
|
def move_to_next_block(self) -> str:
|
|
71
|
+
"""Advance to the next block and return its block_name."""
|
|
53
72
|
self.block_id += 1
|
|
54
73
|
return self.block_name
|
|
55
74
|
|
|
56
|
-
def status_string(self):
|
|
57
|
-
|
|
75
|
+
def status_string(self) -> str:
|
|
76
|
+
"""Single-line status summary suitable for logs or simple UIs."""
|
|
77
|
+
name = self.block["name"] if self.block is not None else "N/A"
|
|
78
|
+
return f'Participant index: {self.participant_index} \nBlock: {self._block_id} / {len(self.config)} \n Name: {name}'
|
|
58
79
|
|
|
59
80
|
|
|
60
81
|
class Experiment:
|
|
61
|
-
"""Load and manage
|
|
62
|
-
|
|
82
|
+
"""Load and manage an experiment configuration file and participant states."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, config_file: str, default_participant_index: int = 1) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Initialize experiment state and watch the configuration file for changes.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config_file (str): Path to the TOML configuration file.
|
|
90
|
+
default_participant_index (int): Default 1-based index used when none is provided.
|
|
91
|
+
"""
|
|
63
92
|
assert default_participant_index > 0, "Default participant index should be >0"
|
|
64
93
|
self.global_state: Dict[int, ParticipantState] = {}
|
|
65
94
|
self.default_participant_index = default_participant_index
|
|
@@ -67,90 +96,99 @@ class Experiment:
|
|
|
67
96
|
self.config_file = config_file
|
|
68
97
|
|
|
69
98
|
if default_participant_index not in self.global_state:
|
|
70
|
-
self.global_state[default_participant_index] = ParticipantState(
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
self.global_state[default_participant_index] = ParticipantState(
|
|
100
|
+
process_config_file(config_file, default_participant_index),
|
|
101
|
+
default_participant_index,
|
|
102
|
+
False,
|
|
103
|
+
)
|
|
73
104
|
|
|
74
105
|
def _config_file_modified_callback(self):
|
|
106
|
+
"""Reload configurations for all known participants when the file changes."""
|
|
75
107
|
logger.info("Reloading config")
|
|
76
108
|
for participantState in self.global_state.values():
|
|
77
109
|
config = process_config_file(self.config_file, participantState.participant_index)
|
|
78
110
|
participantState.config = config
|
|
79
111
|
|
|
80
112
|
def get_next_participant(self) -> int:
|
|
81
|
-
"""
|
|
113
|
+
"""Allocate and return the next participant index (max existing + 1)."""
|
|
82
114
|
new_participant_index = max(self.global_state.keys()) + 1
|
|
83
115
|
self.add_participant_index(new_participant_index)
|
|
84
116
|
return new_participant_index
|
|
85
117
|
|
|
86
118
|
def add_participant_index(self, participant_index) -> bool:
|
|
87
|
-
"""
|
|
88
|
-
|
|
119
|
+
"""
|
|
120
|
+
Add a participant by index.
|
|
121
|
+
|
|
122
|
+
Returns True if added, False if the index already exists.
|
|
123
|
+
"""
|
|
89
124
|
if participant_index in self.global_state:
|
|
90
125
|
return False
|
|
91
|
-
self.global_state[participant_index] = ParticipantState(
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
self.global_state[participant_index] = ParticipantState(
|
|
127
|
+
process_config_file(self.config_file, participant_index),
|
|
128
|
+
participant_index,
|
|
129
|
+
False,
|
|
130
|
+
)
|
|
94
131
|
return True
|
|
95
132
|
|
|
96
133
|
def get_participant_state(self, participant_index) -> ParticipantState:
|
|
134
|
+
"""
|
|
135
|
+
Return the ParticipantState for the given index.
|
|
136
|
+
|
|
137
|
+
If participant_index is None, uses the default. Raises ExperimentServerExcetion if not present.
|
|
138
|
+
"""
|
|
97
139
|
if participant_index is None:
|
|
98
140
|
participant_index = self.default_participant_index
|
|
99
141
|
if participant_index not in self.global_state:
|
|
100
142
|
raise ExperimentServerExcetion(f"participant with index {participant_index} is not set. Consider using `add_participant_index`")
|
|
101
143
|
return self.global_state[participant_index]
|
|
102
144
|
|
|
103
|
-
def get_state(self, participant_index:int|None=None):
|
|
145
|
+
def get_state(self, participant_index:int|None=None) -> bool:
|
|
146
|
+
"""Return whether the participant is active."""
|
|
104
147
|
if participant_index is None:
|
|
105
148
|
participant_index = self.default_participant_index
|
|
106
149
|
return self.global_state[participant_index].active
|
|
107
150
|
|
|
108
151
|
def move_to_next(self, participant_index:int|None=None) -> str:
|
|
109
|
-
"""
|
|
110
|
-
if `participant_index` is None, seld.default_participant_index is used.
|
|
111
|
-
"""
|
|
152
|
+
"""Advance the participant to the next block and return the new block_name."""
|
|
112
153
|
if participant_index is None:
|
|
113
154
|
participant_index = self.default_participant_index
|
|
114
155
|
return self.global_state[participant_index].move_to_next_block()
|
|
115
156
|
|
|
116
157
|
def get_config(self, participant_index:int|None=None) -> Union[Dict[str, Any], None]:
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
been called atleast once), this will return `None`."""
|
|
158
|
+
"""
|
|
159
|
+
Return the current block's config for the participant, or None if experiment not started or finished.
|
|
160
|
+
"""
|
|
121
161
|
if participant_index is None:
|
|
122
162
|
participant_index = self.default_participant_index
|
|
123
163
|
block = self.global_state[participant_index].block
|
|
124
164
|
if block is None:
|
|
125
165
|
return None
|
|
126
|
-
|
|
127
|
-
return block["config"]
|
|
166
|
+
return block["config"]
|
|
128
167
|
|
|
129
168
|
def reset_participant(self, participant_index:int|None=None) -> bool:
|
|
130
|
-
"""
|
|
169
|
+
"""Reload the participant's configuration from file and replace their stored config."""
|
|
131
170
|
if participant_index is None:
|
|
132
171
|
participant_index = self.default_participant_index
|
|
133
172
|
self.global_state[participant_index].config = process_config_file(self.config_file, participant_index)
|
|
134
173
|
return True
|
|
135
174
|
|
|
136
175
|
def get_blocks_count(self, participant_index:int|None=None) -> int:
|
|
137
|
-
"""Return the
|
|
176
|
+
"""Return the number of blocks for the participant."""
|
|
138
177
|
if participant_index is None:
|
|
139
178
|
participant_index = self.default_participant_index
|
|
140
179
|
return len(self.global_state[participant_index].config)
|
|
141
180
|
|
|
142
181
|
def get_all_configs(self, participant_index:int|None=None) -> List[dict]:
|
|
143
|
-
"""Return all
|
|
144
|
-
if `participant_index` is None, seld.default_participant_index is used.
|
|
145
|
-
"""
|
|
182
|
+
"""Return the list of all block 'config' dicts for the participant in order."""
|
|
146
183
|
if participant_index is None:
|
|
147
184
|
participant_index = self.default_participant_index
|
|
148
185
|
return [c["config"] for c in self.global_state[participant_index].config]
|
|
149
186
|
|
|
150
187
|
def move_to_block(self, block_id: int, participant_index:int|None=None) -> str:
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
"""
|
|
189
|
+
Move the participant pointer to a specific block index and return its block_name.
|
|
190
|
+
|
|
191
|
+
block_id must be an integer.
|
|
154
192
|
"""
|
|
155
193
|
assert isinstance(block_id, int), "`block` should be an int"
|
|
156
194
|
if participant_index is None:
|
|
@@ -159,9 +197,10 @@ class Experiment:
|
|
|
159
197
|
return self.global_state[participant_index].block_name
|
|
160
198
|
|
|
161
199
|
def move_all_to_block(self, block_id: int) -> str:
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
"""
|
|
201
|
+
Move every participant's pointer to `block_id`.
|
|
202
|
+
|
|
203
|
+
Returns the block_name of the first participant after the move.
|
|
165
204
|
"""
|
|
166
205
|
assert isinstance(block_id, int), "`block` should be an int"
|
|
167
206
|
for participantState in self.global_state.values():
|
|
@@ -169,12 +208,17 @@ class Experiment:
|
|
|
169
208
|
return list(self.global_state.values())[0].block_name
|
|
170
209
|
|
|
171
210
|
|
|
172
|
-
def _generate_config_json(config_file: Union[str, Path], participant_indices:Iterable[int], out_dir: Union[str, Path, None]
|
|
211
|
+
def _generate_config_json(config_file: Union[str, Path], participant_indices: Iterable[int], out_dir: Union[str, Path, None]=None) -> None:
|
|
173
212
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
If
|
|
177
|
-
|
|
213
|
+
Emit the resolved JSON configuration for the given participant indices.
|
|
214
|
+
|
|
215
|
+
If out_dir is provided, writes one file per participant named "<config_stem>-participant_<i>.json".
|
|
216
|
+
If out_dir is None, writes each participant's JSON to stdout (one line per participant).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
config_file: Path to the TOML configuration file.
|
|
220
|
+
participant_indices: Iterable of 1-based participant indices to generate.
|
|
221
|
+
out_dir: Optional directory to write files into. Created if missing.
|
|
178
222
|
"""
|
|
179
223
|
if out_dir is not None:
|
|
180
224
|
out_dir = Path(out_dir)
|
|
@@ -6,6 +6,34 @@ from experiment_server.utils import ExperimentServerExcetion
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Client:
|
|
9
|
+
"""
|
|
10
|
+
HTTP client for the Experiment Server API.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
- Instantiate with optional server_host and server_port.
|
|
14
|
+
- Methods operate on a per-participant basis when applicable (provide
|
|
15
|
+
participant_index or omit to use the server's default participant).
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
- server_host (str): Hostname or IP of the server (default "127.0.0.1").
|
|
19
|
+
- server_port (str|int): Port of the server (default "5000").
|
|
20
|
+
|
|
21
|
+
Return value for API methods
|
|
22
|
+
- All public API methods return a tuple (success: bool, data: dict|str).
|
|
23
|
+
On success data is the parsed JSON response (or "" if empty). On failure
|
|
24
|
+
success is False and data contains an error message.
|
|
25
|
+
|
|
26
|
+
Main methods
|
|
27
|
+
- move_to_next(participant_index=None)
|
|
28
|
+
- get_config(participant_index=None)
|
|
29
|
+
- server_is_active(participant_index=None)
|
|
30
|
+
- get_blocks_count(participant_index=None)
|
|
31
|
+
- get_all_configs(participant_index=None)
|
|
32
|
+
- move_to_block(block_id, participant_index=None)
|
|
33
|
+
- new_participant()
|
|
34
|
+
- add_participant(participant_index)
|
|
35
|
+
- shutdown()
|
|
36
|
+
"""
|
|
9
37
|
def __init__(self, server_host:str ="127.0.0.1", server_port:Union[str, int]="5000") -> None:
|
|
10
38
|
self._server_url = f"http://{server_host}:{server_port}"
|
|
11
39
|
|