revisit 0.0.2__py2.py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
revisit/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ import importlib.metadata
2
+ import pathlib
3
+ import anywidget
4
+ import traitlets
5
+
6
+ try:
7
+ __version__ = importlib.metadata.version("revisit_notebook_widget")
8
+ except importlib.metadata.PackageNotFoundError:
9
+ __version__ = "unknown"
10
+
11
+
12
+
13
+ # class Widget2(anywidget.AnyWidget):
14
+
15
+ class TestWidget(anywidget.AnyWidget):
16
+ _esm = """
17
+ function render({ model, el }) {
18
+ let button = document.createElement("button");
19
+ button.innerHTML = `count is ${model.get("value")}`;
20
+ button.addEventListener("click", () => {
21
+ model.set("value", model.get("value") + 1);
22
+ model.save_changes();
23
+ });
24
+ model.on("change:value", () => {
25
+ button.innerHTML = `count is ${model.get("value")}`;
26
+ });
27
+ el.classList.add("counter-widget");
28
+ el.appendChild(button);
29
+ }
30
+ export default { render };
31
+ """
32
+ _css = """
33
+ .counter-widget button { color: white; font-size: 1.75rem; background-color: #ea580c; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; }
34
+ .counter-widget button:hover { background-color: #9a3412; }
35
+ """
36
+ value = traitlets.Int(0).tag(sync=True)
37
+
38
+
39
+
40
+ class Widget(anywidget.AnyWidget):
41
+ _esm = pathlib.Path(__file__).parent / "static" / "widget.js"
42
+ _css = pathlib.Path(__file__).parent / "static" / "widget.css"
43
+ # value = traitlets.Int(0).tag(sync=True)
44
+ config = traitlets.Dict({}).tag(sync=True)
45
+ sequence = traitlets.List([]).tag(sync=True)
46
+ internalWidget = TestWidget()
47
+
48
+ @traitlets.observe('sequence')
49
+ def _sequence_changed(self, change):
50
+ self.internalWidget.value += 1
51
+ # internalWidget.value += 1
52
+ # print("{name} changed from {old} to {new}".format(**change))
53
+
54
+
@@ -0,0 +1,521 @@
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
+
11
+
12
+ class _JSONableBaseModel(BaseModel):
13
+ def __str__(self):
14
+ return json.dumps(
15
+ json.loads(
16
+ self.root.model_dump_json(
17
+ exclude_none=True, by_alias=True
18
+ )),
19
+ indent=4
20
+ )
21
+
22
+
23
+ # Private
24
+ class _WrappedResponse(_JSONableBaseModel):
25
+ root: rvt_models.Response
26
+
27
+ def model_post_init(self, __context: Any) -> None:
28
+ # Sets the root to be the instantiation of the individual response type instead
29
+ # of the union response type
30
+ self.root = self.root.root
31
+
32
+ def set(self, overwrite=True, **kwargs) -> _WrappedResponse:
33
+ for key, value in kwargs.items():
34
+ # Disallow changing type
35
+ if key == 'type':
36
+ if getattr(self.root, key) != value:
37
+ raise RevisitError(message=f"Cannot change type from {getattr(self.root, key)} to {value}")
38
+ elif key != 'base':
39
+ if overwrite is True or (overwrite is False and getattr(self.root, key) is None):
40
+ setattr(self.root, key, value)
41
+
42
+ # Re-validates the model. Returns the new model.
43
+ self.root = _validate_response(self.root.__dict__)
44
+ return self
45
+
46
+
47
+ # Private
48
+ class _WrappedComponent(_JSONableBaseModel):
49
+ component_name__: str
50
+ base__: Optional[_WrappedComponent] = None
51
+ context__: Optional[dict] = None
52
+ root: rvt_models.IndividualComponent
53
+
54
+ def model_post_init(self, __context: Any) -> None:
55
+ # Sets the root to be the instantiation of the individual response type instead
56
+ # of the union response type
57
+ self.root = self.root.root
58
+
59
+ def responses(self, responses: List[_WrappedResponse]) -> _WrappedComponent:
60
+ for item in responses:
61
+ if not isinstance(item, _WrappedResponse):
62
+ raise ValueError(f'Expecting type Response got {type(item)}')
63
+ self.root.response = responses
64
+ return self
65
+
66
+ def get_response(self, id: str) -> _WrappedResponse | None:
67
+ for response in self.root.response:
68
+ if response.root.id == id:
69
+ return response
70
+ return None
71
+
72
+ def edit_response(self, id: str, **kwargs) -> _WrappedComponent:
73
+ for response in self.root.response:
74
+ if response.root.id == id:
75
+ response.set(**kwargs)
76
+ return self
77
+
78
+ raise ValueError('No response with given ID found.')
79
+
80
+ def response_context(self, **kwargs):
81
+ self.context__ = kwargs
82
+
83
+ for type, data in self.context__.items():
84
+ for response in self.root.response:
85
+ if response.root.type == type or type == 'all':
86
+ response.set(
87
+ overwrite=False,
88
+ **data
89
+ )
90
+
91
+ return self
92
+
93
+
94
+ class _WrappedStudyMetadata(_JSONableBaseModel):
95
+ root: rvt_models.StudyMetadata
96
+
97
+
98
+ class _WrappedUIConfig(_JSONableBaseModel):
99
+ root: rvt_models.UIConfig
100
+
101
+
102
+ class _WrappedComponentBlock(_JSONableBaseModel):
103
+ root: rvt_models.ComponentBlock
104
+ component_objects__: List[_WrappedComponent]
105
+
106
+ def __add__(self, other):
107
+ """Allows addition operator to append to sequence components list."""
108
+ if isinstance(other, _WrappedComponentBlock) or isinstance(other, _WrappedComponent):
109
+ self.component_objects__.append(other)
110
+ self.root.components.append(other.component_name__)
111
+ return self
112
+ return NotImplemented
113
+
114
+ def from_data(self, data_list: list):
115
+ return DataIterator(data_list, self)
116
+
117
+
118
+ class _WrappedStudyConfig(_JSONableBaseModel):
119
+ root: rvt_models.StudyConfig
120
+
121
+
122
+ class _StudyConfigType(rvt_models.StudyConfigType):
123
+ components: List[_WrappedComponent]
124
+
125
+
126
+ class DataIterator:
127
+ def __init__(self, data_list: List, parent_class: _WrappedComponentBlock):
128
+ self.data = data_list
129
+ self.parent_class = parent_class
130
+
131
+ def component(self, **kwargs):
132
+ for datum in self.data:
133
+ current_dict = {}
134
+ for key, value in kwargs.items():
135
+ if key == 'parameters':
136
+ param_dict = {}
137
+ for param_key, param_value in value.items():
138
+ if type(param_value) is str:
139
+ param_datum_value = _extract_datum_value(param_value)
140
+ if param_datum_value is not None:
141
+ param_dict[param_key] = getattr(datum, param_datum_value)
142
+ else:
143
+ param_dict[param_key] = value
144
+ else:
145
+ param_dict[param_key] = value
146
+ current_dict[key] = param_dict
147
+ else:
148
+ if type(value) is str:
149
+ datum_value = _extract_datum_value(value)
150
+ if datum_value is not None:
151
+ if key == 'component_name__':
152
+ current_dict[key] = str(getattr(datum, datum_value))
153
+ else:
154
+ current_dict[key] = getattr(datum, datum_value)
155
+ else:
156
+ current_dict[key] = value
157
+ else:
158
+ current_dict[key] = value
159
+ curr_component = component(**current_dict)
160
+ self.parent_class = self.parent_class + curr_component
161
+ # Return the parent class calling iterator when component is finished.
162
+ return self.parent_class
163
+
164
+
165
+ # # -----------------------------------
166
+ # # Factory Functions
167
+ # # -----------------------------------
168
+
169
+ # Component factory function
170
+ # Allows additional items to be sent over to our Component model while keeping restrictions
171
+ # for the model that is auto-generated.
172
+
173
+ @overload
174
+ def component(**kwargs: Unpack[rvt_models.MarkdownComponentType]) -> _WrappedComponent: ...
175
+ @overload
176
+ def component(**kwargs: Unpack[rvt_models.ReactComponentType]) -> _WrappedComponent: ...
177
+ @overload
178
+ def component(**kwargs: Unpack[rvt_models.ImageComponentType]) -> _WrappedComponent: ...
179
+ @overload
180
+ def component(**kwargs: Unpack[rvt_models.WebsiteComponentType]) -> _WrappedComponent: ...
181
+ @overload
182
+ def component(**kwargs: Unpack[rvt_models.QuestionnaireComponentType]) -> _WrappedComponent: ...
183
+ @overload
184
+ def component(**kwargs: Any) -> _WrappedComponent: ...
185
+
186
+
187
+ def component(**kwargs) -> _WrappedComponent:
188
+ # Inherit base
189
+ base_component = kwargs.get('base__', None)
190
+ if base_component:
191
+ base_fields = vars(base_component.root)
192
+ for key, value in base_fields.items():
193
+ if key not in kwargs:
194
+ kwargs[key] = value
195
+ # Get kwargs to pass to individual component
196
+ filter_kwargs = _get_filtered_kwargs(rvt_models.IndividualComponent, kwargs)
197
+ # Grab response list
198
+ response = filter_kwargs.get('response')
199
+
200
+ # Sets default response list
201
+ valid_response = []
202
+ # If response present
203
+ if response is not None:
204
+ for r in response:
205
+
206
+ # Prevent dict input
207
+ if isinstance(r, dict):
208
+ raise RevisitError(message='Cannot pass a dictionary directly into "Response" list.')
209
+
210
+ response_type_hint = get_type_hints(rvt_models.Response).get('root')
211
+ response_types = get_args(response_type_hint)
212
+
213
+ # If wrapped, get root
214
+ if isinstance(r, _WrappedResponse):
215
+ valid_response.append(r.root)
216
+
217
+ # If not wrapped but is valid response, append to list
218
+ elif r.__class__ in response_types:
219
+ valid_response.append(r)
220
+
221
+ # If other unknown type, raise error
222
+ else:
223
+ raise RevisitError(message=f'Invalid type {type(r)} for "Response" class.')
224
+
225
+ filter_kwargs['response'] = valid_response
226
+
227
+ # Validate component
228
+ _validate_component(filter_kwargs)
229
+ base_model = rvt_models.IndividualComponent(**filter_kwargs)
230
+
231
+ try:
232
+ return _WrappedComponent(**kwargs, root=base_model)
233
+ except ValidationError as e:
234
+ raise RevisitError(e.errors())
235
+
236
+
237
+ # Response factory function
238
+ @overload
239
+ def response(**kwargs: Unpack[rvt_models.NumericalResponseType]) -> _WrappedResponse: ...
240
+ @overload
241
+ def response(**kwargs: Unpack[rvt_models.ShortTextResponseType]) -> _WrappedResponse: ...
242
+ @overload
243
+ def response(**kwargs: Unpack[rvt_models.LongTextResponseType]) -> _WrappedResponse: ...
244
+ @overload
245
+ def response(**kwargs: Unpack[rvt_models.LikertResponseType]) -> _WrappedResponse: ...
246
+ @overload
247
+ def response(**kwargs: Unpack[rvt_models.DropdownResponseType]) -> _WrappedResponse: ...
248
+ @overload
249
+ def response(**kwargs: Unpack[rvt_models.SliderResponseType]) -> _WrappedResponse: ...
250
+ @overload
251
+ def response(**kwargs: Unpack[rvt_models.RadioResponseType]) -> _WrappedResponse: ...
252
+ @overload
253
+ def response(**kwargs: Unpack[rvt_models.CheckboxResponseType]) -> _WrappedResponse: ...
254
+ @overload
255
+ def response(**kwargs: Unpack[rvt_models.IFrameResponseType]) -> _WrappedResponse: ...
256
+ @overload
257
+ def response(**kwargs: Unpack[rvt_models.MatrixResponseType]) -> _WrappedResponse: ...
258
+ @overload
259
+ def response(**kwargs: Any) -> _WrappedResponse: ...
260
+
261
+
262
+ def response(**kwargs) -> _WrappedResponse:
263
+ filter_kwargs = _get_filtered_kwargs(rvt_models.Response, kwargs)
264
+ _validate_response(filter_kwargs)
265
+ base_model = rvt_models.Response(**filter_kwargs)
266
+ # We've validated the response for a particular type. Now, how do we validate the wrapped component correctly?
267
+ try:
268
+ return _WrappedResponse(**kwargs, root=base_model)
269
+ except ValidationError as e:
270
+ raise RevisitError(e.errors())
271
+
272
+
273
+ def studyMetadata(**kwargs: Unpack[rvt_models.StudyMetadataType]):
274
+ filter_kwargs = _get_filtered_kwargs(rvt_models.StudyMetadata, kwargs)
275
+ base_model = rvt_models.StudyMetadata(**filter_kwargs)
276
+ return _WrappedStudyMetadata(**kwargs, root=base_model)
277
+
278
+
279
+ def uiConfig(**kwargs: Unpack[rvt_models.UIConfigType]):
280
+ filter_kwargs = _get_filtered_kwargs(rvt_models.UIConfig, kwargs)
281
+ base_model = rvt_models.UIConfig(**filter_kwargs)
282
+ return _WrappedUIConfig(**kwargs, root=base_model)
283
+
284
+
285
+ def sequence(**kwargs: Unpack[rvt_models.ComponentBlockType]):
286
+ filter_kwargs = _get_filtered_kwargs(rvt_models.ComponentBlock, kwargs)
287
+ valid_component_names = []
288
+ valid_components = []
289
+ components = filter_kwargs.get('components')
290
+ if components is not None:
291
+ for c in components:
292
+
293
+ # Prevent dict input
294
+ if isinstance(c, dict):
295
+ raise RevisitError(message='Cannot pass a dictionary directly into "Component" list.')
296
+
297
+ # If wrapped, get root
298
+ if isinstance(c, _WrappedComponent):
299
+ valid_component_names.append(c.component_name__)
300
+ valid_components.append(c)
301
+
302
+ # If other unknown type, raise error
303
+ else:
304
+ raise RevisitError(message=f'Invalid type {type(c)} for "Component" class.')
305
+
306
+ filter_kwargs['components'] = valid_component_names
307
+ base_model = rvt_models.ComponentBlock(**filter_kwargs)
308
+ return _WrappedComponentBlock(**kwargs, root=base_model, component_objects__=valid_components)
309
+
310
+
311
+ @overload
312
+ def studyConfig(**kwargs: Unpack[_StudyConfigType]) -> _WrappedStudyConfig: ...
313
+ @overload
314
+ def studyConfig(**kwargs: Any) -> _WrappedStudyConfig: ...
315
+
316
+
317
+ def studyConfig(**kwargs: Unpack[_StudyConfigType]) -> _WrappedStudyConfig:
318
+ filter_kwargs = _get_filtered_kwargs(rvt_models.StudyConfig, kwargs)
319
+
320
+ root_list = ['studyMetadata', 'uiConfig', 'sequence']
321
+ un_rooted_kwargs = {x: (y.root if x in root_list and hasattr(y, 'root') else y) for x, y in filter_kwargs.items()}
322
+
323
+ study_sequence = filter_kwargs['sequence']
324
+
325
+ # Merges components from the components list given and the components that are stored in the sequence
326
+ un_rooted_kwargs['components'] = {
327
+ comp.component_name__: comp.root for comp in un_rooted_kwargs.get('components', [])
328
+ } | {
329
+ comp.component_name__: comp.root for comp in study_sequence.component_objects__
330
+ }
331
+
332
+ base_model = rvt_models.StudyConfig(**un_rooted_kwargs)
333
+ return _WrappedStudyConfig(**kwargs, root=base_model)
334
+
335
+
336
+ # Function to parse the CSV and dynamically create data classes
337
+ def data(file_path: str) -> List[Any]:
338
+ # Read the first row to get the headers
339
+ with open(file_path, mode='r') as csvfile:
340
+ csv_reader = csv.DictReader(csvfile)
341
+ headers = csv_reader.fieldnames
342
+ if not headers:
343
+ raise RevisitError(message="No headers found in CSV file.")
344
+
345
+ # Create a data class with attributes based on the headers
346
+ DataRow = make_dataclass("DataRow", [(header, Any) for header in headers])
347
+
348
+ # Parse each row into an instance of the dynamically created data class
349
+ data_rows = []
350
+ for row in csv_reader:
351
+ # Convert the row values to the appropriate types (e.g., int, float, bool)
352
+ data = {key: _convert_value(value) for key, value in row.items()}
353
+ data_row = DataRow(**data)
354
+ data_rows.append(data_row)
355
+
356
+ return data_rows
357
+
358
+
359
+ # ------- PRIVATE FUNCTIONS ------------ #
360
+
361
+ def _validate_component(kwargs: dict):
362
+ component_mapping = _generate_possible_component_types()[1]
363
+ if 'type' not in kwargs:
364
+ raise RevisitError(message='"Type" is required on Component.')
365
+ elif component_mapping.get(kwargs['type']) is None:
366
+ raise RevisitError(message=f'Unexpected component type: {kwargs['type']}')
367
+
368
+ try:
369
+ return rvt_models.IndividualComponent.model_validate(kwargs).root
370
+ except ValidationError as e:
371
+ temp_errors = []
372
+
373
+ for entry in e.errors():
374
+ if entry['loc'][0] == component_mapping[kwargs['type']]:
375
+ temp_errors.append(entry)
376
+
377
+ if len(temp_errors) > 0:
378
+ raise RevisitError(temp_errors)
379
+ else:
380
+ raise RevisitError(
381
+ message='Unexpected error occurred during Component instantiation.'
382
+ )
383
+
384
+
385
+ # Call validate response when creating response component.
386
+ def _validate_response(kwargs: dict):
387
+ response_mapping = _generate_possible_response_types()[1]
388
+ if 'type' not in kwargs:
389
+ raise RevisitError(message='"Type" is required on Response.')
390
+ else:
391
+
392
+ type_value = kwargs.get('type')
393
+
394
+ # Handles enum class type
395
+ if isinstance(kwargs.get('type'), Enum):
396
+ type_value = type_value.value
397
+
398
+ if response_mapping.get(type_value) is None:
399
+ raise RevisitError(message=f'Unexpected type: {type_value}')
400
+
401
+ try:
402
+ return rvt_models.Response.model_validate(kwargs).root
403
+ except ValidationError as e:
404
+ temp_errors = []
405
+ for entry in e.errors():
406
+ if entry['loc'][0] == response_mapping[type_value]:
407
+ temp_errors.append(entry)
408
+
409
+ if len(temp_errors) > 0:
410
+ raise RevisitError(temp_errors)
411
+ else:
412
+ raise RevisitError(
413
+ message='Unexpected error occurred during Response instantiation'
414
+ )
415
+
416
+
417
+ def _generate_possible_response_types():
418
+ return _generate_possible_types(rvt_models.Response)
419
+
420
+
421
+ def _generate_possible_component_types():
422
+ return _generate_possible_types(rvt_models.IndividualComponent)
423
+
424
+
425
+ # Generates mappings between the response class name and the
426
+ # type string literal. Creates the reversed mapping as well.
427
+ def _generate_possible_types(orig_cls):
428
+ response_type_hint = get_type_hints(orig_cls).get('root')
429
+ response_types = get_args(response_type_hint)
430
+ type_hints = {}
431
+ type_hints_reversed = {}
432
+ for cls in response_types:
433
+ curr_type = get_type_hints(cls).get('type')
434
+ curr_origin = get_origin(get_type_hints(cls).get('type'))
435
+ if curr_origin is Literal:
436
+ type_hints[cls.__name__] = set([get_args(curr_type)[0]])
437
+ type_hints_reversed[get_args(curr_type)[0]] = cls.__name__
438
+ elif isinstance(curr_type, type) and issubclass(curr_type, Enum):
439
+ enum_list = [member.value for member in curr_type]
440
+ type_hints[cls.__name__] = set(enum_list)
441
+ for item in enum_list:
442
+ type_hints_reversed[item] = cls.__name__
443
+
444
+ return (type_hints, type_hints_reversed)
445
+
446
+
447
+ # Custom exception
448
+ class RevisitError(Exception):
449
+ def __init__(self, errors=None, message=None):
450
+ # Case 1: Validation Errors From Pydantic
451
+ # Case 2: Standard Error Message
452
+ super().__init__('There was an error.')
453
+ if message is None:
454
+ pretty_message_list = pretty_error(errors)
455
+ self.message = \
456
+ f'There was an error. \n' \
457
+ f'----------------------------------------------------' \
458
+ f'\n\n' \
459
+ f'{'\n\n'.join(pretty_message_list)}' \
460
+ f'\n'
461
+ else:
462
+ self.message = \
463
+ f'There was an error. \n' \
464
+ f'----------------------------------------------------' \
465
+ f'\n\n' \
466
+ f'{message}' \
467
+ f'\n'
468
+
469
+ def __str__(self):
470
+ return self.message
471
+
472
+
473
+ def pretty_error(errors):
474
+ custom_messages = {
475
+ 'missing': 'Field is missing'
476
+ }
477
+ new_error_messages = []
478
+ for error in errors:
479
+ custom_message = custom_messages.get(error['type'])
480
+ if custom_message:
481
+ new_error_messages.append(f'Location: {error['loc']}\nError: Field "{error['loc'][-1]}" is required.')
482
+ else:
483
+ new_error_messages.append(f'Location: {error['loc']}\nError: {error['msg']}')
484
+ return new_error_messages
485
+
486
+
487
+ def _get_filtered_kwargs(class_type: Any, kwargs):
488
+ try:
489
+ possible_items = get_args(class_type.__fields__.get('root').annotation)
490
+ except AttributeError:
491
+ possible_items = [class_type]
492
+
493
+ valid_fields = set()
494
+ for model in possible_items:
495
+ valid_fields.update(model.model_fields.keys())
496
+
497
+ return {key: value for key, value in kwargs.items() if key in valid_fields}
498
+
499
+
500
+ def _convert_value(value: str) -> Any:
501
+ """Helper function to convert string values to appropriate data types."""
502
+ value = value.strip()
503
+ if value.lower() == "true":
504
+ return True
505
+ elif value.lower() == "false":
506
+ return False
507
+ try:
508
+ if '.' in value:
509
+ return float(value)
510
+ else:
511
+ return int(value)
512
+ except ValueError:
513
+ return value # Return as string if it cannot be converted
514
+
515
+
516
+ def _extract_datum_value(text: str) -> str:
517
+ # Use regex to match 'datum:thing' and capture 'thing'
518
+ match = re.match(r'^datum:(\w+)$', text)
519
+ if match:
520
+ return match.group(1) # Return the captured part (i.e., 'thing')
521
+ return None # Return None if the pattern doesn't match