class SetTool(Tool): selection = T.Instance(Selection, allow_none=True) active = TypedTuple(T.Instance(BaseElement), kw={}) css_classes = TypedTuple(T.Unicode()) @T.default("css_classes") def _default_css_classes(self): return tuple([ "active-set", ]) @T.observe("active") def handler(self, change): try: new = set(change.new) except Exception: new = set() try: old = set(change.old) except Exception: old = set() exiting, entering = lifecycle(old, new) for el in entering: el.add_class(*self.css_classes) for el in exiting: el.remove_class(*self.css_classes) def add(self): self.active = tuple(set(self.active) | set(self.selection.elements())) def remove(self): self.active = tuple(set(self.active) - set(self.selection.elements())) def set_active(self): self.active = tuple(set(self.selection.elements())) @T.default("ui") def _default_ui(self): add_btn = W.Button(description="", icon="plus", layout={"width": "2.6em"}) remove_btn = W.Button(description="", icon="minus", layout={"width": "2.6em"}) set_btn = W.Button(description="", icon="circle", layout={"width": "2.6em"}) add_btn.on_click(lambda *_: self.add()) remove_btn.on_click(lambda *_: self.remove()) set_btn.on_click(lambda *_: self.set_active()) return W.HBox([add_btn, set_btn, remove_btn])
class Pipe(W.Widget): disposition = T.Instance(PipeDisposition) enabled: bool = T.Bool(default_value=True) inlet: MarkElementWidget = T.Instance(MarkElementWidget, kw={}) outlet: MarkElementWidget = T.Instance(MarkElementWidget, kw={}) dirty: bool = T.Bool(default_value=True) observes: Tuple[str] = TypedTuple(T.Unicode(), kw={}) reports: Tuple[str] = TypedTuple(T.Unicode(), kw={}) _task: asyncio.Future = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def schedule_run(self, change: T.Bunch = None) -> asyncio.Task: # schedule task on loop if self._task: self._task.cancel() self._task = asyncio.create_task(self.run()) self._task.add_done_callback(self._post_run) return self._task def _post_run(self, future: asyncio.Future): try: future.exception() except asyncio.CancelledError: pass except Exception as E: raise E async def run(self): # do work self.outlet.value = self.inlet.value def check_dirty(self) -> bool: flow = self.inlet.flow if any( any(re.match(f"^{obs}$", f) for f in flow) for obs in self.observes): # mark this pipe as dirty so will run self.dirty = True self.disposition = PipeDisposition.waiting # add this pipes reporting to the outlet flow flow = tuple(set([*flow, *self.reports])) else: self.dirty = False self.disposition = PipeDisposition.done self.outlet.flow = flow return self.dirty
class MarkElementWidget(W.DOMWidget): value: Node = T.Instance(Node, allow_none=True).tag(sync=True, **elk_serialization) index: MarkIndex = T.Instance(MarkIndex, kw={}).tag(sync=True, **W.widget_serialization) flow: Tuple[str] = TypedTuple(T.Unicode(), kw={}).tag(sync=True) def persist(self): if self.index.elements is None: self.build_index() else: self.index.elements.update(ElementIndex.from_els(self.value)) return self def build_index(self) -> MarkIndex: if self.value is None: index = ElementIndex() else: with self.index.context: index = ElementIndex.from_els(self.value) self.index.elements = index return self.index def _ipython_display_(self, **kwargs): from IPython.display import JSON, display display(JSON(self.value.dict()))
class Viewer(W.Widget): source: MarkElementWidget = T.Instance( MarkElementWidget, allow_none=True).tag(sync=True, **W.widget_serialization) selection: Selection = T.Instance(Selection, kw={}).tag(sync=True, **W.widget_serialization) hover: Hover = T.Instance(Hover, kw={}).tag(sync=True, **W.widget_serialization) zoom = T.Instance(Zoom, kw={}).tag(sync=True, **W.widget_serialization) pan = T.Instance(Pan, kw={}).tag(sync=True, **W.widget_serialization) viewed: Tuple[str] = TypedTuple(trait=T.Unicode()).tag( sync=True) # list element ids in the current view bounding box fit_tool: FitTool = T.Instance(FitTool) center_tool: CenterTool = T.Instance(CenterTool) @T.default("fit_tool") def _default_fit_tool(self) -> FitTool: return FitTool(handler=lambda _: self.fit()) @T.default("center_tool") def _default_center_tool(self) -> CenterTool: return CenterTool(handler=lambda _: self.center()) def fit(self): pass def center(self): pass
class Menu(ReactWidget): _model_name = Unicode('MenuModel').tag(sync=True) description = Unicode(help="Menu item").tag(sync=True) items = TypedTuple(trait=Instance(MenuItem), help="Menu items", default=[], allow_none=True).tag( sync=True, **widget_serialization).tag(sync=True)
class ToggleButtons(_Selection): """Group of toggle buttons that represent an enumeration. Only one toggle button can be toggled at any point in time. Parameters ---------- {selection_params} tooltips: list Tooltip for each button. If specified, must be the same length as `options`. icons: list Icons to show on the buttons. This must be the name of a font-awesome icon. See `http://fontawesome.io/icons/` for a list of icons. button_style: str One of 'primary', 'success', 'info', 'warning' or 'danger'. Applies a predefined style to every button. style: ToggleButtonsStyle Style parameters for the buttons. """ _view_name = Unicode('ToggleButtonsView').tag(sync=True) _model_name = Unicode('ToggleButtonsModel').tag(sync=True) tooltips = TypedTuple(Unicode(), help="Tooltips for each button.").tag(sync=True) icons = TypedTuple( Unicode(), help= "Icons names for each button (FontAwesome names without the fa- prefix)." ).tag(sync=True) style = InstanceDict(ToggleButtonsStyle).tag(sync=True, **widget_serialization) button_style = CaselessStrEnum( values=['primary', 'success', 'info', 'warning', 'danger', ''], default_value='', allow_none=True, help="""Use a predefined styling for the buttons.""").tag(sync=True)
class DatWidget(Widget): """TODO: Add docstring here """ _model_name = Unicode('DatModel').tag(sync=True) _model_module = Unicode(module_name).tag(sync=True) _model_module_version = Unicode(module_version).tag(sync=True) targets = TypedTuple(WidgetTraitTuple(), allow_none=True).tag(sync=True) view = Instance(DOMWidget).tag(sync=True)
class ElkJS(SyncedPipe): """Jupyterlab widget for calling `elkjs <https://github.com/kieler/elkjs>`_ layout given a valid elkjson dictionary""" _model_name = T.Unicode("ELKLayoutModel").tag(sync=True) _model_module = T.Unicode(EXTENSION_NAME).tag(sync=True) _model_module_version = T.Unicode(EXTENSION_SPEC_VERSION).tag(sync=True) _view_module = T.Unicode(EXTENSION_NAME).tag(sync=True) observes = TypedTuple(T.Unicode(), default_value=(F.Anythinglayout, )) reports = TypedTuple(T.Unicode(), default_value=(F.Layout, )) async def run(self): # watch once if self.outlet is None: return # signal to browser and wait for done future_value = wait_for_change(self.outlet, "value") self.send({"action": "run"}) # wait to return until await future_value self.outlet.persist()
class Selection(Tool): ids = TypedTuple(trait=T.Unicode()).tag( sync=True) # list element ids currently selected def get_index(self) -> MarkIndex: if self.tee is None: raise ValueError("Tool not attached to a pipe") if self.tee.inlet.index is None: # make the element index self.tee.inlet.build_index() return self.tee.inlet.index def elements(self) -> Iterator[BaseElement]: index = self.get_index() for el in map(index.from_id, self.ids): yield el
class ReactWidget(widgets.DOMWidget, ClickMixin): """An example widget.""" _view_name = Unicode('ReactView').tag(sync=True) _model_name = Unicode('ReactModel').tag(sync=True) _view_module = Unicode('ipyantd').tag(sync=True) _model_module = Unicode('ipyantd').tag(sync=True) _view_module_version = Unicode('^0.1.0').tag(sync=True) _model_module_version = Unicode('^0.1.0').tag(sync=True) child = Instance('ipywidgets.widgets.domwidget.DOMWidget', default_value=None, allow_none=True)\ .tag(sync=True, **widget_serialization).tag(sync=True) children = TypedTuple(trait=Instance('ipywidgets.widgets.domwidget.DOMWidget'), help="children", default=[], allow_none=True)\ .tag(sync=True, **widget_serialization).tag(sync=True) visible = CBool(True).tag(sync=True) content = Unicode(help="").tag(sync=True) style = Dict().tag(sync=True) class_name = Unicode('', help="class_name").tag(sync=True)
class BufferGeometry(BaseBufferGeometry): """BufferGeometry Autogenerated by generate-wrappers.js See https://threejs.org/docs/#api/core/BufferGeometry """ def __init__(self, **kwargs): super(BufferGeometry, self).__init__(**kwargs) _model_name = Unicode('BufferGeometryModel').tag(sync=True) index = Union([ Instance(BufferAttribute, allow_none=True), Instance(InterleavedBufferAttribute, allow_none=True) ]).tag(sync=True, **widget_serialization) attributes = Dict( Union([ Instance(BufferAttribute), Instance(InterleavedBufferAttribute) ])).tag(sync=True, **widget_serialization) morphAttributes = Dict( TypedTuple( Union([ Instance(BufferAttribute), Instance(InterleavedBufferAttribute) ]))).tag(sync=True, **widget_serialization) userData = Dict(default_value={}, allow_none=False).tag(sync=True) MaxIndex = CInt(65535, allow_none=False).tag(sync=True) _ref_geometry = Union([ Instance(BaseGeometry, allow_none=True), Instance(BaseBufferGeometry, allow_none=True) ]).tag(sync=True, **widget_serialization) _store_ref = Bool(False, allow_none=False).tag(sync=True) type = Unicode("BufferGeometry", allow_none=False).tag(sync=True)
class VisibilityPipe(Pipe): observes = TypedTuple( T.Unicode(), default_value=( F.AnyHidden, F.Layout, ), ) @T.default("reports") def _default_reports(self): return (F.Layout, ) async def run(self): if self.outlet is None or self.inlet is None: return root = self.inlet.index.root # generate an index of hidden elements vis_index = VisIndex.from_els(root) # clear old slack css classes from elements vis_index.clear_slack(root) # serialize the elements excluding hidden with exclude_hidden, exclude_layout: data = root.dict() # new root node with slack edges / ports introduced due to hidden # elements with Registry(): value = convert_elkjson(data, vis_index) for el in index.iter_elements(value): el.id = el.get_id() self.outlet.value = value return self.outlet
class Tool(W.Widget): tee: Pipe = T.Instance(Pipe, allow_none=True).tag(sync=True, **W.widget_serialization) on_done = T.Any(allow_none=True) # callback when done disable = T.Bool(default_value=False).tag(sync=True, **W.widget_serialization) reports = TypedTuple(T.Unicode(), kw={}) _task: asyncio.Future = None ui = T.Instance(W.DOMWidget, allow_none=True) priority = T.Int(default_value=10) def handler(self, *args): """Handler callback for running the tool""" # canel old work if needed if self._task: self._task.cancel() # schedule work self._task = asyncio.create_task(self.run()) # callback self._task.add_done_callback(self._finished) if self.tee: self.tee.inlet.flow = self.reports async def run(self): raise NotImplementedError() def _finished(self, future: asyncio.Future): try: future.result() if callable(self.on_done): self.on_done() except asyncio.CancelledError: pass # cancellation should not log an error except Exception: self.log.exception(f"Error running tool: {type(self)}")
class Geometry(BaseGeometry): """Geometry Autogenerated by generate-wrappers.js See https://threejs.org/docs/#api/core/Geometry """ def __init__(self, **kwargs): super(Geometry, self).__init__(**kwargs) _model_name = Unicode('GeometryModel').tag(sync=True) vertices = List(trait=List()).tag(sync=True) colors = List(trait=Unicode(), default_value=["#ffffff"]).tag(sync=True) faces = TypedTuple(trait=Face3()).tag(sync=True) faceVertexUvs = List().tag(sync=True) lineDistances = List().tag(sync=True) morphTargets = List().tag(sync=True) morphNormals = List().tag(sync=True) skinWeights = List(trait=List()).tag(sync=True) skinIndices = List(trait=List()).tag(sync=True) _ref_geometry = Instance(BaseGeometry, allow_none=True).tag(sync=True, **widget_serialization) _store_ref = Bool(False, allow_none=False).tag(sync=True) type = Unicode("Geometry", allow_none=False).tag(sync=True)
class _Selection(DescriptionWidget, ValueWidget, CoreWidget): """Base class for Selection widgets ``options`` can be specified as a list of values, list of (label, value) tuples, or a dict of {label: value}. The labels are the strings that will be displayed in the UI, representing the actual Python choices, and should be unique. If labels are not specified, they are generated from the values. When programmatically setting the value, a reverse lookup is performed among the options to check that the value is valid. The reverse lookup uses the equality operator by default, but another predicate may be provided via the ``equals`` keyword argument. For example, when dealing with numpy arrays, one may set equals=np.array_equal. """ value = Any(None, help="Selected value", allow_none=True) label = Unicode(None, help="Selected label", allow_none=True) index = Int(None, help="Selected index", allow_none=True).tag(sync=True) options = Any( (), help= """Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select. The labels are the strings that will be displayed in the UI, representing the actual Python choices, and should be unique. """) _options_full = None # This being read-only means that it cannot be changed by the user. _options_labels = TypedTuple( trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True) disabled = Bool(help="Enable or disable user changes").tag(sync=True) def __init__(self, *args, **kwargs): self.equals = kwargs.pop('equals', lambda x, y: x == y) # We have to make the basic options bookkeeping consistent # so we don't have errors the first time validators run self._initializing_traits_ = True options = _make_options(kwargs.get('options', ())) self._options_full = options self.set_trait('_options_labels', tuple(i[0] for i in options)) self._options_values = tuple(i[1] for i in options) # Select the first item by default, if we can if 'index' not in kwargs and 'value' not in kwargs and 'label' not in kwargs: nonempty = (len(options) > 0) kwargs['index'] = 0 if nonempty else None kwargs['label'], kwargs['value'] = options[0] if nonempty else ( None, None) super(_Selection, self).__init__(*args, **kwargs) self._initializing_traits_ = False @validate('options') def _validate_options(self, proposal): # if an iterator is provided, exhaust it if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping): proposal.value = tuple(proposal.value) # throws an error if there is a problem converting to full form self._options_full = _make_options(proposal.value) return proposal.value @observe('options') def _propagate_options(self, change): "Set the values and labels, and select the first option if we aren't initializing" options = self._options_full self.set_trait('_options_labels', tuple(i[0] for i in options)) self._options_values = tuple(i[1] for i in options) if self._initializing_traits_ is not True: if len(options) > 0: if self.index == 0: # Explicitly trigger the observers to pick up the new value and # label. Just setting the value would not trigger the observers # since traitlets thinks the value hasn't changed. self._notify_trait('index', 0, 0) else: self.index = 0 else: self.index = None @validate('index') def _validate_index(self, proposal): if proposal.value is None or 0 <= proposal.value < len( self._options_labels): return proposal.value else: raise TraitError('Invalid selection: index out of bounds') @observe('index') def _propagate_index(self, change): "Propagate changes in index to the value and label properties" label = self._options_labels[ change.new] if change.new is not None else None value = self._options_values[ change.new] if change.new is not None else None if self.label is not label: self.label = label if self.value is not value: self.value = value @validate('value') def _validate_value(self, proposal): value = proposal.value try: return findvalue(self._options_values, value, self.equals) if value is not None else None except ValueError: raise TraitError('Invalid selection: value not found') @observe('value') def _propagate_value(self, change): if change.new is None: index = None elif self.index is not None and self._options_values[ self.index] == change.new: index = self.index else: index = self._options_values.index(change.new) if self.index != index: self.index = index @validate('label') def _validate_label(self, proposal): if (proposal.value is not None) and (proposal.value not in self._options_labels): raise TraitError('Invalid selection: label not found') return proposal.value @observe('label') def _propagate_label(self, change): if change.new is None: index = None elif self.index is not None and self._options_labels[ self.index] == change.new: index = self.index else: index = self._options_labels.index(change.new) if self.index != index: self.index = index def _repr_keys(self): keys = super(_Selection, self)._repr_keys() # Include options manually, as it isn't marked as synced: for key in sorted(chain(keys, ('options', ))): if key == 'index' and self.index == 0: # Index 0 is default when there are options continue yield key
class TestCase(HasTraits): value = TypedTuple((1, 2, 3))
class TestCase(HasTraits): value = TypedTuple(Int())
class TestCase(HasTraits): value = TypedTuple(trait=Int(), default_value=(1, 2, 'foobar'))
class TestCase(HasTraits): value = TypedTuple(default_value=(1, 2, 3))
class Pipe(W.Widget): enabled: bool = T.Bool(default_value=True) inlet: MarkElementWidget = T.Instance(MarkElementWidget, kw={}) outlet: MarkElementWidget = T.Instance(MarkElementWidget, kw={}) observes: Tuple[str] = TypedTuple(T.Unicode(), kw={}) reports: Tuple[str] = TypedTuple(T.Unicode(), kw={}) on_progress: Optional[Callable] = T.Any(allow_none=True) _task: asyncio.Future = None status: PipeStatus = T.Instance(PipeStatus, kw={}) status_widget: W.DOMWidget = T.Instance(W.DOMWidget, allow_none=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @T.default("status_widget") def _default_status_widget(self): widget = PipeStatusView() def update(change=None): widget.update(self) update() self.observe(update, "status") return widget def _ipython_display_(self, **kwargs): if self.status_widget is None: raise NotImplementedError() return self.status_widget._ipython_display_(**kwargs) def schedule_run(self, change: T.Bunch = None) -> asyncio.Task: # schedule task on loop if self._task: self._task.cancel() self._task = asyncio.create_task(self.run()) self._task.add_done_callback(self._post_run) return self._task def _post_run(self, future: asyncio.Future): try: future.exception() except asyncio.CancelledError: pass except Exception as E: raise E async def run(self): # do work self.outlet.value = self.inlet.value def check_dirty(self) -> bool: flow = self.inlet.flow if any( any(re.match(f"^{obs}$", f) for f in flow) for obs in self.observes): # mark this pipe as dirty so will run self.status = PipeStatus.waiting() # add this pipes reporting to the outlet flow flow = tuple(set([*flow, *self.reports])) else: self.status = PipeStatus.finished() self.outlet.flow = flow return self.status.dirty() def status_update( self, status: PipeStatus, pipe: Optional["Pipe"] = None, ): if isinstance(pipe, Pipe): pipe.status_update(status=status) self.status = status if callable(self.on_progress): self.on_progress(self) def get_progress_value(self) -> float: return self.status.step() def error(self): if self.status.exception: raise self.status.exception
class ValidationPipe(Pipe): observes = TypedTuple(T.Unicode(), default_value=(F.New, )) reports = TypedTuple(T.Unicode(), default_value=(F.Layout, )) fix_null_id = T.Bool(default_value=True) fix_edge_owners = T.Bool(default_value=True) fix_orphans = T.Bool(default_value=True) id_report = T.Instance(IDReport, kw={}) edge_report = T.Instance(EdgeReport, kw={}) schema_report = T.Dict(kw={}) errors = T.Dict(kw={}) async def run(self): index: MarkIndex = self.inlet.build_index() with index.context: self.get_reports(index) self.errors = self.collect_errors() if self.errors: raise ValueError("Inlet value is not valid") self.apply_fixes(index) value = self.inlet.index.root if value is self.outlet.value: # force refresh if same instance self.outlet._notify_trait("value", None, value) else: self.outlet.value = value self.get_reports(self.outlet.build_index()) self.errors = self.collect_errors() if self.errors: raise ValueError("Outlet value is not valid") def get_reports(self, index: MarkIndex): self.edge_report = index.elements.check_edges() self.id_report = index.elements.check_ids(*self.edge_report.orphans) self.schema_report = {} # TODO run elkjson schema validator def collect_errors(self) -> Dict: errors = {} if self.id_report.duplicated: errors["Nonunique Element Ids"] = self.id_report.duplicated if self.id_report.null_ids and not self.fix_null_id: errors["Null Id Elements"] = self.id_report.null_ids if self.edge_report.orphans and not self.fix_orphans: errors["Orphan Nodes"] = self.edge_report.orphans if self.edge_report.lca_mismatch and not self.fix_edge_owners: errors[ "Lowest Common Ancestor Mismatch"] = self.edge_report.lca_mismatch if self.schema_report: errors["Schema Error"] = self.schema_report return errors def apply_fixes(self, index: MarkIndex): root = index.root if self.id_report.null_ids and self.fix_null_id: self.log.warning(f"fixing {len(self.id_report.null_ids)} ids") for el in self.id_report.null_ids: el.id = el.get_id() if self.edge_report.orphans and self.fix_orphans: for el in self.edge_report.orphans: root.add_child(el) if self.edge_report.lca_mismatch and self.fix_edge_owners: for edge, (old, new) in self.edge_report.lca_mismatch.items(): old.edges.remove(edge) if new is None: new = root new.edges.append(edge)
class _MultipleSelection(DescriptionWidget, ValueWidget, CoreWidget): """Base class for multiple Selection widgets ``options`` can be specified as a list of values, list of (label, value) tuples, or a dict of {label: value}. The labels are the strings that will be displayed in the UI, representing the actual Python choices, and should be unique. If labels are not specified, they are generated from the values. When programmatically setting the value, a reverse lookup is performed among the options to check that the value is valid. The reverse lookup uses the equality operator by default, but another predicate may be provided via the ``equals`` keyword argument. For example, when dealing with numpy arrays, one may set equals=np.array_equal. """ value = TypedTuple(trait=Any(), help="Selected values") label = TypedTuple(trait=Unicode(), help="Selected labels") index = TypedTuple(trait=Int(), help="Selected indices").tag(sync=True) options = Any( (), help= """Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select. The labels are the strings that will be displayed in the UI, representing the actual Python choices, and should be unique. """) _options_full = None # This being read-only means that it cannot be changed from the frontend! _options_labels = TypedTuple( trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True) disabled = Bool(help="Enable or disable user changes").tag(sync=True) def __init__(self, *args, **kwargs): self.equals = kwargs.pop('equals', lambda x, y: x == y) # We have to make the basic options bookkeeping consistent # so we don't have errors the first time validators run self._initializing_traits_ = True options = _make_options(kwargs.get('options', ())) self._full_options = options self.set_trait('_options_labels', tuple(i[0] for i in options)) self._options_values = tuple(i[1] for i in options) super(_MultipleSelection, self).__init__(*args, **kwargs) self._initializing_traits_ = False @validate('options') def _validate_options(self, proposal): if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping): proposal.value = tuple(proposal.value) # throws an error if there is a problem converting to full form self._options_full = _make_options(proposal.value) return proposal.value @observe('options') def _propagate_options(self, change): "Unselect any option" options = self._options_full self.set_trait('_options_labels', tuple(i[0] for i in options)) self._options_values = tuple(i[1] for i in options) if self._initializing_traits_ is not True: self.index = () @validate('index') def _validate_index(self, proposal): "Check the range of each proposed index." if all(0 <= i < len(self._options_labels) for i in proposal.value): return proposal.value else: raise TraitError('Invalid selection: index out of bounds') @observe('index') def _propagate_index(self, change): "Propagate changes in index to the value and label properties" label = tuple(self._options_labels[i] for i in change.new) value = tuple(self._options_values[i] for i in change.new) # we check equality so we can avoid validation if possible if self.label != label: self.label = label if self.value != value: self.value = value @validate('value') def _validate_value(self, proposal): "Replace all values with the actual objects in the options list" try: return tuple( findvalue(self._options_values, i, self.equals) for i in proposal.value) except ValueError: raise TraitError('Invalid selection: value not found') @observe('value') def _propagate_value(self, change): index = tuple(self._options_values.index(i) for i in change.new) if self.index != index: self.index = index @validate('label') def _validate_label(self, proposal): if any(i not in self._options_labels for i in proposal.value): raise TraitError('Invalid selection: label not found') return proposal.value @observe('label') def _propagate_label(self, change): index = tuple(self._options_labels.index(i) for i in change.new) if self.index != index: self.index = index def _repr_keys(self): keys = super(_MultipleSelection, self)._repr_keys() # Include options manually, as it isn't marked as synced: for key in sorted(chain(keys, ('options', ))): yield key
class SysML2Client(trt.HasTraits): """ A traitleted SysML v2 API Client. ..todo: - Add ability to use element download pagination. """ model: Model = trt.Instance(Model, allow_none=True) host_url = trt.Unicode(default_value="http://localhost", ) host_port = trt.Integer( default_value=9000, min=1, max=65535, ) page_size = trt.Integer( default_value=5000, min=1, ) paginate = trt.Bool(default_value=True) folder_path: Path = trt.Instance(Path, allow_none=True) json_files: Tuple[Path] = TypedTuple(trt.Instance(Path)) json_file: Path = trt.Instance(Path, allow_none=True) selected_project: str = trt.Unicode(allow_none=True) selected_commit: str = trt.Unicode(allow_none=True) projects = trt.Dict() name_hints = trt.Dict() log_out: ipyw.Output = trt.Instance(ipyw.Output, args=()) _next_url_regex = re.compile(r'<(http://.*)>; rel="next"') @trt.default("projects") def _make_projects(self): def process_project_safely(project) -> dict: # protect against projects that can't be parsed try: name_with_date = project["name"] name = " ".join(name_with_date.split()[:-6]) timestamp = " ".join(name_with_date.split()[-6:]) created = self._parse_timestamp(timestamp) except ValueError: # TODO: revise this when the API server changes the project name return dict() return dict( created=created, full_name=name_with_date, name=name, ) try: projects = self._retrieve_data(self.projects_url) except Exception as exc: # pylint: disable=broad-except warn( f"Could not retrieve projects from {self.projects_url}.\n{exc}" ) return {} results = { project["@id"]: process_project_safely(project) for project in projects } return { project_id: project_data for project_id, project_data in results.items() if project_data } @trt.observe("host_url", "host_port") def _update_api_configuration(self, *_): self.projects = self._make_projects() @trt.observe("selected_commit") def _update_elements(self, *_, elements=None): if not (self.selected_commit or elements): return elements = elements or [] self.model = Model.load( elements=elements, name=f"""{ self.projects[self.selected_project]["name"] } ({self.host})""", source=self.elements_url, ) for element in self.model.elements.values(): if "label" not in element._derived: element._derived["label"] = get_label(element) @trt.observe("folder_path") def _update_json_files(self, *_): if self.folder_path.exists(): self.json_files = tuple(self.folder_path.glob("*.json")) @trt.observe("json_file") def _update_elements_from_file(self, change: trt.Bunch = None): if change is None: return if change.new != change.old and change.new.exists(): self.model = Model.load_from_file(self.json_file) @property def host(self): return f"{self.host_url}:{self.host_port}" @property def projects_url(self): return f"{self.host}/projects" @property def commits_url(self): return f"{self.projects_url}/{self.selected_project}/commits" @property def elements_url(self): if not self.paginate: warn("By default, disabling pagination still retrieves 100 " "records at a time! True pagination is not supported yet.") if not self.selected_project: raise SystemError("No selected project!") if not self.selected_commit: raise SystemError("No selected commit!") arguments = f"?page[size]={self.page_size}" if self.page_size else "" return f"{self.commits_url}/{self.selected_commit}/elements{arguments}" @lru_cache def _retrieve_data(self, url: str) -> List[Dict]: """Retrieve model data from a URL using pagination""" result = [] while url: response = requests.get(url) if not response.ok: raise requests.HTTPError( f"Failed to retrieve elements from '{url}', reason: {response.reason}" ) result += response.json() link = response.headers.get("Link") if not link: break urls = self._next_url_regex.findall(link) url = None if len(urls) == 1: url = urls[0] elif len(urls) > 1: raise SystemError( f"Found multiple 'next' pagination urls: {urls}") return result @staticmethod def _parse_timestamp(timestamp: str) -> datetime: if isinstance(timestamp, datetime): return timestamp return parser.parse(timestamp, tzinfos=TIMEZONES).astimezone(timezone.utc) def _get_project_commits(self): def clean_fields(data: dict) -> dict: for key, value in tuple(data.items()): if not isinstance(key, str): continue if key == "timestamp": data[key] = self._parse_timestamp(value) return data commits = sorted(self._retrieve_data(self.commits_url), key=lambda x: x["timestamp"]) return {commit["@id"]: clean_fields(commit) for commit in commits} def _download_elements(self): elements = self._retrieve_data(self.elements_url) max_elements = self.page_size if self.paginate else 100 if len(elements) == max_elements: warn("There are probably more elements that were not retrieved!") self._update_elements(elements=elements) def _load_from_file(self, file_path: Union[str, Path]): self.model = Model.load_from_file(file_path)