revisit 0.0.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- revisit/StudyConfigSchema.json +2130 -0
- revisit/__init__.py +12 -0
- revisit/models.py +2827 -0
- revisit/revisit.py +768 -0
- revisit/static/widget.css +1 -0
- revisit/static/widget.js +55 -0
- revisit/widget.py +27 -0
- revisit-0.0.20.dist-info/METADATA +290 -0
- revisit-0.0.20.dist-info/RECORD +10 -0
- revisit-0.0.20.dist-info/WHEEL +5 -0
revisit/revisit.py
ADDED
@@ -0,0 +1,768 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import json
|
3
|
+
from . import models as rvt_models
|
4
|
+
from pydantic import BaseModel, ValidationError # type: ignore
|
5
|
+
from typing import List, Literal, get_origin, Optional, get_args, Any, Unpack, overload, get_type_hints
|
6
|
+
from enum import Enum
|
7
|
+
import csv
|
8
|
+
from dataclasses import make_dataclass
|
9
|
+
import re
|
10
|
+
import os
|
11
|
+
import shutil
|
12
|
+
from . import widget as _widget
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"component",
|
17
|
+
"sequence",
|
18
|
+
"response",
|
19
|
+
"uiConfig",
|
20
|
+
"studyMetadata",
|
21
|
+
"studyConfig",
|
22
|
+
"data",
|
23
|
+
"widget"
|
24
|
+
]
|
25
|
+
|
26
|
+
|
27
|
+
class _JSONableBaseModel(BaseModel):
|
28
|
+
def __str__(self):
|
29
|
+
return json.dumps(
|
30
|
+
json.loads(
|
31
|
+
self.root.model_dump_json(
|
32
|
+
exclude_none=True, by_alias=True
|
33
|
+
)),
|
34
|
+
indent=4
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
# Private
|
39
|
+
class _WrappedResponse(_JSONableBaseModel):
|
40
|
+
root: rvt_models.Response
|
41
|
+
|
42
|
+
def model_post_init(self, __context: Any) -> None:
|
43
|
+
# Sets the root to be the instantiation of the individual response type instead
|
44
|
+
# of the union response type
|
45
|
+
self.root = self.root.root
|
46
|
+
|
47
|
+
def set(self, overwrite=True, **kwargs) -> _WrappedResponse:
|
48
|
+
for key, value in kwargs.items():
|
49
|
+
# Disallow changing type
|
50
|
+
if key == 'type':
|
51
|
+
if getattr(self.root, key) != value:
|
52
|
+
raise RevisitError(message=f"Cannot change type from {getattr(self.root, key)} to {value}")
|
53
|
+
elif key != 'base':
|
54
|
+
if overwrite is True or (overwrite is False and getattr(self.root, key) is None):
|
55
|
+
setattr(self.root, key, value)
|
56
|
+
|
57
|
+
# Re-validates the model. Returns the new model.
|
58
|
+
self.root = _validate_response(self.root.__dict__)
|
59
|
+
return self
|
60
|
+
|
61
|
+
def clone(self):
|
62
|
+
return __response__(**self.root.__dict__)
|
63
|
+
|
64
|
+
|
65
|
+
# Private
|
66
|
+
class _WrappedComponent(_JSONableBaseModel):
|
67
|
+
component_name__: str
|
68
|
+
base__: Optional[_WrappedComponent] = None
|
69
|
+
context__: Optional[dict] = None
|
70
|
+
metadata__: Optional[dict] = None
|
71
|
+
root: rvt_models.IndividualComponent
|
72
|
+
|
73
|
+
def model_post_init(self, __context: Any) -> None:
|
74
|
+
# Sets the root to be the instantiation of the individual response type instead
|
75
|
+
# of the union response type
|
76
|
+
self.root = self.root.root
|
77
|
+
|
78
|
+
def responses(self, responses: List[_WrappedResponse]) -> _WrappedComponent:
|
79
|
+
for item in responses:
|
80
|
+
if not isinstance(item, _WrappedResponse):
|
81
|
+
raise RevisitError(message=f'Expecting type Response but got {type(item)}')
|
82
|
+
self.root.response = responses
|
83
|
+
return self
|
84
|
+
|
85
|
+
def get_response(self, id: str) -> _WrappedResponse | None:
|
86
|
+
for response in self.root.response:
|
87
|
+
if response.root.id == id:
|
88
|
+
return response
|
89
|
+
return None
|
90
|
+
|
91
|
+
def edit_response(self, id: str, **kwargs) -> _WrappedComponent:
|
92
|
+
for r in self.root.response:
|
93
|
+
if r.root.id == id:
|
94
|
+
# Get dict
|
95
|
+
response_dict = r.root.__dict__
|
96
|
+
# Create new response
|
97
|
+
new_response = __response__(**response_dict)
|
98
|
+
# Set with new values
|
99
|
+
new_response.set(**kwargs)
|
100
|
+
# Filter out old response
|
101
|
+
self.root.response = [_r for _r in self.root.response if _r.root.id != id]
|
102
|
+
# Add new response
|
103
|
+
self.root.response.append(new_response)
|
104
|
+
# Return component
|
105
|
+
return self
|
106
|
+
|
107
|
+
raise ValueError('No response with given ID found.')
|
108
|
+
|
109
|
+
def response_context(self, **kwargs):
|
110
|
+
self.context__ = kwargs
|
111
|
+
|
112
|
+
for type, data in self.context__.items():
|
113
|
+
for response in self.root.response:
|
114
|
+
if response.root.type == type or type == 'all':
|
115
|
+
response.set(
|
116
|
+
overwrite=False,
|
117
|
+
**data
|
118
|
+
)
|
119
|
+
|
120
|
+
return self
|
121
|
+
|
122
|
+
def clone(self, component_name__):
|
123
|
+
return __component__(**self.root.__dict__, component_name__=component_name__)
|
124
|
+
|
125
|
+
|
126
|
+
class _WrappedStudyMetadata(_JSONableBaseModel):
|
127
|
+
root: rvt_models.StudyMetadata
|
128
|
+
|
129
|
+
|
130
|
+
class _WrappedUIConfig(_JSONableBaseModel):
|
131
|
+
root: rvt_models.UIConfig
|
132
|
+
|
133
|
+
|
134
|
+
class _WrappedComponentBlock(_JSONableBaseModel):
|
135
|
+
root: rvt_models.ComponentBlock
|
136
|
+
component_objects__: List[_WrappedComponent]
|
137
|
+
|
138
|
+
def __add__(self, other):
|
139
|
+
"""Allows addition operator to append to sequence components list."""
|
140
|
+
if isinstance(other, _WrappedComponent):
|
141
|
+
self.component_objects__.append(other)
|
142
|
+
self.root.components.append(other.component_name__)
|
143
|
+
return self
|
144
|
+
elif isinstance(other, _WrappedComponentBlock):
|
145
|
+
# Extend existing list of components with new set of components for tracking
|
146
|
+
self.component_objects__.extend(other.component_objects__)
|
147
|
+
|
148
|
+
# Add root object to components
|
149
|
+
self.root.components.append(other.root)
|
150
|
+
return self
|
151
|
+
return NotImplemented
|
152
|
+
|
153
|
+
def from_data(self, data_list) -> DataIterator:
|
154
|
+
if not isinstance(data_list, list):
|
155
|
+
raise RevisitError(
|
156
|
+
message="'from_data' must take in a list of data rows. Use reVISit's 'data' method to parse a CSV file into a valid input."
|
157
|
+
)
|
158
|
+
return DataIterator(data_list, self)
|
159
|
+
|
160
|
+
def get_component(self, name: str) -> _WrappedComponent:
|
161
|
+
for entry in self.component_objects__:
|
162
|
+
if entry.component_name__ == name:
|
163
|
+
return entry
|
164
|
+
|
165
|
+
def permute(
|
166
|
+
self,
|
167
|
+
factors: List[str],
|
168
|
+
order: rvt_models.Order,
|
169
|
+
numSamples: Optional[int] = None,
|
170
|
+
component_function=None
|
171
|
+
) -> None:
|
172
|
+
|
173
|
+
# Initialize components list with blank component if empty
|
174
|
+
make_comp_block = True
|
175
|
+
if len(self.component_objects__) == 0:
|
176
|
+
self = self + __component__(type='questionnaire', component_name__='place-holder-component')
|
177
|
+
# If there only exists one component (either existing one or placeholder),
|
178
|
+
# do not create the first component blocks.
|
179
|
+
if len(self.component_objects__) == 1:
|
180
|
+
make_comp_block = False
|
181
|
+
|
182
|
+
# Convert to JSON
|
183
|
+
self_json = json.loads(self.__str__())
|
184
|
+
# Get all current component dictionaries
|
185
|
+
components_dict = {c.component_name__: c for c in self.component_objects__}
|
186
|
+
# Recursively start permutation function
|
187
|
+
new_permuted_component_block = _recursive_json_permutation(
|
188
|
+
self_json,
|
189
|
+
factors=factors,
|
190
|
+
order=order,
|
191
|
+
numSamples=numSamples,
|
192
|
+
input_components=components_dict,
|
193
|
+
component_function=component_function,
|
194
|
+
make_comp_block=make_comp_block
|
195
|
+
)
|
196
|
+
# Set new objects
|
197
|
+
self.component_objects__ = new_permuted_component_block.component_objects__
|
198
|
+
# Set new root
|
199
|
+
self.root = new_permuted_component_block.root
|
200
|
+
return self
|
201
|
+
|
202
|
+
|
203
|
+
class _WrappedStudyConfig(_JSONableBaseModel):
|
204
|
+
root: rvt_models.StudyConfig
|
205
|
+
|
206
|
+
|
207
|
+
class _StudyConfigType(rvt_models.StudyConfigType):
|
208
|
+
components: List[_WrappedComponent]
|
209
|
+
|
210
|
+
|
211
|
+
class DataIterator:
|
212
|
+
def __init__(self, data_list: List, parent_class: _WrappedComponentBlock):
|
213
|
+
self.data = data_list
|
214
|
+
self.parent_class = parent_class
|
215
|
+
|
216
|
+
def component(self, **kwargs):
|
217
|
+
for datum in self.data:
|
218
|
+
current_dict = {}
|
219
|
+
for key, value in kwargs.items():
|
220
|
+
if key == 'parameters':
|
221
|
+
param_dict = {}
|
222
|
+
for param_key, param_value in value.items():
|
223
|
+
if type(param_value) is str:
|
224
|
+
param_datum_value = _extract_datum_value(param_value)
|
225
|
+
if param_datum_value is not None:
|
226
|
+
param_dict[param_key] = getattr(datum, param_datum_value)
|
227
|
+
else:
|
228
|
+
param_dict[param_key] = value
|
229
|
+
else:
|
230
|
+
param_dict[param_key] = value
|
231
|
+
current_dict[key] = param_dict
|
232
|
+
else:
|
233
|
+
if type(value) is str:
|
234
|
+
datum_value = _extract_datum_value(value)
|
235
|
+
if datum_value is not None:
|
236
|
+
if key == 'component_name__':
|
237
|
+
current_dict[key] = str(getattr(datum, datum_value))
|
238
|
+
else:
|
239
|
+
current_dict[key] = getattr(datum, datum_value)
|
240
|
+
else:
|
241
|
+
current_dict[key] = value
|
242
|
+
else:
|
243
|
+
current_dict[key] = value
|
244
|
+
curr_component = __component__(**current_dict)
|
245
|
+
self.parent_class = self.parent_class + curr_component
|
246
|
+
# Return the parent class calling iterator when component is finished.
|
247
|
+
return self.parent_class
|
248
|
+
|
249
|
+
|
250
|
+
# # -----------------------------------
|
251
|
+
# # Factory Functions
|
252
|
+
# # -----------------------------------
|
253
|
+
|
254
|
+
# Component factory function
|
255
|
+
# Allows additional items to be sent over to our Component model while keeping restrictions
|
256
|
+
# for the model that is auto-generated.
|
257
|
+
|
258
|
+
@overload
|
259
|
+
def component(**kwargs: Unpack[rvt_models.MarkdownComponentType]) -> _WrappedComponent: ...
|
260
|
+
@overload
|
261
|
+
def component(**kwargs: Unpack[rvt_models.ReactComponentType]) -> _WrappedComponent: ...
|
262
|
+
@overload
|
263
|
+
def component(**kwargs: Unpack[rvt_models.ImageComponentType]) -> _WrappedComponent: ...
|
264
|
+
@overload
|
265
|
+
def component(**kwargs: Unpack[rvt_models.WebsiteComponentType]) -> _WrappedComponent: ...
|
266
|
+
@overload
|
267
|
+
def component(**kwargs: Unpack[rvt_models.QuestionnaireComponentType]) -> _WrappedComponent: ...
|
268
|
+
@overload
|
269
|
+
def component(**kwargs: Any) -> _WrappedComponent: ...
|
270
|
+
|
271
|
+
|
272
|
+
def component(**kwargs) -> _WrappedComponent:
|
273
|
+
# Inherit base
|
274
|
+
base_component = kwargs.get('base__', None)
|
275
|
+
if base_component:
|
276
|
+
base_fields = vars(base_component.root)
|
277
|
+
for key, value in base_fields.items():
|
278
|
+
if key not in kwargs:
|
279
|
+
kwargs[key] = value
|
280
|
+
# Get kwargs to pass to individual component
|
281
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.IndividualComponent, kwargs)
|
282
|
+
# Grab response list
|
283
|
+
response = filter_kwargs.get('response')
|
284
|
+
|
285
|
+
# Sets default response list
|
286
|
+
valid_response = []
|
287
|
+
# If response present
|
288
|
+
if response is not None:
|
289
|
+
for r in response:
|
290
|
+
|
291
|
+
# Prevent dict input
|
292
|
+
if isinstance(r, dict):
|
293
|
+
raise RevisitError(message='Cannot pass a dictionary directly into "Response" list.')
|
294
|
+
|
295
|
+
response_type_hint = get_type_hints(rvt_models.Response).get('root')
|
296
|
+
response_types = get_args(response_type_hint)
|
297
|
+
|
298
|
+
# If wrapped, get root
|
299
|
+
if isinstance(r, _WrappedResponse):
|
300
|
+
valid_response.append(r.root)
|
301
|
+
|
302
|
+
# If not wrapped but is valid response, append to list
|
303
|
+
elif r.__class__ in response_types:
|
304
|
+
valid_response.append(r)
|
305
|
+
|
306
|
+
# If other unknown type, raise error
|
307
|
+
else:
|
308
|
+
raise RevisitError(message=f'Invalid type {type(r)} for "Response" class.')
|
309
|
+
|
310
|
+
filter_kwargs['response'] = valid_response
|
311
|
+
|
312
|
+
# Validate component
|
313
|
+
_validate_component(filter_kwargs)
|
314
|
+
base_model = rvt_models.IndividualComponent(**filter_kwargs)
|
315
|
+
|
316
|
+
try:
|
317
|
+
return _WrappedComponent(**kwargs, root=base_model)
|
318
|
+
except ValidationError as e:
|
319
|
+
raise RevisitError(e.errors())
|
320
|
+
|
321
|
+
|
322
|
+
# Shadowing
|
323
|
+
__component__ = component
|
324
|
+
|
325
|
+
|
326
|
+
# Response factory function
|
327
|
+
@overload
|
328
|
+
def response(**kwargs: Unpack[rvt_models.NumericalResponseType]) -> _WrappedResponse: ...
|
329
|
+
@overload
|
330
|
+
def response(**kwargs: Unpack[rvt_models.ShortTextResponseType]) -> _WrappedResponse: ...
|
331
|
+
@overload
|
332
|
+
def response(**kwargs: Unpack[rvt_models.LongTextResponseType]) -> _WrappedResponse: ...
|
333
|
+
@overload
|
334
|
+
def response(**kwargs: Unpack[rvt_models.LikertResponseType]) -> _WrappedResponse: ...
|
335
|
+
@overload
|
336
|
+
def response(**kwargs: Unpack[rvt_models.DropdownResponseType]) -> _WrappedResponse: ...
|
337
|
+
@overload
|
338
|
+
def response(**kwargs: Unpack[rvt_models.SliderResponseType]) -> _WrappedResponse: ...
|
339
|
+
@overload
|
340
|
+
def response(**kwargs: Unpack[rvt_models.RadioResponseType]) -> _WrappedResponse: ...
|
341
|
+
@overload
|
342
|
+
def response(**kwargs: Unpack[rvt_models.CheckboxResponseType]) -> _WrappedResponse: ...
|
343
|
+
@overload
|
344
|
+
def response(**kwargs: Unpack[rvt_models.IFrameResponseType]) -> _WrappedResponse: ...
|
345
|
+
@overload
|
346
|
+
def response(**kwargs: Unpack[rvt_models.MatrixResponseType]) -> _WrappedResponse: ...
|
347
|
+
@overload
|
348
|
+
def response(**kwargs: Any) -> _WrappedResponse: ...
|
349
|
+
|
350
|
+
|
351
|
+
def response(**kwargs) -> _WrappedResponse:
|
352
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.Response, kwargs)
|
353
|
+
_validate_response(filter_kwargs)
|
354
|
+
base_model = rvt_models.Response(**filter_kwargs)
|
355
|
+
# We've validated the response for a particular type. Now, how do we validate the wrapped component correctly?
|
356
|
+
try:
|
357
|
+
return _WrappedResponse(**kwargs, root=base_model)
|
358
|
+
except ValidationError as e:
|
359
|
+
raise RevisitError(e.errors())
|
360
|
+
|
361
|
+
|
362
|
+
# Shadowing
|
363
|
+
__response__ = response
|
364
|
+
|
365
|
+
|
366
|
+
def studyMetadata(**kwargs: Unpack[rvt_models.StudyMetadataType]):
|
367
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.StudyMetadata, kwargs)
|
368
|
+
base_model = rvt_models.StudyMetadata(**filter_kwargs)
|
369
|
+
return _WrappedStudyMetadata(**kwargs, root=base_model)
|
370
|
+
|
371
|
+
|
372
|
+
def uiConfig(**kwargs: Unpack[rvt_models.UIConfigType]):
|
373
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.UIConfig, kwargs)
|
374
|
+
base_model = rvt_models.UIConfig(**filter_kwargs)
|
375
|
+
return _WrappedUIConfig(**kwargs, root=base_model)
|
376
|
+
|
377
|
+
|
378
|
+
def sequence(**kwargs: Unpack[rvt_models.ComponentBlockType]):
|
379
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.ComponentBlock, kwargs)
|
380
|
+
valid_component_names = []
|
381
|
+
valid_components = []
|
382
|
+
components = filter_kwargs.get('components')
|
383
|
+
if components is not None:
|
384
|
+
for c in components:
|
385
|
+
|
386
|
+
# Prevent dict input
|
387
|
+
if isinstance(c, dict):
|
388
|
+
raise RevisitError(message='Cannot pass a dictionary directly into "Component" list.')
|
389
|
+
|
390
|
+
# If wrapped, get root
|
391
|
+
if isinstance(c, _WrappedComponent):
|
392
|
+
valid_component_names.append(c.component_name__)
|
393
|
+
valid_components.append(c)
|
394
|
+
|
395
|
+
# If other unknown type, raise error
|
396
|
+
else:
|
397
|
+
raise RevisitError(message=f'Invalid type {type(c)} for "Component" class.')
|
398
|
+
|
399
|
+
filter_kwargs['components'] = valid_component_names
|
400
|
+
base_model = rvt_models.ComponentBlock(**filter_kwargs)
|
401
|
+
return _WrappedComponentBlock(**kwargs, root=base_model, component_objects__=valid_components)
|
402
|
+
|
403
|
+
|
404
|
+
# Shadowing
|
405
|
+
__sequence__ = sequence
|
406
|
+
|
407
|
+
|
408
|
+
@overload
|
409
|
+
def studyConfig(**kwargs: Unpack[_StudyConfigType]) -> _WrappedStudyConfig: ...
|
410
|
+
@overload
|
411
|
+
def studyConfig(**kwargs: Any) -> _WrappedStudyConfig: ...
|
412
|
+
|
413
|
+
|
414
|
+
def studyConfig(**kwargs: Unpack[_StudyConfigType]) -> _WrappedStudyConfig:
|
415
|
+
filter_kwargs = _get_filtered_kwargs(rvt_models.StudyConfig, kwargs)
|
416
|
+
|
417
|
+
root_list = ['studyMetadata', 'uiConfig', 'sequence']
|
418
|
+
un_rooted_kwargs = {x: (y.root if x in root_list and hasattr(y, 'root') else y) for x, y in filter_kwargs.items()}
|
419
|
+
|
420
|
+
study_sequence = filter_kwargs['sequence']
|
421
|
+
|
422
|
+
# Merges components from the components list given and the components that are stored in the sequence
|
423
|
+
un_rooted_kwargs['components'] = {
|
424
|
+
comp.component_name__: comp.root for comp in un_rooted_kwargs.get('components', [])
|
425
|
+
} | {
|
426
|
+
comp.component_name__: comp.root for comp in study_sequence.component_objects__
|
427
|
+
}
|
428
|
+
|
429
|
+
base_model = rvt_models.StudyConfig(**un_rooted_kwargs)
|
430
|
+
return _WrappedStudyConfig(**kwargs, root=base_model)
|
431
|
+
|
432
|
+
|
433
|
+
# Function to parse the CSV and dynamically create data classes
|
434
|
+
def data(file_path: str) -> List[Any]:
|
435
|
+
# Read the first row to get the headers
|
436
|
+
with open(file_path, mode='r') as csvfile:
|
437
|
+
csv_reader = csv.DictReader(csvfile)
|
438
|
+
headers = csv_reader.fieldnames
|
439
|
+
if not headers:
|
440
|
+
raise RevisitError(message="No headers found in CSV file.")
|
441
|
+
|
442
|
+
# Create a data class with attributes based on the headers
|
443
|
+
DataRow = make_dataclass("DataRow", [(header, Any) for header in headers])
|
444
|
+
|
445
|
+
# Parse each row into an instance of the dynamically created data class
|
446
|
+
data_rows = []
|
447
|
+
for row in csv_reader:
|
448
|
+
# Convert the row values to the appropriate types (e.g., int, float, bool)
|
449
|
+
data = {key: _convert_value(value) for key, value in row.items()}
|
450
|
+
data_row = DataRow(**data)
|
451
|
+
data_rows.append(data_row)
|
452
|
+
|
453
|
+
return data_rows
|
454
|
+
|
455
|
+
|
456
|
+
def widget(study: _WrappedStudyConfig, revisitPath: str):
|
457
|
+
if not os.path.isdir(revisitPath):
|
458
|
+
raise RevisitError(message=f'"{revisitPath}" does not exist.')
|
459
|
+
|
460
|
+
extracted_paths = []
|
461
|
+
|
462
|
+
for component in study.root.components.values():
|
463
|
+
actual_component = component.root
|
464
|
+
if hasattr(actual_component, 'root'):
|
465
|
+
actual_component = actual_component.root
|
466
|
+
if hasattr(actual_component, 'path'):
|
467
|
+
|
468
|
+
fileName = actual_component.path.split('/')[-1]
|
469
|
+
|
470
|
+
if actual_component.type == 'react-component':
|
471
|
+
dest = f"{revisitPath}/src/public/__revisit-widget/assets/{fileName}"
|
472
|
+
else:
|
473
|
+
dest = f"{revisitPath}/public/__revisit-widget/assets/{fileName}"
|
474
|
+
|
475
|
+
extracted_paths.append({
|
476
|
+
"src": actual_component.path,
|
477
|
+
"dest": dest
|
478
|
+
})
|
479
|
+
|
480
|
+
newPath = f"__revisit-widget/assets/{fileName}"
|
481
|
+
actual_component.path = newPath
|
482
|
+
|
483
|
+
uiConfig = study.root.uiConfig
|
484
|
+
if uiConfig.helpTextPath is not None:
|
485
|
+
|
486
|
+
fileName = uiConfig.helpTextPath.split('/')[-1]
|
487
|
+
dest = f"{revisitPath}/public/__revisit-widget/assets/{fileName}"
|
488
|
+
|
489
|
+
extracted_paths.append({
|
490
|
+
"src": uiConfig.helpTextPath,
|
491
|
+
"dest": dest
|
492
|
+
})
|
493
|
+
|
494
|
+
newPath = f"__revisit-widget/assets/{fileName}"
|
495
|
+
uiConfig.helpTextPath = newPath
|
496
|
+
|
497
|
+
if uiConfig.logoPath is not None:
|
498
|
+
|
499
|
+
fileName = uiConfig.logoPath.split('/')[-1]
|
500
|
+
|
501
|
+
dest = f"{revisitPath}/public/__revisit-widget/assets/{fileName}"
|
502
|
+
|
503
|
+
extracted_paths.append({
|
504
|
+
"src": uiConfig.logoPath,
|
505
|
+
"dest": dest
|
506
|
+
})
|
507
|
+
|
508
|
+
newPath = f"__revisit-widget/assets/{fileName}"
|
509
|
+
uiConfig.logoPath = newPath
|
510
|
+
|
511
|
+
# Copy all files
|
512
|
+
for item in extracted_paths:
|
513
|
+
_copy_file(item['src'], item['dest'])
|
514
|
+
|
515
|
+
w = _widget.Widget()
|
516
|
+
w.config = json.loads(study.__str__())
|
517
|
+
return w
|
518
|
+
|
519
|
+
|
520
|
+
# ------- PRIVATE FUNCTIONS ------------ #
|
521
|
+
|
522
|
+
def _validate_component(kwargs: dict):
|
523
|
+
component_mapping = _generate_possible_component_types()[1]
|
524
|
+
if 'type' not in kwargs:
|
525
|
+
raise RevisitError(message='"Type" is required on Component.')
|
526
|
+
elif component_mapping.get(kwargs['type']) is None:
|
527
|
+
raise RevisitError(message=f'Unexpected component type: {kwargs['type']}')
|
528
|
+
|
529
|
+
try:
|
530
|
+
return rvt_models.IndividualComponent.model_validate(kwargs).root
|
531
|
+
except ValidationError as e:
|
532
|
+
temp_errors = []
|
533
|
+
|
534
|
+
for entry in e.errors():
|
535
|
+
if entry['loc'][0] == component_mapping[kwargs['type']]:
|
536
|
+
temp_errors.append(entry)
|
537
|
+
|
538
|
+
if len(temp_errors) > 0:
|
539
|
+
raise RevisitError(temp_errors)
|
540
|
+
else:
|
541
|
+
raise RevisitError(
|
542
|
+
message='Unexpected error occurred during Component instantiation.'
|
543
|
+
)
|
544
|
+
|
545
|
+
|
546
|
+
# Call validate response when creating response component.
|
547
|
+
def _validate_response(kwargs: dict):
|
548
|
+
response_mapping = _generate_possible_response_types()[1]
|
549
|
+
if 'type' not in kwargs:
|
550
|
+
raise RevisitError(message='"Type" is required on Response.')
|
551
|
+
else:
|
552
|
+
|
553
|
+
type_value = kwargs.get('type')
|
554
|
+
|
555
|
+
# Handles enum class type
|
556
|
+
if isinstance(kwargs.get('type'), Enum):
|
557
|
+
type_value = type_value.value
|
558
|
+
|
559
|
+
if response_mapping.get(type_value) is None:
|
560
|
+
raise RevisitError(message=f'Unexpected type: {type_value}')
|
561
|
+
|
562
|
+
try:
|
563
|
+
return rvt_models.Response.model_validate(kwargs).root
|
564
|
+
except ValidationError as e:
|
565
|
+
temp_errors = []
|
566
|
+
for entry in e.errors():
|
567
|
+
if entry['loc'][0] == response_mapping[type_value]:
|
568
|
+
temp_errors.append(entry)
|
569
|
+
|
570
|
+
if len(temp_errors) > 0:
|
571
|
+
raise RevisitError(temp_errors)
|
572
|
+
else:
|
573
|
+
raise RevisitError(
|
574
|
+
message='Unexpected error occurred during Response instantiation'
|
575
|
+
)
|
576
|
+
|
577
|
+
|
578
|
+
def _generate_possible_response_types():
|
579
|
+
return _generate_possible_types(rvt_models.Response)
|
580
|
+
|
581
|
+
|
582
|
+
def _generate_possible_component_types():
|
583
|
+
return _generate_possible_types(rvt_models.IndividualComponent)
|
584
|
+
|
585
|
+
|
586
|
+
# Generates mappings between the response class name and the
|
587
|
+
# type string literal. Creates the reversed mapping as well.
|
588
|
+
def _generate_possible_types(orig_cls):
|
589
|
+
type_hints = get_type_hints(orig_cls).get('root')
|
590
|
+
types = get_args(type_hints)
|
591
|
+
type_hints = {}
|
592
|
+
type_hints_reversed = {}
|
593
|
+
for cls in types:
|
594
|
+
# If class is the union of two separate classes,
|
595
|
+
# need to get types from root
|
596
|
+
if get_type_hints(cls).get('root') is not None:
|
597
|
+
test = get_type_hints(cls).get('root')
|
598
|
+
for item in get_args(test):
|
599
|
+
curr_type = get_type_hints(item).get('type')
|
600
|
+
type_hints[cls.__name__] = set([get_args(curr_type)[0]])
|
601
|
+
type_hints_reversed[get_args(curr_type)[0]] = cls.__name__
|
602
|
+
else:
|
603
|
+
curr_type = get_type_hints(cls).get('type')
|
604
|
+
curr_origin = get_origin(get_type_hints(cls).get('type'))
|
605
|
+
if curr_origin is Literal:
|
606
|
+
type_hints[cls.__name__] = set([get_args(curr_type)[0]])
|
607
|
+
type_hints_reversed[get_args(curr_type)[0]] = cls.__name__
|
608
|
+
elif isinstance(curr_type, type) and issubclass(curr_type, Enum):
|
609
|
+
enum_list = [member.value for member in curr_type]
|
610
|
+
type_hints[cls.__name__] = set(enum_list)
|
611
|
+
for item in enum_list:
|
612
|
+
type_hints_reversed[item] = cls.__name__
|
613
|
+
return (type_hints, type_hints_reversed)
|
614
|
+
|
615
|
+
|
616
|
+
# Custom exception
|
617
|
+
class RevisitError(Exception):
|
618
|
+
def __init__(self, errors=None, message=None):
|
619
|
+
# Case 1: Validation Errors From Pydantic
|
620
|
+
# Case 2: Standard Error Message
|
621
|
+
super().__init__('There was an error.')
|
622
|
+
if message is None:
|
623
|
+
pretty_message_list = pretty_error(errors)
|
624
|
+
self.message = \
|
625
|
+
f'There was an error. \n' \
|
626
|
+
f'----------------------------------------------------' \
|
627
|
+
f'\n\n' \
|
628
|
+
f'{'\n\n'.join(pretty_message_list)}' \
|
629
|
+
f'\n'
|
630
|
+
else:
|
631
|
+
self.message = \
|
632
|
+
f'There was an error. \n' \
|
633
|
+
f'----------------------------------------------------' \
|
634
|
+
f'\n\n' \
|
635
|
+
f'{message}' \
|
636
|
+
f'\n'
|
637
|
+
|
638
|
+
def __str__(self):
|
639
|
+
return self.message
|
640
|
+
|
641
|
+
|
642
|
+
def pretty_error(errors):
|
643
|
+
custom_messages = {
|
644
|
+
'missing': 'Field is missing'
|
645
|
+
}
|
646
|
+
new_error_messages = []
|
647
|
+
for error in errors:
|
648
|
+
custom_message = custom_messages.get(error['type'])
|
649
|
+
if custom_message:
|
650
|
+
new_error_messages.append(f'Location: {error['loc']}\nError: Field "{error['loc'][-1]}" is required.')
|
651
|
+
else:
|
652
|
+
new_error_messages.append(f'Location: {error['loc']}\nError: {error['msg']}')
|
653
|
+
return new_error_messages
|
654
|
+
|
655
|
+
|
656
|
+
def _get_filtered_kwargs(class_type: Any, kwargs):
|
657
|
+
try:
|
658
|
+
possible_items = get_args(class_type.model_fields.get('root').annotation)
|
659
|
+
except AttributeError:
|
660
|
+
possible_items = [class_type]
|
661
|
+
|
662
|
+
valid_fields = set()
|
663
|
+
for model in possible_items:
|
664
|
+
if 'root' in model.model_fields.keys():
|
665
|
+
unioned_classes = (get_args(get_type_hints(model).get('root')))
|
666
|
+
for cls in unioned_classes:
|
667
|
+
valid_fields.update(cls.model_fields.keys())
|
668
|
+
|
669
|
+
valid_fields.update(model.model_fields.keys())
|
670
|
+
|
671
|
+
return {key: value for key, value in kwargs.items() if key in valid_fields}
|
672
|
+
|
673
|
+
|
674
|
+
def _convert_value(value: str) -> Any:
|
675
|
+
"""Helper function to convert string values to appropriate data types."""
|
676
|
+
value = value.strip()
|
677
|
+
if value.lower() == "true":
|
678
|
+
return True
|
679
|
+
elif value.lower() == "false":
|
680
|
+
return False
|
681
|
+
try:
|
682
|
+
if '.' in value:
|
683
|
+
return float(value)
|
684
|
+
else:
|
685
|
+
return int(value)
|
686
|
+
except ValueError:
|
687
|
+
return value # Return as string if it cannot be converted
|
688
|
+
|
689
|
+
|
690
|
+
def _extract_datum_value(text: str) -> str:
|
691
|
+
# Use regex to match 'datum:thing' and capture 'thing'
|
692
|
+
match = re.match(r'^datum:(\w+)$', text)
|
693
|
+
if match:
|
694
|
+
return match.group(1) # Return the captured part (i.e., 'thing')
|
695
|
+
return None # Return None if the pattern doesn't match
|
696
|
+
|
697
|
+
|
698
|
+
def _copy_file(src: str, dest: str):
|
699
|
+
# Check if file exists
|
700
|
+
if not os.path.exists(src):
|
701
|
+
raise RevisitError(message=f'File "{src}" not found.')
|
702
|
+
|
703
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
704
|
+
|
705
|
+
print(f'Copying file from {src} to {dest}')
|
706
|
+
shutil.copyfile(src, dest)
|
707
|
+
|
708
|
+
|
709
|
+
def _recursive_json_permutation(
|
710
|
+
input_json: dict,
|
711
|
+
factors: List[str],
|
712
|
+
order: rvt_models.Order,
|
713
|
+
input_components: dict,
|
714
|
+
numSamples: Optional[int] = None,
|
715
|
+
component_function=None,
|
716
|
+
make_comp_block=True
|
717
|
+
):
|
718
|
+
new_seq = __sequence__(order=input_json['order'], numSamples=input_json.get('numSamples'))
|
719
|
+
while input_json['components']:
|
720
|
+
c = input_json['components'].pop()
|
721
|
+
# If component name
|
722
|
+
if isinstance(c, str):
|
723
|
+
# Get orig component
|
724
|
+
curr_comp = input_components[c]
|
725
|
+
# Create new comp block for permuting this component across all factors
|
726
|
+
curr_seq = __sequence__(order=order, numSamples=numSamples)
|
727
|
+
# Generate new comp for each
|
728
|
+
for entry in factors:
|
729
|
+
# Assign params
|
730
|
+
metadata = entry
|
731
|
+
if curr_comp.metadata__ is not None:
|
732
|
+
metadata = {**curr_comp.metadata__, **entry}
|
733
|
+
# Create new component
|
734
|
+
comp_name = ":".join(f"{key}:{value}" for key, value in entry.items())
|
735
|
+
if component_function:
|
736
|
+
new_comp = component_function(**metadata)
|
737
|
+
else:
|
738
|
+
new_comp = __component__(
|
739
|
+
base__=curr_comp,
|
740
|
+
component_name__=f"{c}__{comp_name}",
|
741
|
+
metadata__=metadata
|
742
|
+
)
|
743
|
+
# Add to curr seq block
|
744
|
+
curr_seq = curr_seq + new_comp
|
745
|
+
# Add seq block to outer seq block
|
746
|
+
else:
|
747
|
+
new_input_json = c
|
748
|
+
temp_num_samples = None
|
749
|
+
if new_input_json.get('numSamples') is not None:
|
750
|
+
temp_num_samples = new_input_json['numSamples']
|
751
|
+
|
752
|
+
curr_seq = _recursive_json_permutation(
|
753
|
+
input_json=new_input_json,
|
754
|
+
order=order,
|
755
|
+
numSamples=numSamples,
|
756
|
+
input_components=input_components,
|
757
|
+
factors=factors,
|
758
|
+
component_function=component_function
|
759
|
+
)
|
760
|
+
curr_seq.root.order = new_input_json['order']
|
761
|
+
curr_seq.root.numSamples = temp_num_samples
|
762
|
+
new_seq = new_seq + curr_seq
|
763
|
+
|
764
|
+
# Only return curr sequence if not making the comp block.
|
765
|
+
if make_comp_block is True:
|
766
|
+
return new_seq
|
767
|
+
else:
|
768
|
+
return curr_seq
|