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/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