revisit 0.0.20__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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