PKٸLM3_aabowtie/__init__.py"""Interactive Dashboard Toolkit.""" __version__ = '0.11.0' from bowtie._app import App, View from bowtie._command import command from bowtie.pager import Pager from bowtie._cache import cache def load_ipython_extension(ipython): """Enable IPython extension.""" from bowtie._magic import BowtieMagic ipython.register_magics(BowtieMagic) PK4LM ''bowtie/_app.py"""Defines the App class.""" from typing import ( # pylint: disable=unused-import Any, Callable, Generator, List, Optional, Set, Tuple, Union, Dict, Sequence ) import os import json import itertools import shutil from collections import namedtuple, defaultdict from subprocess import Popen, PIPE, STDOUT, check_output from pathlib import Path import secrets import socket import warnings import traceback import eventlet import msgpack import flask from flask import ( Flask, render_template, make_response, copy_current_request_context, jsonify, request ) from flask_socketio import SocketIO from jinja2 import Environment, FileSystemLoader, ChoiceLoader from bowtie._component import Event, Component, COMPONENT_REGISTRY from bowtie.pager import Pager from bowtie.exceptions import ( GridIndexError, NoSidebarError, NotStatefulEvent, NoUnusedCellsError, SpanOverlapError, SizeError, WebpackError, YarnError ) eventlet.monkey_patch(time=True) Route = namedtuple('Route', ['view', 'path', 'exact']) _Import = namedtuple('_Import', ['module', 'component']) _DIRECTORY = Path('build') _WEBPACK = './node_modules/.bin/webpack' _MIN_NODE_VERSION = 6, 11, 5 class Scheduler: """Run scheduled tasks.""" def __init__(self, app, seconds, func): """Create a scheduled function.""" self.app = app self.seconds = seconds self.func = func self.thread = None def context(self, func): """Provide flask context to function.""" def wrap(): with self.app.app_context(): func() return wrap def start(self): """Start the scheduled task.""" self.thread = eventlet.spawn(self.run) def run(self): """Invoke the function repeatedly on a timer.""" ret = eventlet.spawn(self.context(self.func)) eventlet.sleep(self.seconds) try: ret.wait() except Exception: # pylint: disable=broad-except traceback.print_exc() self.thread = eventlet.spawn(self.run) def stop(self): """Stop the scheduled task.""" if self.thread: self.thread.cancel() def raise_not_number(x: float) -> None: """Raise ``SizeError`` if ``x`` is not a number``.""" try: float(x) except ValueError: raise SizeError('Must pass a number, received {}'.format(x)) class Span: """Define the location of a widget.""" def __init__(self, row_start: int, column_start: int, row_end: Optional[int] = None, column_end: Optional[int] = None) -> None: """Create a span for a widget. Indexing starts at 0. Start is inclusive and end is exclusive CSS Grid indexing starts at 1 and is [inclusive, exclusive) Note: `_start` and `_end` follow css grid naming convention. Parameters ---------- row_start : int column_start : int row_end : int, optional column_end : int, optional """ self.row_start = row_start self.column_start = column_start # add 1 to then ends because they start counting from 1 if row_end is None: self.row_end = self.row_start + 1 else: self.row_end = row_end if column_end is None: self.column_end = self.column_start + 1 else: self.column_end = column_end @property def _key(self) -> Tuple[int, int, int, int]: return self.row_start, self.column_start, self.row_end, self.column_end def __hash__(self) -> int: """Hash for dict.""" return hash(self._key) def __eq__(self, other) -> bool: """Compare eq for dict.""" # pylint: disable=protected-access return isinstance(other, type(self)) and self._key == other._key def __repr__(self) -> str: """Show the starting and ending points. This is used as a key in javascript. """ return '{},{},{},{}'.format( self.row_start + 1, self.column_start + 1, self.row_end + 1, self.column_end + 1 ) def overlap(self, other: 'Span'): """Detect if two spans overlap.""" return not ( # if one rectangle is left of other other.column_end <= self.column_start or self.column_end <= other.column_start # if one rectangle is above other or other.row_end <= self.row_start or self.row_end <= other.row_start ) @property def cells(self) -> Generator[Tuple[int, int], None, None]: """Generate cells in span.""" yield from itertools.product( range(self.row_start, self.row_end), range(self.column_start, self.column_end) ) class Size: """Size of rows and columns in grid. This is accessed through ``.rows`` and ``.columns`` from App and View instances. This uses CSS's minmax function. The minmax() CSS function defines a size range greater than or equal to min and less than or equal to max. If max < min, then max is ignored and minmax(min,max) is treated as min. As a maximum, a value sets the flex factor of a grid track; it is invalid as a minimum. Examples -------- Laying out an app with the first row using 1/3 of the space and the second row using 2/3 of the space. >>> app = App(rows=2, columns=3) >>> app.rows[0].fraction(1) 1fr >>> app.rows[1].fraction(2) 2fr """ def __init__(self) -> None: """Create a default row or column size with fraction = 1.""" self.minimum: str = '' self.maximum: str = '' self.fraction(1) def auto(self) -> 'Size': """Set the size to auto or content based.""" self.maximum = 'auto' return self def min_auto(self) -> 'Size': """Set the minimum size to auto or content based.""" self.minimum = 'auto' return self def pixels(self, value: float) -> 'Size': """Set the size in pixels.""" raise_not_number(value) self.maximum = '{}px'.format(value) return self def min_pixels(self, value: float) -> 'Size': """Set the minimum size in pixels.""" raise_not_number(value) self.minimum = '{}px'.format(value) return self def ems(self, value: float) -> 'Size': """Set the size in ems.""" raise_not_number(value) self.maximum = '{}em'.format(value) return self def min_ems(self, value: float) -> 'Size': """Set the minimum size in ems.""" raise_not_number(value) self.minimum = '{}em'.format(value) return self def fraction(self, value: float) -> 'Size': """Set the fraction of free space to use.""" raise_not_number(value) self.maximum = '{}fr'.format(value) return self def percent(self, value: float) -> 'Size': """Set the percentage of free space to use.""" raise_not_number(value) self.maximum = '{}%'.format(value) return self def min_percent(self, value: float) -> 'Size': """Set the minimum percentage of free space to use.""" raise_not_number(value) self.minimum = '{}%'.format(value) return self def __repr__(self) -> str: """Represent the size to be inserted into a JSX template.""" if self.minimum: return 'minmax({}, {})'.format(self.minimum, self.maximum) return self.maximum class Gap: """Margin between rows or columns of the grid. This is accessed through ``.row_gap`` and ``.column_gap`` from App and View instances. Examples -------- Create a gap of 5 pixels between all rows. >>> app = App() >>> app.row_gap.pixels(5) 5px """ def __init__(self) -> None: """Create a default margin of zero.""" self.gap: str = '' self.pixels(0) def pixels(self, value: int) -> 'Gap': """Set the margin in pixels.""" raise_not_number(value) self.gap = '{}px'.format(value) return self def ems(self, value: int) -> 'Gap': """Set the margin in ems.""" raise_not_number(value) self.gap = '{}em'.format(value) return self def percent(self, value) -> 'Gap': """Set the margin as a percentage.""" raise_not_number(value) self.gap = '{}%'.format(value) return self def __repr__(self) -> str: """Represent the margin to be inserted into a JSX template.""" return self.gap def _check_index(value: int, length: int, bound: bool) -> int: if not isinstance(value, int): raise GridIndexError('Indices must be integers, found {}.'.format(value)) if value < 0: value = value + length if value < 0 + bound or value >= length + bound: raise GridIndexError('Index out of range.') return value def _slice_to_start_end(slc: slice, length: int) -> Tuple[int, int]: if slc.step is not None and slc.step != 1: raise GridIndexError( 'slice step is not supported must be None or 1, was {}'.format(slc.step) ) start = 0 if slc.start is not None: start = slc.start end = length if slc.stop is not None: end = slc.stop return start, end class Components: """List like class for storing components to override iadd. The purpose of this class is to override the `iadd` function. I want to be able to support all the following >>> from bowtie import App >>> from bowtie.control import Button >>> app = App() >>> button = Button() >>> app[0, 0] = button >>> app[0, 0] = button, button >>> app[0, 0] += button >>> app[0, 0] += button, button """ TYPE_MSG: str = 'Must add a component or sequence of components, found {}.' def __init__(self, component: Optional[Union[Component, Sequence[Component]]] = None ) -> None: """Create a components list.""" self.data: List[Component] if component is None: self.data = [] elif isinstance(component, Component): self.data = [component] else: self.data = list(component) def __len__(self): """Count components.""" return self.data.__len__() def append(self, component: Component): """Append component to the list.""" return self.data.append(component) def __iter__(self): """Iterate over components.""" return self.data.__iter__() def __getitem__(self, key): """Get item as a list.""" return self.data.__getitem__(key) def _add(self, method, other: Union[Component, Sequence[Component]]) -> 'Components': if isinstance(other, Component): return method([other]) if isinstance(other, Sequence): other = list(other) if not all(True for x in other if isinstance(x, Component)): raise TypeError(self.TYPE_MSG.format(other)) return method(other) raise TypeError(self.TYPE_MSG.format(other)) def __iadd__(self, other: Union[Component, Sequence[Component]]): """Append items to list when adding.""" return self._add(self.data.__iadd__, other) def __add__(self, other: Union[Component, Sequence[Component]]): """Append items to list when adding.""" return self._add(self.data.__add__, other) class View: """Grid of components.""" _NEXT_UUID = 0 @classmethod def _next_uuid(cls) -> int: cls._NEXT_UUID += 1 return cls._NEXT_UUID def __init__(self, rows: int = 1, columns: int = 1, sidebar: bool = False, background_color: str = 'White') -> None: """Create a new grid. Parameters ---------- rows : int, optional Number of rows in the grid. columns : int, optional Number of columns in the grid. sidebar : bool, optional Enable a sidebar for control components. background_color : str, optional Background color of the control pane. """ self._uuid = View._next_uuid() self.layout = None self.column_gap = Gap() self.row_gap = Gap() self.border = Gap().pixels(7) self.rows = [Size() for _ in range(rows)] self.columns = [Size() for _ in range(columns)] self.sidebar = sidebar self.background_color = background_color self.layout: Optional[Callable] = None self._controllers: List[Component] = [] self._spans: Dict[Span, Components] = {} def _all_components(self) -> Generator[Component, None, None]: yield from self._controllers yield from itertools.chain.from_iterable(self._spans.values()) @property def _packages(self) -> Set[str]: # pylint: disable=protected-access packages = set(x._PACKAGE for x in self._all_components()) packages.discard(None) return packages @property def _templates(self) -> Set[str]: # pylint: disable=protected-access return set(x._TEMPLATE for x in self._all_components()) @property def _imports(self) -> Set[_Import]: # pylint: disable=protected-access return set(_Import(component=x._COMPONENT, module=x._TEMPLATE[:x._TEMPLATE.find('.')]) for x in self._all_components()) @property def _components(self) -> Set[Component]: return set(self._all_components()) def _key_to_span(self, key: Any) -> Span: # TODO spaghetti code cleanup needed! if isinstance(key, Span): return key if isinstance(key, tuple): if len(key) == 1: return self._key_to_span(key[0]) try: row_key, column_key = key except ValueError: raise GridIndexError('Index must be 1 or 2 values, found {}'.format(key)) if isinstance(row_key, int): row_start = _check_index(row_key, len(self.rows), False) row_end = row_start + 1 elif isinstance(row_key, slice): row_start, row_end = _slice_to_start_end(row_key, len(self.rows)) row_start = _check_index(row_start, len(self.rows), False) row_end = _check_index(row_end, len(self.rows), True) else: raise GridIndexError( 'Cannot index with {}, pass in a int or a slice.'.format(row_key) ) if isinstance(column_key, int): column_start = _check_index(column_key, len(self.columns), False) column_end = column_start + 1 elif isinstance(column_key, slice): column_start, column_end = _slice_to_start_end(column_key, len(self.columns)) column_start = _check_index(column_start, len(self.columns), False) column_end = _check_index(column_end, len(self.columns), True) else: raise GridIndexError( 'Cannot index with {}, pass in a int or a slice.'.format(column_key) ) rows_cols = row_start, column_start, row_end, column_end elif isinstance(key, slice): start, end = _slice_to_start_end(key, len(self.rows)) start = _check_index(start, len(self.rows), False) end = _check_index(end, len(self.rows), True) rows_cols = start, 0, end, len(self.columns) elif isinstance(key, int): row_start = _check_index(key, len(self.rows), False) rows_cols = row_start, 0, row_start + 1, len(self.columns) else: raise GridIndexError('Invalid index {}'.format(key)) return Span(*rows_cols) def __getitem__(self, key: Any) -> Components: """Get item from the view.""" span = self._key_to_span(key) if span not in self._spans: raise KeyError(f'Key {key} has not been used') return self._spans[span] def __setitem__(self, key: Any, component: Union[Component, Sequence[Component]]) -> None: """Add widget to the view.""" span = self._key_to_span(key) for used_span in self._spans: if span != used_span and span.overlap(used_span): raise SpanOverlapError(f'Spans {span} and {used_span} overlap. ' 'This is not permitted. ' 'If you want to do this please open an issue ' 'and explain your use case. ' 'https://github.com/jwkvam/bowtie/issues') self._spans[span] = Components(component) def add(self, component: Union[Component, Sequence[Component]]) -> None: """Add a widget to the grid in the next available cell. Searches over columns then rows for available cells. Parameters ---------- components : bowtie._Component A Bowtie widget instance. """ try: self[Span(*self._available_cell())] = component except NoUnusedCellsError: span = list(self._spans.keys())[-1] self._spans[span] += component def _available_cell(self) -> Tuple[int, int]: """Find next available cell first by row then column. First, construct a set containing all cells. Then iterate over the spans and remove occupied cells. """ cells = set(itertools.product(range(len(self.rows)), range(len(self.columns)))) for span in self._spans: for cell in span.cells: cells.remove(cell) if not cells: raise NoUnusedCellsError('No available cells') return min(cells) def add_sidebar(self, component: Component) -> None: """Add a widget to the sidebar. Parameters ---------- component : bowtie._Component Add this component to the sidebar, it will be appended to the end. """ if not self.sidebar: raise NoSidebarError('Set `sidebar=True` if you want to use the sidebar.') if not isinstance(component, Component): raise ValueError('component must be Component type, found {}'.format(component)) # self._track_widget(widget) self._controllers.append(component) # pylint: disable=protected-access @property def _columns_sidebar(self): columns = [] if self.sidebar: columns.append(Size().ems(18)) columns += self.columns return columns class App: """Core class to layout, connect, build a Bowtie app.""" def __init__(self, name='__main__', app=None, rows: int = 1, columns: int = 1, sidebar: bool = False, title: str = 'Bowtie App', theme: Optional[str] = None, background_color: str = 'White', socketio: str = '', debug: bool = False) -> None: """Create a Bowtie App. Parameters ---------- name : str, optional Use __name__ or leave as default if using a single module. Consult the Flask docs on "import_name" for details on more complex apps. app : Flask app, optional If you are defining your own Flask app, pass it in here. You only need this if you are doing other stuff with Flask outside of bowtie. row : int, optional Number of rows in the grid. columns : int, optional Number of columns in the grid. sidebar : bool, optional Enable a sidebar for control components. title : str, optional Title of the HTML. theme : str, optional Color for Ant Design components. background_color : str, optional Background color of the control pane. socketio : string, optional Socket.io path prefix, only change this for advanced deployments. debug : bool, optional Enable debugging in Flask. Disable in production! """ self.title = title self.theme = theme self._init: Optional[Callable] = None self._socketio_path = socketio self._schedules: List[Scheduler] = [] self._subscriptions: Dict[Event, List[Tuple[List[Event], Callable]]] = defaultdict(list) self._pages: Dict[Pager, Callable] = {} self._uploads: Dict[int, Callable] = {} self._root = View(rows=rows, columns=columns, sidebar=sidebar, background_color=background_color) self._routes: List[Route] = [] self._package_dir = Path(os.path.dirname(__file__)) self._jinjaenv = Environment( loader=FileSystemLoader(str(self._package_dir / 'templates')), trim_blocks=True, lstrip_blocks=True ) if app is None: self.app = Flask(name) else: self.app = app self.app.debug = debug self._socketio = SocketIO(self.app, binary=True, path=socketio + 'socket.io') self.app.secret_key = secrets.token_bytes() self.add_route(view=self._root, path='/', exact=True) # https://buxty.com/b/2012/05/custom-template-folders-with-flask/ templates = Path(__file__).parent / 'templates' self.app.jinja_loader = ChoiceLoader([ self.app.jinja_loader, FileSystemLoader(str(templates)), ]) self._build_dir = self.app.root_path / _DIRECTORY self.app.before_first_request(self._endpoints) def wsgi_app(self, environ, start_response): """Support uwsgi and gunicorn.""" return self.app.wsgi_app(environ, start_response) def __call__(self, environ, start_response): """Support uwsgi and gunicorn.""" return self.wsgi_app(environ, start_response) def __getattr__(self, name: str): """Export attributes from root view.""" if name == 'columns': return self._root.columns if name == 'rows': return self._root.rows if name == 'column_gap': return self._root.column_gap if name == 'row_gap': return self._root.row_gap if name == 'border': return self._root.border if name == 'layout': return self._root.layout raise AttributeError(name) def __setattr__(self, name, value): """Set layout function for root view.""" if name == 'layout': return self._root.__setattr__(name, value) return super().__setattr__(name, value) def __getitem__(self, key: Any): """Get item from root view.""" return self._root.__getitem__(key) def __setitem__(self, key: Any, value: Union[Component, Sequence[Component]]) -> None: """Add widget to the root view.""" self._root.__setitem__(key, value) def add(self, component: Component) -> None: """Add a widget to the grid in the next available cell. Searches over columns then rows for available cells. Parameters ---------- component : bowtie._Component A Bowtie component instance. """ self._root.add(component) def add_sidebar(self, widget: Component) -> None: """Add a widget to the sidebar. Parameters ---------- widget : bowtie._Component Add this widget to the sidebar, it will be appended to the end. """ self._root.add_sidebar(widget) def add_route(self, view: View, path: str, exact: bool = True) -> None: """Add a view to the app. Parameters ---------- view : View path : str exact : bool, optional """ if path[0] != '/': path = '/' + path for route in self._routes: assert path != route.path, 'Cannot use the same path twice' self._routes.append(Route(view=view, path=path, exact=exact)) self.app.add_url_rule( path, path[1:], lambda: render_template('bowtie.html', title=self.title) ) def subscribe(self, *events: Union[Event, Pager]) -> Callable: """Call a function in response to an event. If more than one event is given, `func` will be given as many arguments as there are events. If the pager calls notify, the decorated function will be called. Parameters ---------- *event : event or pager Bowtie event, must have at least one. Examples -------- Subscribing a function to multiple events. >>> from bowtie.control import Dropdown, Slider >>> app = App() >>> dd = Dropdown() >>> slide = Slider() >>> @app.subscribe(dd.on_change, slide.on_change) ... def callback(dd_item, slide_value): ... pass >>> @app.subscribe(dd.on_change) ... @app.subscribe(slide.on_change) ... def callback2(value): ... pass Using the pager to run a callback function. >>> from bowtie.pager import Pager >>> app = App() >>> pager = Pager() >>> @app.subscribe(pager) ... def callback(): ... pass >>> def scheduledtask(): ... pager.notify() """ try: first_event = events[0] except IndexError: raise IndexError('Must subscribe to at least one event.') if len(events) != len(set(events)): raise ValueError( 'Subscribed to the same event multiple times. All events must be unique.' ) if len(events) > 1: # check if we are using any non stateful events for event in events: if isinstance(event, Pager): raise NotStatefulEvent('Pagers must be subscribed by itself.') if event.getter is None: raise NotStatefulEvent( f'{event.uuid}.on_{event.name} is not a stateful event. ' 'It must be used alone.' ) def decorator(func: Callable) -> Callable: """Handle three types of events: pages, uploads, and normal events.""" if isinstance(first_event, Pager): self._pages[first_event] = func elif first_event.name == 'upload': if first_event.uuid in self._uploads: warnings.warn( ('Overwriting function "{func1}" with function ' '"{func2}" for upload object "{obj}".').format( func1=self._uploads[first_event.uuid], func2=func.__name__, obj=COMPONENT_REGISTRY[first_event.uuid] ), Warning) self._uploads[first_event.uuid] = func else: for event in events: # need to have `events` here to maintain order of arguments # not sure how to deal with mypy typing errors on events so ignoring self._subscriptions[event].append((events, func)) # type: ignore return func return decorator def load(self, func: Callable) -> Callable: """Call a function on page load. Parameters ---------- func : callable Function to be called. """ self._init = func return func def schedule(self, seconds: float): """Call a function periodically. Parameters ---------- seconds : float Minimum interval of function calls. func : callable Function to be called. """ def wrap(func: Callable): self._schedules.append(Scheduler(self.app, seconds, func)) return wrap def _write_templates(self) -> Set[str]: indexjsx = self._jinjaenv.get_template('index.jsx.j2') componentsjs = self._jinjaenv.get_template('components.js.j2') webpack = self._jinjaenv.get_template('webpack.common.js.j2') src = self._create_jspath() webpack_path = self._build_dir / webpack.name[:-3] # type: ignore with webpack_path.open('w') as f: f.write( webpack.render(color=self.theme) ) # copy js modules that are always needed for name in ['progress.jsx', 'view.jsx', 'utils.js']: template_src = self._package_dir / 'src' / name shutil.copy(template_src, src) # Layout Design # # Dictionaries that are keyed by the components # # To layout this will need to look through all components that have a key of the route # # use cases # 1. statically add items to controller in list # 2. remove item from controller # 3. add item back to controller # # issues: # widget reordering # order preserving operations components: Set[Component] = set() imports: Set[_Import] = set() packages: Set[str] = set() for route in self._routes: if route.view.layout: route.view.layout() packages |= route.view._packages # pylint: disable=protected-access imports |= route.view._imports # pylint: disable=protected-access components |= route.view._components # pylint: disable=protected-access for template in route.view._templates: # pylint: disable=protected-access template_src = self._package_dir / 'src' / template shutil.copy(template_src, src) with (src / componentsjs.name[:-3]).open('w') as f: # type: ignore f.write( componentsjs.render( imports=imports, socketio=self._socketio_path, components=components, ) ) with (src / indexjsx.name[:-3]).open('w') as f: # type: ignore f.write( indexjsx.render( maxviewid=View._NEXT_UUID, # pylint: disable=protected-access socketio=self._socketio_path, pages=self._pages, routes=self._routes, ) ) return packages def _build(self, notebook: Optional[str] = None) -> None: """Compile the Bowtie application.""" if node_version() < _MIN_NODE_VERSION: raise WebpackError( f'Webpack requires at least version {_MIN_NODE_VERSION} of Node, ' f'found version {node_version}.' ) packages = self._write_templates() for filename in ['package.json', 'webpack.prod.js', 'webpack.dev.js']: if not (self._build_dir / filename).is_file(): sourcefile = self._package_dir / 'src' / filename shutil.copy(sourcefile, self._build_dir) if self._run(['yarn', '--ignore-engines', 'install'], notebook=notebook) > 1: raise YarnError('Error installing node packages') if packages: installed = self._installed_packages() new_packages = [x for x in packages if x.split('@')[0] not in installed] if new_packages: retval = self._run( ['yarn', '--ignore-engines', 'add'] + new_packages, notebook=notebook ) if retval > 1: raise YarnError('Error installing node packages') elif retval == 1: print('Yarn error but trying to continue build') retval = self._run([_WEBPACK, '--config', 'webpack.dev.js'], notebook=notebook) if retval != 0: raise WebpackError('Error building with webpack') def _endpoints(self): def generate_sio_handler(main_event, supports): # get all events from all subscriptions associated with this event uniq_events = set() for events, _ in supports: uniq_events.update(events) uniq_events.remove(main_event) for event in uniq_events: comp = COMPONENT_REGISTRY[event.uuid] if event.getter is None: raise AttributeError( f'{comp} has no getter associated with event "on_{event.name}"' ) def handler(*args): def wrapuser(): event_data = {} for event in uniq_events: comp = COMPONENT_REGISTRY[event.uuid] # we already checked that this component has a getter event_data[event.signal] = getattr(comp, event.getter)() # if there is no getter, then there is no data to unpack # if there is a getter, then we need to unpack the data sent main_getter = main_event.getter if main_getter is not None: comp = COMPONENT_REGISTRY[main_event.uuid] event_data[main_event.signal] = getattr(comp, '_' + main_getter)( msgpack.unpackb(args[0], encoding='utf8') ) # gather the remaining data from the other events through their getter methods for events, func in supports: if main_getter is not None: func(*(event_data[event.signal] for event in events)) else: func() # TODO replace with flask socketio start_background_task eventlet.spawn(copy_current_request_context(wrapuser)) return handler for event, supports in self._subscriptions.items(): self._socketio.on(event.signal)(generate_sio_handler(event, supports)) if self._init is not None: self._socketio.on('INITIALIZE')(lambda: eventlet.spawn( copy_current_request_context(self._init) )) def gen_upload(func): def upload(): upfile = request.files['file'] retval = func(upfile.filename, upfile.stream) if retval: return make_response(jsonify(), 400) return make_response(jsonify(), 200) return upload for uuid, func in self._uploads.items(): self.app.add_url_rule( f'/upload{uuid}', f'upload{uuid}', gen_upload(func), methods=['POST'] ) for page, func in self._pages.items(): # pylint: disable=protected-access self._socketio.on(f'resp#{page._uuid}')(lambda: eventlet.spawn( copy_current_request_context(func) )) # bundle route @self.app.route('/bowtie/bundle.js') def bowtiebundlejs(): # pylint: disable=unused-variable bundle_path = self.app.root_path + '/build/bundle.js' bundle_path_gz = bundle_path + '.gz' try: if os.path.getmtime(bundle_path) > os.path.getmtime(bundle_path_gz): return open(bundle_path, 'r').read() bundle = open(bundle_path_gz, 'rb').read() response = flask.make_response(bundle) response.headers['content-encoding'] = 'gzip' response.headers['vary'] = 'accept-encoding' response.headers['content-length'] = len(response.data) return response except FileNotFoundError: if os.path.isfile(bundle_path_gz): bundle = open(bundle_path_gz, 'rb').read() response = flask.make_response(bundle) response.headers['Content-Encoding'] = 'gzip' response.headers['Vary'] = 'Accept-Encoding' response.headers['Content-Length'] = len(response.data) return response return open(bundle_path, 'r').read() for schedule in self._schedules: schedule.start() def _serve(self, host='0.0.0.0', port=9991) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((host, port)) if result == 0: raise Exception(f'Port {port} is unavailable on host {host}, aborting.') self._socketio.run(self.app, host=host, port=port) for schedule in self._schedules: schedule.stop() def _installed_packages(self) -> Generator[str, None, None]: """Extract installed packages as list from `package.json`.""" with (self._build_dir / 'package.json').open('r') as f: packages = json.load(f) yield from packages['dependencies'].keys() def _create_jspath(self) -> Path: """Create the source directory for the build.""" src = self._build_dir / 'bowtiejs' os.makedirs(src, exist_ok=True) return src def _run(self, command: List[str], notebook: Optional[str] = None) -> int: """Run command from terminal and notebook and view output from subprocess.""" if notebook is None: return Popen(command, cwd=self._build_dir).wait() cmd = Popen(command, cwd=self._build_dir, stdout=PIPE, stderr=STDOUT) while True: line = cmd.stdout.readline() if line == b'' and cmd.poll() is not None: return cmd.poll() print(line.decode('utf-8'), end='') raise Exception() def node_version(): """Get node version.""" version = check_output(('node', '--version')) return tuple(int(x) for x in version.strip()[1:].split(b'.')) PKECM bowtie/_cache.py"""Bowtie cache functions.""" import flask from flask_socketio import emit import eventlet from eventlet.queue import LightQueue import msgpack from bowtie._component import pack def validate(key): """Check that the key is a string or bytestring. That's the only valid type of key. """ if not isinstance(key, (str, bytes)): raise KeyError('Key must be of type str or bytes, found type {}'.format(type(key))) # pylint: disable=too-few-public-methods class _Cache: """Store data in the browser. This cache uses session storage so data will stay in the browser until the tab is closed. All data must be serializable, which means if the serialization transforms the data it won't be the same when it is fetched. Examples -------- >>> from bowtie import cache >>> cache['a'] = True # doctest: +SKIP >>> cache['a'] # doctest: +SKIP True >>> cache['b'] = np.arange(5) # doctest: +SKIP >>> cache['b'] # doctest: +SKIP [1, 2, 3, 4, 5] """ def __getitem__(self, key): """Load the value stored with the key. Parameters ---------- key : str The key to lookup the value stored. Returns ------- object The value if the key exists in the cache, otherwise None. """ validate(key) signal = 'cache_load' event = LightQueue(1) if flask.has_request_context(): emit(signal, {'data': pack(key)}, callback=event.put) else: sio = flask.current_app.extensions['socketio'] sio.emit(signal, {'data': pack(key)}, callback=event.put) return msgpack.unpackb(bytes(event.get(timeout=10)), encoding='utf8') def __setitem__(self, key, value): """Store the key value pair. Parameters ---------- key : str The key to determine where it's stored, you'll need this to load the value later. value : object The value to store in the cache. Returns ------- None """ validate(key) signal = 'cache_save' if flask.has_request_context(): emit(signal, {'key': pack(key), 'data': pack(value)}) else: sio = flask.current_app.extensions['socketio'] sio.emit(signal, {'key': pack(key), 'data': pack(value)}) eventlet.sleep() # pylint: disable=invalid-name cache = _Cache() PK4LME,[ [ bowtie/_command.py""" Decorates a function for Bowtie. Reference --------- https://gist.github.com/carlsmith/800cbe3e11f630ac8aa0 """ from typing import Callable import sys import inspect from subprocess import call import click from bowtie._app import _WEBPACK, App class WrongNumberOfArguments(TypeError): """The "build" function accepts an incorrect number of arguments.""" def numargs(func: Callable) -> int: """Get number of arguments.""" return len(inspect.signature(func).parameters) def command(func): """Command line interface decorator. Decorate a function for building a Bowtie application and turn it into a command line interface. """ # pylint: disable=protected-access,unused-variable nargs = numargs(func) if nargs > 0: raise WrongNumberOfArguments( f'Decorated function "{func.__name__}" should have no arguments, it has {nargs}.' ) app = func() if app is None: raise TypeError( 'No `App` instance was returned. ' 'In the function decorated with @command, ' 'return the `App` instance so it can be built.' ) if not isinstance(app, App): raise TypeError( f'Returned value {app} is of type {type(app)}, ' 'it needs to be a bowtie.App instance.' ) @click.group(options_metavar='[--help]') def cmd(): """Bowtie CLI to help build and run your app.""" pass @cmd.command(add_help_option=False) def build(): """Write the app, downloads the packages, and bundles it with Webpack.""" app._build() @cmd.command(add_help_option=False) @click.option('--host', '-h', default='0.0.0.0', type=str) @click.option('--port', '-p', default=9991, type=int) def run(host, port): """Build the app and serve it.""" app._build() app._serve(host, port) @cmd.command(add_help_option=True) @click.option('--host', '-h', default='0.0.0.0', type=str) @click.option('--port', '-p', default=9991, type=int) def serve(host, port): """Serve the Bowtie app.""" app._serve(host, port) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) def dev(extra): """Recompile the app for development.""" line = (_WEBPACK, '--config', 'webpack.dev.js') + extra call(line, cwd=app._build_dir) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) def prod(extra): """Recompile the app for production.""" line = (_WEBPACK, '--config', 'webpack.prod.js', '--progress') + extra call(line, cwd=app._build_dir) locale = inspect.stack()[1][0].f_locals module = locale.get("__name__") if module == "__main__": try: arg = sys.argv[1:] except IndexError: arg = ('--help',) # pylint: disable=too-many-function-args sys.exit(cmd(arg)) return cmd PK4LM6Z''bowtie/_component.py"""Bowtie abstract component classes. All visual and control components inherit these. """ from typing import Any, Callable, Optional, ClassVar, Tuple # pylint: disable=unused-import from abc import ABCMeta, abstractmethod import string from functools import wraps import json from datetime import datetime, date, time import msgpack import flask from flask_socketio import emit import eventlet from eventlet.queue import LightQueue from bowtie.exceptions import SerializationError from bowtie._typing import JSON COMPONENT_REGISTRY = {} SEPARATOR = '#' class Event: """Data structure to hold information for events.""" def __init__(self, name: str, uuid: int, getter: Optional[str] = None) -> None: """Create an event. Parameters ---------- name : str uuid : int getter : str, optional """ self.name = name self.uuid = uuid self.getter = getter @property def signal(self) -> str: """Name of socket.io message.""" return '{}{}{}'.format(self.uuid, SEPARATOR, self.name) @property def _key(self) -> Tuple[str, int, Optional[str]]: return self.name, self.uuid, self.getter def __repr__(self) -> str: """Create an Event.""" return "Event('{}', {}, '{}')".format(self.name, self.uuid, self.getter) def __eq__(self, other) -> bool: """Compare Events for equality.""" # pylint: disable=protected-access return isinstance(other, type(self)) and self._key == other._key def __hash__(self) -> int: """Compute hash for Event.""" return hash(self._key) def jsbool(x: bool) -> str: """Convert Python bool to Javascript bool.""" return repr(x).lower() def json_conversion(obj: Any) -> JSON: """Encode additional objects to JSON.""" try: # numpy isn't an explicit dependency of bowtie # so we can't assume it's available import numpy as np if isinstance(obj, (np.ndarray, np.generic)): return obj.tolist() except ImportError: pass try: # pandas isn't an explicit dependency of bowtie # so we can't assume it's available import pandas as pd if isinstance(obj, pd.DatetimeIndex): return [x.isoformat() for x in obj.to_pydatetime()] if isinstance(obj, pd.Index): return obj.tolist() if isinstance(obj, pd.Series): try: return [x.isoformat() for x in obj.dt.to_pydatetime()] except AttributeError: return obj.tolist() except ImportError: pass if isinstance(obj, (datetime, time, date)): return obj.isoformat() raise TypeError('Not sure how to serialize {} of type {}'.format(obj, type(obj))) def jdumps(data: Any) -> str: """Encode Python object to JSON with additional encoders.""" return json.dumps(data, default=json_conversion) def encoders(obj: Any) -> JSON: # pylint: disable=too-many-return-statements """Convert Python object to msgpack encodable ones.""" try: # numpy isn't an explicit dependency of bowtie # so we can't assume it's available import numpy as np if isinstance(obj, (np.ndarray, np.generic)): # https://docs.scipy.org/doc/numpy/reference/arrays.scalars.html return obj.tolist() except ImportError: pass try: # pandas isn't an explicit dependency of bowtie # so we can't assume it's available import pandas as pd if isinstance(obj, pd.DatetimeIndex): return [x.isoformat() for x in obj.to_pydatetime()] if isinstance(obj, pd.Index): return obj.tolist() if isinstance(obj, pd.Series): try: return [x.isoformat() for x in obj.dt.to_pydatetime()] except AttributeError: return obj.tolist() except ImportError: pass if isinstance(obj, (datetime, time, date)): return obj.isoformat() return obj def pack(x: Any) -> bytes: """Encode ``x`` into msgpack with additional encoders.""" try: return msgpack.packb(x, default=encoders) except TypeError as exc: message = ('Serialization error, check the data passed to a do_ command. ' 'Cannot serialize this object:\n') + str(exc)[16:] raise SerializationError(message) def unpack(x: bytes) -> JSON: """Decode ``x`` from msgpack into Python object.""" return msgpack.unpackb(x, encoding='utf8') def make_event(event: Callable) -> Callable: """Create an event from a method signature.""" @property # type: ignore @wraps(event) def actualevent(self): # pylint: disable=missing-docstring name = event.__name__[3:] try: # the getter post processing function # is preserved with an underscore getter = event(self).__name__ except AttributeError: getter = None return Event(name, self._uuid, getter) # pylint: disable=protected-access return actualevent def is_event(attribute: str) -> bool: """Test if a method is an event.""" return attribute.startswith('on_') def make_command(command: Callable) -> Callable: """Create an command from a method signature.""" @wraps(command) def actualcommand(self, *args, **kwds): # pylint: disable=missing-docstring data = command(self, *args, **kwds) name = command.__name__[3:] signal = '{uuid}{sep}{event}'.format( uuid=self._uuid, # pylint: disable=protected-access sep=SEPARATOR, event=name ) if flask.has_request_context(): emit(signal, {'data': pack(data)}) else: sio = flask.current_app.extensions['socketio'] sio.emit(signal, {'data': pack(data)}) eventlet.sleep() return actualcommand def is_command(attribute: str) -> bool: """Test if a method is an command.""" return attribute.startswith('do_') def make_getter(getter: Callable) -> Callable: """Create an command from a method signature.""" def get(self, timeout=10): # pylint: disable=missing-docstring name = getter.__name__ signal = '{uuid}{sep}{event}'.format( uuid=self._uuid, # pylint: disable=protected-access sep=SEPARATOR, event=name ) event = LightQueue(1) if flask.has_request_context(): emit(signal, callback=lambda x: event.put(unpack(x))) else: sio = flask.current_app.extensions['socketio'] sio.emit(signal, callback=lambda x: event.put(unpack(x))) data = event.get(timeout=timeout) return getter(self, data) # don't want to copy the signature in this case get.__doc__ = getter.__doc__ return get def is_getter(attribute: str) -> bool: """Test if a method is a getter. It can be `get` or `get_*`. """ return attribute.startswith('get') class _Maker(ABCMeta): def __new__(cls, name, bases, namespace): # pylint: disable=arguments-differ for k in list(namespace.keys()): if is_event(k): namespace[k] = make_event(namespace[k]) if is_command(k): namespace[k] = make_command(namespace[k]) if is_getter(k): # preserve the post-processor with an underscore namespace['_' + k] = namespace[k] namespace[k] = make_getter(namespace[k]) return super().__new__(cls, name, bases, namespace) class FormatDict(dict): """Dict to replace missing keys.""" def __missing__(self, key: str) -> str: """Replace missing key with '{key}'.""" return '{' + key + '}' class Component(metaclass=_Maker): # pylint: disable=too-few-public-methods """Abstract class for all components. All visual and control classes subclass this so their events and commands get transformed by the metaclass. """ _NEXT_UUID = 0 # type: ClassVar[int] @property @abstractmethod def _TEMPLATE(self): pass # pylint: disable=invalid-name,multiple-statements @property @abstractmethod def _COMPONENT(self): pass # pylint: disable=invalid-name,multiple-statements @property @abstractmethod def _PACKAGE(self): pass # pylint: disable=invalid-name,multiple-statements @property @abstractmethod def _ATTRS(self): pass # pylint: disable=invalid-name,multiple-statements @property @abstractmethod def _instantiate(self): pass # pylint: disable=invalid-name,multiple-statements @classmethod def _next_uuid(cls) -> int: cls._NEXT_UUID += 1 return cls._NEXT_UUID def __init__(self) -> None: """Give the component a unique ID.""" # wanted to put "self" instead of "Component" # was surprised that didn't work self._uuid = Component._next_uuid() super().__init__() self._tagbase = " socket={{socket}} uuid={{'{uuid}'}} />".format(uuid=self._uuid) self._tag = '<' + self._COMPONENT if self._ATTRS: self._tag += ' ' + self._ATTRS self._comp = None COMPONENT_REGISTRY[self._uuid] = self @staticmethod def _insert(wrap: str, tag: Optional[str]) -> str: """Insert the component tag into the wrapper html. This ignores other tags already created like ``{socket}``. https://stackoverflow.com/a/11284026/744520 """ if tag is None: raise ValueError('tag cannot be None') formatter = string.Formatter() mapping = FormatDict(component=tag) return formatter.vformat(wrap, (), mapping) def __eq__(self, other) -> bool: """Compare Events for equality.""" # pylint: disable=protected-access return isinstance(other, type(self)) and self._uuid == other._uuid def __hash__(self) -> int: """Compute hash for Event.""" return hash(self._uuid) PK4LMm+$bowtie/_magic.py"""Jupyter Integration.""" import ast import socket from os.path import join as pjoin from urllib.parse import urljoin import json import re import sys import types import time from multiprocessing import Process from IPython import get_ipython from IPython.display import display, HTML, clear_output from IPython.core.error import UsageError from IPython.core.interactiveshell import InteractiveShell from IPython.core.magic import Magics, magics_class, line_magic from nbformat import read from notebook.notebookapp import list_running_servers import ipykernel import requests from bowtie._app import App def get_notebook_name() -> str: """Return the full path of the jupyter notebook. References ---------- https://github.com/jupyter/notebook/issues/1000#issuecomment-359875246 """ kernel_id = re.search( # type: ignore 'kernel-(.*).json', ipykernel.connect.get_connection_file() ).group(1) servers = list_running_servers() for server in servers: response = requests.get(urljoin(server['url'], 'api/sessions'), params={'token': server.get('token', '')}) for session in json.loads(response.text): if session['kernel']['id'] == kernel_id: relative_path = session['notebook']['path'] return pjoin(server['notebook_dir'], relative_path) raise Exception('Noteboook not found.') def load_notebook(fullname: str): """Import a notebook as a module.""" shell = InteractiveShell.instance() path = fullname # load the notebook object with open(path, 'r', encoding='utf-8') as f: notebook = read(f, 4) # create the module and add it to sys.modules mod = types.ModuleType(fullname) mod.__file__ = path # mod.__loader__ = self mod.__dict__['get_ipython'] = get_ipython sys.modules[fullname] = mod # extra work to ensure that magics that would affect the user_ns # actually affect the notebook module's ns save_user_ns = shell.user_ns shell.user_ns = mod.__dict__ try: for cell in notebook.cells: if cell.cell_type == 'code': try: # only run valid python code ast.parse(cell.source) except SyntaxError: continue try: # pylint: disable=exec-used exec(cell.source, mod.__dict__) except NameError: print(cell.source) raise finally: shell.user_ns = save_user_ns return mod @magics_class class BowtieMagic(Magics): """Bowtie magic commands.""" process = None @line_magic def bowtie_stop(self, line=''): # pylint: disable=unused-argument """Terminate Bowtie app.""" if self.process is None: print('No app has been run.') else: print('Terminating Bowtie app.') self.process.terminate() if self.process.is_alive(): time.sleep(1) self.process.kill() time.sleep(1) if self.process.is_alive(): print('Failed to stop Bowtie app.', file=sys.stderr) return print('Successfully stopped Bowtie app.') self.process.close() self.process = None @line_magic def bowtie(self, line=''): """Build and serve a Bowtie app.""" opts, appvar = self.parse_options(line, 'w:h:b:p:') width = opts.get('w', 1500) height = opts.get('h', 1000) border = opts.get('b', 2) port = opts.get('p', 9991) host = '0.0.0.0' sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((host, port)) if result == 0: raise Exception(f'Port {port} is unavailable on host {host}, aborting.') global_ns = self.shell.user_global_ns local_ns = self.shell.user_ns try: # pylint: disable=eval-used app = eval(appvar, global_ns, local_ns) except NameError: raise UsageError(f'Could not find App {appvar}') if not isinstance(app, App): raise UsageError(f'App is of type {type(app)} needs to be type ') app._build(notebook=get_notebook_name()) # pylint: disable=protected-access self.process = Process(target=app._serve) # pylint: disable=protected-access self.process.start() time.sleep(5) clear_output() display(HTML( f'' )) PK4LMI:r r bowtie/_progress.py"""Progress component. Not for direct use by user. """ from bowtie._component import Component class Progress(Component): """Circle progress indicator.""" _TEMPLATE = 'progress.jsx' _COMPONENT = 'AntProgress' _PACKAGE = None _ATTRS = None def __init__(self) -> None: """Create a progress indicator. This component is used by all visual components. It is not meant to be used alone. By default, it is not visible. It is an opt-in feature and you can happily use Bowtie without using the progress indicators at all. It is useful for indicating progress to the user for long-running processes. It can be accessed through the ``.progress`` accessor. Examples -------- Using the progress widget to provide feedback to the user. >>> from bowtie.visual import Plotly >>> plotly = Plotly() >>> def callback(x): ... plotly.progress.do_visible(True) ... plotly.progress.do_percent(0) ... compute1() ... plotly.progress.do_inc(50) ... compute2() ... plotly.progress.do_visible(False) References ---------- https://ant.design/components/progress/ """ super().__init__() self._tagbase = self._tagbase[:-3] + '>' self._tags = '<' + self._COMPONENT + self._tagbase, '' @property def _instantiate(self) -> str: """Instantiate a progress bar. This is normally never used. """ return ''.join(self._tags) # pylint: disable=no-self-use def do_percent(self, percent): """Set the percentage of the progress. Parameters ---------- percent : number Sets the progress to this percentage. Returns ------- None """ return percent def do_inc(self, inc): """Increment the progress indicator. Parameters ---------- inc : number Value to increment the progress. Returns ------- None """ return inc def do_visible(self, visible): """Hide and shows the progress indicator. Parameters ---------- visible : bool If ``True`` shows the progress indicator otherwise it is hidden. Returns ------- None """ return visible def do_active(self): """Reset the progress to active (in progress) status. Returns ------- None """ def do_success(self): """Display the progress indicator as done. Returns ------- None """ def do_error(self): """Display an error in the progress indicator. Returns ------- None """ PKECM0Ebowtie/_typing.py"""Bowtie types.""" from typing import Any, Union, Mapping, List JSON = Union[str, int, float, bool, None, Mapping[str, Any], List[Any]] PKECMu~D,bowtie/_utils.py"""Utility functions.""" import inspect def func_name() -> str: """Return name of calling function.""" return inspect.stack()[1].function PK4LMMẉ bowtie/auth.py"""Authentication out of the box. References ---------- https://stackoverflow.com/questions/13428708/best-way-to-make-flask-logins-login-required-the-default https://stackoverflow.com/questions/14367991/flask-before-request-add-exception-for-specific-route """ from typing import Dict, Optional from abc import ABC, abstractmethod from flask import Response, request, session from bowtie._app import App class Auth(ABC): """Abstract Authentication class.""" def __init__(self, app: App) -> None: """Create Auth class to protect flask routes and socketio connect.""" self.app = app self.app.app.before_request(self.before_request) # only need to check credentials on "connect" event self.app._socketio.on('connect')(self.socketio_auth) # pylint: disable=protected-access @abstractmethod def before_request(self): """Determine if a user is allowed to view this route. Name is subject to change. Returns ------- None, if no protection is needed. """ @abstractmethod def socketio_auth(self) -> bool: """Determine if a user is allowed to establish socketio connection. Name is subject to change. """ class BasicAuth(Auth): """Basic Authentication.""" def __init__(self, app: App, credentials: Dict[str, str]) -> None: """ Create basic auth with credentials. Parameters ---------- credentials : dict Usernames and passwords should be passed in as a dictionary. Examples -------- >>> from bowtie import App >>> from bowtie.auth import BasicAuth >>> app = App(__name__) >>> auth = BasicAuth(app, {'alice': 'secret1', 'bob': 'secret2'}) """ self.credentials = credentials super().__init__(app) def _check_auth(self, username: str, password: str) -> bool: """Check if a username/password combination is valid.""" try: return self.credentials[username] == password except KeyError: return False def socketio_auth(self) -> bool: """Determine if a user is allowed to establish socketio connection.""" try: return session['logged_in'] in self.credentials except KeyError: return False def before_request(self) -> Optional[Response]: """Determine if a user is allowed to view this route.""" auth = request.authorization if not auth or not self._check_auth(auth.username, auth.password): return Response( 'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'} ) session['logged_in'] = auth.username # pylint wants this return statement return None PK4LMijqMqMbowtie/control.py"""Control components.""" from typing import Callable, Optional, List, Union, Sequence from bowtie._component import Component, jdumps, jsbool # pylint: disable=too-few-public-methods class _Controller(Component): """Abstract class all control components inherit. Used to test if a an object is a controller. """ # pylint: disable=abstract-method @property def _instantiate(self) -> str: tagwrap = '{component}' + self._tagbase return self._insert(tagwrap, self._comp) class Button(_Controller): """An Ant design button.""" _TEMPLATE = 'button.jsx' _COMPONENT = 'SimpleButton' _PACKAGE = None _ATTRS = "label={{'{label}'}}" def __init__(self, label: str = '') -> None: """Create a button. Parameters ---------- label : str, optional Label on the button. """ super().__init__() self._comp = self._tag.format( label=label ) def on_click(self) -> None: """Emit an event when the button is clicked. There is no getter associated with this event. | **Payload:** ``None``. Returns ------- str Name of click event. """ class Upload(_Controller): """Draggable file upload widget.""" _TEMPLATE = 'upload.jsx' _COMPONENT = 'AntUpload' _PACKAGE = None _ATTRS = "multiple={{{multiple}}}" def __init__(self, multiple=True): """Create the widget. Note: the handler parameter may be changed in the future. Parameters ---------- multiple : bool, optional If true, you can upload multiple files at once. Even with this set to true, the handler will get called once per file uploaded. """ super().__init__() self._comp = self._tag.format( multiple=jsbool(multiple) ) def on_upload(self): """Emit an event when the selection changes. There is no getter associated with this event. | **Payload:** ``tuple`` with a str (name) and BytesIO (stream). The user is responsible for storing the object in this function if they want it for later use. To indicate an error, return True, otherwise a return value of None or False indicate success. """ class Dropdown(_Controller): """Dropdown based on react-select.""" _TEMPLATE = 'dropdown.jsx' _COMPONENT = 'Dropdown' _PACKAGE = 'react-select@1.3.0' _ATTRS = ('initOptions={{{options}}} ' 'multi={{{multi}}} ' 'default={{{default}}}') def __init__(self, labels: Optional[List[str]] = None, values: Optional[List[Union[str, int]]] = None, multi: bool = False, default: Optional[Union[str, int]] = None) -> None: """Create a drop down. Parameters ---------- labels : array-like, optional List of strings which will be visible to the user. values : array-like, optional List of values associated with the labels that are hidden from the user. multi : bool, optional If multiple selections are allowed. default : str or int, optional The default selected value. """ super().__init__() if values is not None and labels is not None: options = [{'value': value, 'label': str(label)} for value, label in zip(values, labels)] else: options = [] self._comp = self._tag.format( options=jdumps(options), multi=jsbool(multi), default=jdumps(default), ) def on_change(self) -> Callable: """Emit an event when the selection changes. | **Payload:** ``dict`` with keys "value" and "label". """ return self.get # pylint: disable=no-self-use def do_options(self, labels, values): """Replace the drop down fields. Parameters ---------- labels : array-like List of strings which will be visible to the user. values : array-like List of values associated with the labels that are hidden from the user. Returns ------- None """ return [dict(label=l, value=v) for l, v in zip(labels, values)] # pylint: disable=no-self-use def do_choose(self, values): """Replace the drop down fields. Parameters ---------- values : list or str or int Value(s) of drop down item(s) to be selected. Returns ------- None """ return values def get(self, data): """Return selected value(s).""" return data class Switch(_Controller): """Toggle switch.""" _TEMPLATE = 'switch.jsx' _COMPONENT = 'Toggle' _PACKAGE = None _ATTRS = 'defaultChecked={{{defaultChecked}}}' def __init__(self, initial: bool = False) -> None: """Create a toggle switch. Parameters ---------- initial : bool, optional Starting state of the switch. """ super().__init__() self._comp = self._tag.format( defaultChecked=jsbool(initial) ) def on_switch(self): """Emit an event when the switch is toggled. | **Payload:** ``bool`` status of the switch. Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the state of the switch. Returns ------- bool True if the switch is enabled. """ return data class _DatePickers(_Controller): """Specific Date Pickers inherit this class.""" _TEMPLATE = 'date.jsx' _COMPONENT = 'PickDates' _PACKAGE = None _ATTRS = ('date={{{date_type}}} ' 'month={{{month_type}}} ' 'range={{{range_type}}}') def __init__(self, date_type: bool = False, month_type: bool = False, range_type: bool = False) -> None: super().__init__() self._comp = self._tag.format( date_type=jsbool(date_type), month_type=jsbool(month_type), range_type=jsbool(range_type) ) class DatePicker(_DatePickers): """A Date Picker. Let's you choose an individual day. """ def __init__(self) -> None: """Create a date picker.""" super().__init__(date_type=True) def on_change(self): """Emit an event when a date is selected. | **Payload:** ``str`` of the form ``"yyyy-mm-dd"``. Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the currently selected date. Returns ------- str Date in the format "YYYY-MM-DD" """ return data class MonthPicker(_DatePickers): """A Month Picker. Let's you choose a month and year. """ def __init__(self) -> None: """Create month picker.""" super().__init__(month_type=True) def on_change(self): """Emit an event when a month is selected. | **Payload:** ``str`` of the form ``"yyyy-mm"``. Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the currently selected month. Returns ------- str Month in the format "YYYY-MM" """ return data class RangePicker(_DatePickers): """A Date Range Picker. Choose two dates to use as a range. """ def __init__(self) -> None: """Create a date range picker.""" super().__init__(range_type=True) def on_change(self): """Emit an event when a range is selected. | **Payload:** ``list`` of two dates ``["yyyy-mm-dd", "yyyy-mm-dd"]``. Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the currently selected date range. Returns ------- list A list of two strings ``["yyyy-mm-dd", "yyyy-mm-dd"]``. """ return data class Number(_Controller): """A number input widget with increment and decrement buttons.""" _TEMPLATE = 'number.jsx' _COMPONENT = 'AntNumber' _PACKAGE = None _ATTRS = ('start={{{start}}} ' 'min={{{minimum}}} ' 'max={{{maximum}}} ' 'step={{{step}}} ' "size={{'{size}'}}") def __init__(self, start: int = 0, minimum: float = -1e100, maximum: float = 1e100, step: int = 1, size: str = 'default') -> None: """Create a number input. Parameters ---------- start : number, optional Starting number minimum : number, optional Lower bound maximum : number, optional Upper bound size : 'default', 'large', 'small', optional Size of the text box. References ---------- https://ant.design/components/input/ """ super().__init__() self._comp = self._tag.format( start=start, minimum=minimum, maximum=maximum, step=step, size=size ) def on_change(self): """Emit an event when the number is changed. | **Payload:** ``number`` Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the current number. Returns ------- number """ return data class Textbox(_Controller): """A single line text box.""" _TEMPLATE = 'textbox.jsx' _COMPONENT = 'Textbox' _PACKAGE = None _ATTRS = ("placeholder={{'{placeholder}'}} " "size={{'{size}'}} " "type={{'{area}'}} " 'autosize={{{autosize}}} ' 'disabled={{{disabled}}}') def __init__(self, placeholder: str = 'Enter text', size: str = 'default', area: bool = False, autosize: bool = False, disabled: bool = False) -> None: """Create a text box. Parameters ---------- placeholder : str, optional Initial text that appears. size : 'default', 'large', 'small', optional Size of the text box. area : bool, optional Create a text area if True else create a single line input. autosize : bool, optional Automatically size the widget based on the content. disabled : bool, optional Disable input to the widget. References ---------- https://ant.design/components/input/ """ super().__init__() self._comp = self._tag.format( area='textarea' if area else 'text', autosize=jsbool(autosize), disabled=jsbool(disabled), placeholder=placeholder, size=size ) # pylint: disable=no-self-use def do_text(self, text): """Set the text of the text box. Parameters ---------- text : str String of the text box. """ return text def on_enter(self): """Emit an event when enter is pressed in the text box. | **Payload:** ``str`` Returns ------- str Name of event. """ return self.get def on_change(self) -> Callable: """Emit an event when the text is changed. | **Payload:** ``str`` Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the current text. Returns ------- str """ return data class Slider(_Controller): """Ant Design slider.""" _TEMPLATE = 'slider.jsx' _COMPONENT = 'AntSlider' _PACKAGE = None _ATTRS = ('range={{{range}}} ' 'min={{{minimum}}} ' 'max={{{maximum}}} ' 'step={{{step}}} ' 'start={{{start}}} ' 'marks={{{marks}}} ' 'vertical={{{vertical}}}') def __init__(self, start: Optional[Union[float, Sequence[float]]] = None, ranged: bool = False, minimum: float = 0, maximum: float = 100, step: float = 1, vertical: bool = False) -> None: """Create a slider. Parameters ---------- start : number or list with two values, optional Determines the starting value. If a list of two values are given it will be a range slider. ranged : bool, optional If this is a range slider. minimum : number, optional Minimum value of the slider. maximum : number, optional Maximum value of the slider. step : number, optional Step size. vertical : bool, optional If True, the slider will be vertical References ---------- https://ant.design/components/slider/ """ super().__init__() if start is None: if ranged: start = [minimum, maximum] else: start = minimum elif isinstance(start, Sequence): if len(start) > 2: raise ValueError('start cannot be more than 2 numbers') start = list(start) ranged = True self._comp = self._tag.format( range=jsbool(ranged), minimum=minimum, maximum=maximum, start=start, step=step, marks={minimum: str(minimum), maximum: str(maximum)}, vertical=jsbool(vertical) ) # pylint: disable=no-self-use def do_max(self, value): """Replace the max value of the slider. Parameters ---------- value : int Maximum value of the slider. """ return value def do_min(self, value): """Replace the min value of the slider. Parameters ---------- value : int Minimum value of the slider. """ return value def do_value(self, value): """Set the value of the slider. Parameters ---------- value : int Value of the slider. """ return value def do_inc(self, value=1): """Increment value of slider by given amount. Parameters ---------- value : int Number to change value of slider by. """ return value def do_min_max_value(self, minimum, maximum, value): """Set the minimum, maximum, and value of slider simultaneously. Parameters ---------- minimum : int Minimum value of the slider. maximum : int Maximum value of the slider. value : int Value of the slider. """ return minimum, maximum, value def on_change(self) -> Callable: """Emit an event when the slider's value changes. | **Payload:** ``number`` or ``list`` of values. Returns ------- str Name of event. """ return self.get def on_after_change(self): """Emit an event when the slider control is released. | **Payload:** ``number`` or ``list`` of values. Returns ------- str Name of event. """ return self.get def get(self, data): """ Get the currently selected value(s). Returns ------- list or number List if it's a range slider and gives two values. """ return data class Nouislider(_Controller): """A lightweight JavaScript range slider library.""" _TEMPLATE = 'nouislider.jsx' _COMPONENT = 'Nouislider' _PACKAGE = 'nouislider@11.1.0' _ATTRS = ('range={{{{min: {min}, max: {max}}}}} ' 'socket={{socket}} ' 'start={{{start}}} ' 'tooltips={{{tooltips}}}') def __init__(self, start: Union[int, Sequence[int]] = 0, minimum: int = 0, maximum: int = 100, tooltips: bool = True) -> None: """Create a slider. Parameters ---------- start : number or list with two values, optional Determines the starting value. If a list of two values are given it will be a range slider. minimum : number, optional Minimum value of the slider. maximum : number, optional Maximum value of the slider. tooltips : bool, optional Show a popup text box. References ---------- https://refreshless.com/nouislider/events-callbacks/ """ super().__init__() if not isinstance(start, Sequence): nstart = [start] else: nstart = list(start) self._comp = self._tag.format( min=minimum, max=maximum, start=nstart, tooltips=jsbool(tooltips) ) def on_update(self): """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get def on_slide(self): """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get def on_set(self): """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get def on_change(self) -> Callable: """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get def on_start(self): """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get def on_end(self): """Emit an event when the slider is moved. https://refreshless.com/nouislider/events-callbacks/ | **Payload:** ``list`` of values. Returns ------- str Name of event. """ return self.get # pylint: disable=no-self-use def get(self, data): """ Get the currently selected value(s). Returns ------- list or number List if it's a range slider and gives two values. """ return data PK4LMj #00bowtie/exceptions.py"""Bowtie exceptions.""" class YarnError(Exception): """Errors from ``Yarn``.""" class WebpackError(Exception): """Errors from ``Webpack``.""" class SizeError(Exception): """Size values must be a number.""" class GridIndexError(IndexError): """Invalid index into the grid layout.""" class NoUnusedCellsError(Exception): """All cells are used.""" class SpanOverlapError(Exception): """Spans may not overlap.""" class MissingRowOrColumn(Exception): """Missing a row or column.""" class NoSidebarError(Exception): """Cannot add to the sidebar when it doesn't exist.""" class NotStatefulEvent(Exception): """This event is not stateful and cannot be paired with other events.""" class SerializationError(TypeError): """Cannot serialize the data for command.""" PKBiFM$+YYbowtie/html.py"""Static HTML Components.""" from flask import Markup from markdown import markdown from bowtie._component import Component # pylint: disable=too-few-public-methods class _HTML(Component): """Abstract class for HTML components.""" # pylint: disable=abstract-method @property def _instantiate(self) -> str: tagwrap = '{component}' + self._tagbase return self._insert(tagwrap, self._comp) # pylint: disable=too-few-public-methods class Markdown(_HTML): """Display Markdown.""" _TEMPLATE = 'markdown.jsx' _COMPONENT = 'Markdown' _PACKAGE = None _ATTRS = "initial={{'{initial}'}}" def __init__(self, initial: str = '') -> None: """Create a Markdown widget. Parameters ---------- initial : str, optional Default markdown for the widget. """ super().__init__() self._comp = self._tag.format( initial=Markup(markdown(initial).replace('\n', '\\n')) ) # pylint: disable=no-self-use def do_text(self, text): """Replace widget with this text. Parameters ---------- test : str Markdown text as a string. Returns ------- None """ return Markup(markdown(text)) def get(self, text): """Get the current text. Returns ------- String of html. """ return text class Link(_HTML): """An internal link. This doesn't create a page reload. """ _TEMPLATE = 'link.jsx' _COMPONENT = 'ALink' _PACKAGE = None _ATTRS = "to={{'{link}'}}" def __init__(self, link: str = '/') -> None: """Create a button. Parameters ---------- link : str """ super().__init__() self._comp = self._tag.format( link=link ) class Div(_HTML): """Div tag.""" _TEMPLATE = 'div.jsx' _COMPONENT = 'Bowdiv' _PACKAGE = None _ATTRS = "initial={{'{initial}'}}" def __init__(self, text: str = '') -> None: """Create header text with a size. Parameters ---------- text : str Text of the header tag. """ super().__init__() self._comp = self._tag.format( initial=text ) # pylint: disable=no-self-use def do_text(self, text): """Replace widget with this text. Parameters ---------- test : str Markdown text as a string. Returns ------- None """ return text def get(self, text): """Get the current text. Returns ------- String of html. """ return text class Header(_HTML): """Header tag.""" _TEMPLATE = 'header.jsx' _COMPONENT = 'Bowhead' _PACKAGE = None _ATTRS = ("initial={{'{initial}'}} " 'size={{{size}}}') def __init__(self, text: str = '', size: int = 1) -> None: """Create header text with a size. Parameters ---------- text : str Text of the header tag. size : int Size of the header tag from 1 to 6. """ super().__init__() if size not in range(1, 7): raise ValueError('Header size must be in [1..6], found {}.'.format(size)) self._comp = self._tag.format( initial=text, size=size ) # pylint: disable=no-self-use def do_text(self, text): """Replace widget with this text. Parameters ---------- test : str Markdown text as a string. Returns ------- None """ return text def get(self, text): """Get the current text. Returns ------- String of html. """ return text PK#rJM\Ybowtie/pager.py"""Bowtie pager.""" import flask from flask_socketio import emit import eventlet from bowtie._component import SEPARATOR _NAME = 'page' + SEPARATOR class Pager: """Tell the client to send a message to the server.""" _NEXT_UUID = 0 @classmethod def _next_uuid(cls): cls._NEXT_UUID += 1 return cls._NEXT_UUID def __init__(self): """Create a pager.""" self._uuid = Pager._next_uuid() def notify(self): """Notify the client. The function passed to ``App.respond`` will get called. """ if flask.has_request_context(): emit(_NAME + str(self._uuid)) else: sio = flask.current_app.extensions['socketio'] sio.emit(_NAME + str(self._uuid)) eventlet.sleep() PKBiFM|xi%%bowtie/visual.py"""Visual components.""" from typing import Dict, Optional, List, Union, Tuple from bowtie._component import Component, jdumps, jsbool from bowtie._progress import Progress # pylint: disable=too-few-public-methods class _Visual(Component): """Abstract class all visual components inherit. Used to test if a an object is a visual component. """ # pylint: disable=abstract-method def __init__(self) -> None: self.progress = Progress() super().__init__() @property def _instantiate(self) -> str: # pylint: disable=protected-access begin, end = self.progress._tags tagwrap = begin + '{component}' + self._tagbase + end return self._insert(tagwrap, self._comp) class Table(_Visual): """Ant Design table with filtering and sorting.""" _TEMPLATE = 'table.jsx' _COMPONENT = 'AntTable' _PACKAGE = None _ATTRS = ('columns={{{columns}}} ' 'resultsPerPage={{{results_per_page}}}') def __init__(self, data=None, columns: Optional[List[Union[int, str]]] = None, results_per_page: int = 10) -> None: """Create a table and optionally initialize the data. Parameters ---------- data : pd.DataFrame, optional columns : list, optional List of column names to display. results_per_page : int, optional Number of rows on each pagination of the table. """ super().__init__() self.data = [] # type: List[Dict] self.columns = [] # type: List[Dict] if data: self.data, self.columns = self._make_data(data) elif columns: self.columns = self._make_columns(columns) self.results_per_page = results_per_page self._comp = self._tag.format( columns=self.columns, results_per_page=self.results_per_page ) @staticmethod def _make_columns(columns: List[Union[int, str]]) -> List[Dict]: """Transform list of columns into AntTable format.""" return [dict(title=str(c), dataIndex=str(c), key=str(c)) for c in columns] @staticmethod def _make_data(data) -> Tuple[List[Dict], List[Dict]]: """Transform table data into JSON.""" jsdata = [] for idx, row in data.iterrows(): row.index = row.index.astype(str) rdict = row.to_dict() rdict.update(dict(key=str(idx))) jsdata.append(rdict) return jsdata, Table._make_columns(data.columns) # pylint: disable=no-self-use def do_data(self, data): """Replace the columns and data of the table. Parameters ---------- data : pandas.DataFrame Returns ------- None """ return self._make_data(data) def do_columns(self, columns): """Update the columns of the table. Parameters ---------- columns : array-like List of strings. Returns ------- None """ return self._make_columns(columns) class SmartGrid(_Visual): """Griddle table with filtering and sorting.""" _TEMPLATE = 'griddle.jsx' _COMPONENT = 'SmartGrid' _PACKAGE = 'griddle-react@1.11.2' _ATTRS = None def __init__(self) -> None: """Create the table, optionally set the columns.""" super().__init__() self._comp = self._tag # pylint: disable=no-self-use def do_update(self, data): """Update the data of the table. Parameters ---------- data : pandas.DataFrame Returns ------- None """ return data.to_dict(orient='records') def get(self, data): """ Get the table data. Returns ------- list Each entry in the list is a dict of labels and values for a row. """ return data class SVG(_Visual): """SVG image. Mainly for matplotlib plots. """ _TEMPLATE = 'svg.jsx' _COMPONENT = 'SVG' _PACKAGE = None _ATTRS = 'preserveAspectRatio={{{preserve_aspect_ratio}}}' def __init__(self, preserve_aspect_ratio: bool = False) -> None: """Create SVG component. Parameters ---------- preserve_aspect_ratio : bool, optional If ``True`` it preserves the aspect ratio otherwise it will stretch to fill up the space available. """ super().__init__() self.preserve_aspect_ratio = preserve_aspect_ratio self._comp = self._tag.format( preserve_aspect_ratio=jsbool(self.preserve_aspect_ratio) ) # pylint: disable=no-self-use def do_image(self, image): """Replace the image. Parameters ---------- image : str Generated by ``savefig`` from matplotlib with ``format=svg``. Returns ------- None Examples -------- This shows how to update an SVG widget with Matplotlib. >>> from io import StringIO >>> import matplotlib >>> matplotlib.use('Agg') >>> import matplotlib.pyplot as plt >>> image = SVG() >>> >>> def callback(x): ... sio = StringIO() ... plt.plot(range(5)) ... plt.savefig(sio, format='svg') ... sio.seek(0) ... s = sio.read() ... idx = s.find(' None: """Create a Plotly component. Parameters ---------- init : dict, optional Initial Plotly data to plot. """ super().__init__() if init is None: init = dict(data=[], layout={'autosize': False}) self.init = init self._comp = self._tag.format( init=jdumps(self.init) ) # Events def on_click(self): """React to Plotly click event. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_click def on_beforehover(self): """Emit an event before hovering over a point. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_hover(self): """Emit an event after hovering over a point. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_unhover(self): """Emit an event when hover is removed. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_select(self): """Emit an event when points are selected with a tool. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_select def on_relayout(self): """Emit an event when the chart axes change. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_layout # Commands # pylint: disable=no-self-use def do_all(self, plot): """Replace the entire plot. Parameters ---------- plot : dict Dict that can be plotted with Plotly. It should have this structure: ``{data: [], layout: {}}``. Returns ------- None """ return plot def do_data(self, data): """Replace the data portion of the plot. Parameters ---------- data : list of traces List of data to replace the old data. Returns ------- None """ return data def do_layout(self, layout): """Update the layout. Parameters ---------- layout : dict Contains layout information. Returns ------- None """ return layout def do_config(self, config): """Update the configuration of the plot. Parameters ---------- config : dict Plotly config information. Returns ------- None """ return config def get(self, data): """Get the current selection of points. Returns ------- list """ return data def get_select(self, data): """Get the current selection of points. Returns ------- list """ return data def get_click(self, data): """Get the current selection of points. Returns ------- list """ return data def get_hover(self, data): """Get the current selection of points. Returns ------- list """ return data def get_layout(self, data): """Get the current layout. Returns ------- list """ return data PKECMJE++bowtie/feedback/__init__.py"""Feedback components from ant.design.""" PK4LM-bowtie/feedback/message.py"""Messages provide a temporary message that will disappear after a few seconds. Reference ^^^^^^^^^ https://ant.design/components/message/ """ import flask from flask_socketio import emit import eventlet from bowtie._component import pack from bowtie._utils import func_name def _message(status, content): """Send message interface. Parameters ---------- status : str The type of message content : str """ event = f'message.{status}' if flask.has_request_context(): emit(event, dict(data=pack(content))) else: sio = flask.current_app.extensions['socketio'] sio.emit(event, dict(data=pack(content))) eventlet.sleep() def success(content): """Success message. Parameters ---------- content : str Message to show user. """ _message(func_name(), content) def error(content): """Error message. Parameters ---------- content : str Message to show user. """ _message(func_name(), content) def info(content): """Info message. Parameters ---------- content : str Message to show user. """ _message(func_name(), content) def warning(content): """Warning message. Parameters ---------- content : str Message to show user. """ _message(func_name(), content) def loading(content): """Load message. Parameters ---------- content : str Message to show user. """ _message(func_name(), content) PKECM^~bowtie/src/button.jsximport PropTypes from 'prop-types'; import React from 'react'; import { Button } from 'antd'; export default class SimpleButton extends React.Component { constructor(props) { super(props); } handleClick = event => { this.props.socket.emit(this.props.uuid + '#click'); } render() { return ( ); } } SimpleButton.propTypes = { label: PropTypes.string.isRequired, uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired }; PKECM3__bowtie/src/datagrid.jsximport 'react-datagrid/index.css'; import PropTypes from 'prop-types'; import React from 'react'; import DataGrid from 'react-datagrid'; export default class Table extends React.Component { render() { var data = [ { id: '1', firstName: 'John', lastName: 'Bobson'}, { id: '2', firstName: 'Bob', lastName: 'Mclaren'}, { id: '3', firstName: 'Bob', lastName: 'Mclaren'}, { id: '4', firstName: 'Bob', lastName: 'Mclaren'}, { id: '5', firstName: 'Bob', lastName: 'Mclaren'}, { id: '6', firstName: 'Bob', lastName: 'Mclaren'}, { id: '7', firstName: 'Bob', lastName: 'Mclaren'}, { id: '8', firstName: 'Bob', lastName: 'Mclaren'}, { id: '9', firstName: 'Bob', lastName: 'Mclaren'}, { id: '10', firstName: 'Bob', lastName: 'Mclaren'}, { id: '11', firstName: 'Bob', lastName: 'Mclaren'}, { id: '12', firstName: 'Bob', lastName: 'Mclaren'}, { id: '13', firstName: 'Bob', lastName: 'Mclaren'}, { id: '14', firstName: 'Bob', lastName: 'Mclaren'}, { id: '15', firstName: 'Bob', lastName: 'Mclaren'}, { id: '16', firstName: 'Bob', lastName: 'Mclaren'}, { id: '17', firstName: 'Bob', lastName: 'Mclaren'}, { id: '18', firstName: 'Bob', lastName: 'Mclaren'}, { id: '19', firstName: 'Bob', lastName: 'Mclaren'}, { id: '20', firstName: 'Bob', lastName: 'Mclaren'}, { id: '21', firstName: 'Bob', lastName: 'Mclaren'}, { id: '22', firstName: 'Bob', lastName: 'Mclaren'}, { id: '23', firstName: 'Bob', lastName: 'Mclaren'}, { id: '24', firstName: 'Bob', lastName: 'Mclaren'} ]; var columns = [ { name: 'firstName'}, { name: 'lastName'} ]; return ( ); } } Table.propTypes = { uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired }; PKBiFM*~ bowtie/src/date.jsximport PropTypes from 'prop-types'; import React from 'react'; import { DatePicker, LocaleProvider } from 'antd'; import enUS from 'antd/lib/locale-provider/en_US'; const { MonthPicker, RangePicker } = DatePicker; import { storeState } from './utils'; import moment from 'moment'; var msgpack = require('msgpack-lite'); export default class PickDates extends React.Component { constructor(props) { super(props); var local = sessionStorage.getItem(this.props.uuid); if (local === null) { this.state = {value: null, datestring: null}; } else { local = JSON.parse(local); if (Array.isArray(local.value)) { local.value = local.value.map(x => moment(x)); } else { local.value = moment(local.value); } this.state = local; } } handleChange = (mom, ds) => { this.setState({value: mom, datestring: ds}); storeState(this.props.uuid, this.state, {value: mom, datestring: ds}); this.props.socket.emit(this.props.uuid + '#change', msgpack.encode(ds)); } componentDidMount() { var socket = this.props.socket; var uuid = this.props.uuid; socket.on(uuid + '#get', this.getValue); } getValue = (data, fn) => { fn(msgpack.encode(this.state.datestring)); } render () { if (this.props.date) { return ( ); } else if (this.props.month) { return ( ); } else { return ( ); } } } PickDates.propTypes = { uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired, date: PropTypes.bool.isRequired, month: PropTypes.bool.isRequired, range: PropTypes.bool.isRequired, }; PKECMDwbowtie/src/div.jsximport PropTypes from 'prop-types'; import React from 'react'; import { storeState } from './utils'; var msgpack = require('msgpack-lite'); export default class Bowdiv extends React.Component { constructor(props) { super(props); var local = sessionStorage.getItem(this.props.uuid); if (local === null) { this.state = {value: this.props.initial}; } else { this.state = JSON.parse(local); } } getValue = (data, fn) => { fn(msgpack.encode(this.state.value)); } setText = data => { var arr = new Uint8Array(data['data']); arr = msgpack.decode(arr); this.setState({value: arr}); storeState(this.props.uuid, this.state, {value: arr}); } componentDidMount() { var socket = this.props.socket; var uuid = this.props.uuid; socket.on(uuid + '#get', this.getText); socket.on(uuid + '#text', this.setText); } render () { return
; } } Bowdiv.propTypes = { uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired, initial: PropTypes.string.isRequired }; PKECM3GGbowtie/src/dropdown.jsximport PropTypes from 'prop-types'; import React from 'react'; import Select from 'react-select'; // Be sure to include styles at some point, probably during your bootstrapping import 'react-select/dist/react-select.css'; import { storeState } from './utils'; var msgpack = require('msgpack-lite'); export default class Dropdown extends React.Component { constructor(props) { super(props); var local = sessionStorage.getItem(this.props.uuid); if (local === null) { this.state = {value: this.props.default, options: this.props.initOptions}; } else { this.state = JSON.parse(local); } } handleChange = value => { this.setState({value: value}); this.props.socket.emit(this.props.uuid + '#change', msgpack.encode(value)); storeState(this.props.uuid, this.state, {value: value}); } choose = data => { var arr = new Uint8Array(data['data']); this.setState({value: arr}); storeState(this.props.uuid, this.state, {value: arr}); } newOptions = data => { var arr = new Uint8Array(data['data']); this.setState({value: null, options: msgpack.decode(arr)}); storeState(this.props.uuid, this.state, {value: null, options: msgpack.decode(arr)}); } componentDidMount() { var socket = this.props.socket; var uuid = this.props.uuid; socket.on(uuid + '#get', this.getValue); socket.on(uuid + '#options', this.newOptions); socket.on(uuid + '#choose', this.choose); } getValue = (data, fn) => { fn(msgpack.encode(this.state.value)); } render () { return ( ); } } } Textbox.propTypes = { uuid: PropTypes.string.isRequired, type: PropTypes.string.isRequired, autosize: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired, socket: PropTypes.object.isRequired, placeholder: PropTypes.string.isRequired, size: PropTypes.string.isRequired, }; PKChJMջp]@@bowtie/src/upload.jsximport React from 'react'; import PropTypes from 'prop-types'; import { Upload, Icon, LocaleProvider } from 'antd'; import enUS from 'antd/lib/locale-provider/en_US'; const Dragger = Upload.Dragger; export default class AntUpload extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return (

Click or drag file to this area to upload

Support for a single or bulk upload.

); } } AntUpload.propTypes = { uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired, multiple: PropTypes.bool.isRequired, }; PKBiFMbowtie/src/utils.jsexport const storeState = (uuid, state, data) => { sessionStorage.setItem(uuid, JSON.stringify(Object.assign(state, data))); }; export const str2ints = x => { return x.split(',').map(y => parseInt(y, 10)); }; PKBiFMH8+d bowtie/src/view.jsximport 'normalize.css'; import React from 'react'; import PropTypes from 'prop-types'; import { components } from './components'; import { str2ints } from './utils'; export class View extends React.Component { constructor(props) { super(props); this.state = { columns: this.props.columns, rows: this.props.rows, column_gap: this.props.column_gap, row_gap: this.props.row_gap, spans: this.props.spans, controllers: this.props.controllers, sidebar: this.props.sidebar, }; } render() { var widgets = []; const controls = this.state.controllers.map(index =>
{components[index]}
); for (const key in this.state.spans) { if (this.state.spans.hasOwnProperty(key)) { const comps = this.state.spans[key].map((number) =>
{components[number]}
); const rowcols = str2ints(key); widgets.push(
{comps}
); } } return (
{this.state.sidebar &&
{controls}
} {widgets}
); } } View.propTypes = { uuid: PropTypes.string.isRequired, background_color: PropTypes.string.isRequired, spans: PropTypes.object.isRequired, controllers: PropTypes.arrayOf(PropTypes.number).isRequired, columns: PropTypes.string.isRequired, rows: PropTypes.string.isRequired, column_gap: PropTypes.string.isRequired, row_gap: PropTypes.string.isRequired, border: PropTypes.string.isRequired, sidebar: PropTypes.bool.isRequired, }; PKECM bowtie/src/webpack.dev.jsconst merge = require('webpack-merge'); const common = require('./webpack.common.js'); var config = { mode: 'development' }; module.exports = merge(common, config); PKUiGM4|ssbowtie/src/webpack.prod.jsconst merge = require('webpack-merge'); const common = require('./webpack.common.js'); var CompressionPlugin = require('compression-webpack-plugin'); var config = { mode: 'production', plugins: [ new CompressionPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', }) ] }; module.exports = merge(common, config); PK4LM^bowtie/templates/bowtie.html {{ title }}
PKBiFMGא!bowtie/templates/components.js.j2import React from 'react'; import io from 'socket.io-client'; import AntProgress from './progress'; {% for import in imports %} import {{ import.component }} from './{{ import.module }}'; {% endfor %} export const socket = io({path: '/{{ socketio }}socket.io'}); export const components = { {% for component in components %} {{ component._uuid }}: {{ component._instantiate }}, {% endfor %} }; PKBiFManppbowtie/templates/index.jsx.j2import 'normalize.css'; import React from 'react'; import ReactDOM from 'react-dom'; import { message } from 'antd'; import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'; import { View } from './view'; import { socket } from './components'; var msgpack = require('msgpack-lite'); class Dashboard extends React.Component { constructor(props) { super(props); this.cache = {}; socket.emit('INITIALIZE'); } saveValue = data => { var arr = new Uint8Array(data['key']); var key = msgpack.decode(arr); var value = new Uint8Array(data['data']).toString(); sessionStorage.setItem('cache' + key, value); } loadValue = (data, fn) => { var arr = new Uint8Array(data['data']); var key = msgpack.decode(arr); var x = sessionStorage.getItem('cache' + key) if (x === null) { var buffer = new ArrayBuffer(1); var x = new DataView(buffer, 0); // msgpack encodes null to 0xc0 x.setUint8(0, 0xc0); fn(buffer); } else { x = new Uint8Array(x.split(',')); fn(x.buffer); } } componentDidMount() { socket.on('cache_save', this.saveValue); socket.on('cache_load', this.loadValue); {% for page in pages %} socket.on('page#{{ page._uuid }}', function () {socket.emit('resp#{{ page._uuid }}')}); {% endfor %} socket.on('message.success', (data) => { var arr = new Uint8Array(data['data']); message.success(msgpack.decode(arr)); }); socket.on('message.error', (data) => { var arr = new Uint8Array(data['data']); message.error(msgpack.decode(arr)); }); socket.on('message.info', (data) => { var arr = new Uint8Array(data['data']); message.info(msgpack.decode(arr)); }); socket.on('message.warning', (data) => { var arr = new Uint8Array(data['data']); message.warning(msgpack.decode(arr)); }); socket.on('message.loading', (data) => { var arr = new Uint8Array(data['data']); message.loading(msgpack.decode(arr)); }); } render() { return ( {% for route in routes %} } /> {% endfor %} ); } } ReactDOM.render(, document.getElementById('app')); PK4LMܼN%bowtie/templates/webpack.common.js.j2const webpack = require('webpack'); var path = require('path'); var CompressionPlugin = require('compression-webpack-plugin'); var BUILD_DIR = path.resolve(__dirname); var APP_DIR = path.resolve(__dirname, 'bowtiejs'); var config = { context: __dirname, entry: APP_DIR + '/index.jsx', output: { path: BUILD_DIR, filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, include: APP_DIR, loader: 'babel-loader', exclude: /node_modules/, options: { presets: [ ['@babel/preset-env', {'modules': false}], '@babel/preset-react'], plugins: [ ['babel-plugin-import', {'libraryName': 'antd', 'style': true}], '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties', ], babelrc: false } }, { test: /\.scss$/, loaders: ['style-loader', 'css-loader', 'sass-loader'], }, { test: /\.css$/, loader: 'style-loader!css-loader!sass-loader', }, { test: /\.less$/, use: [ {loader: "style-loader"}, {loader: "css-loader"}, {loader: "less-loader", options: { strictMath: false, javascriptEnabled: true, noIeCompat: true, {% if color %} modifyVars: {"primary-color": "{{ color }}"}, {% endif %} root: path.resolve(__dirname, './') } } ] }, ], noParse: [ /plotly\.js$/ ], }, resolve: { extensions: ['.jsx', '.js', '.json'], modules: [ path.resolve(__dirname, APP_DIR), 'node_modules' ] } }; module.exports = config; PKECMBxbowtie/tests/__init__.py"""Test directory.""" PK4LMveebowtie/tests/conftest.py#!/usr/bin/env python """Pytest configuration.""" from sys import platform import socket import shutil from pathlib import Path from selenium import webdriver import pytest from bowtie import View from bowtie._app import _DIRECTORY @pytest.fixture def build_reset(): """Path for building apps with pytest.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex(('127.0.0.1', 9991)) if result == 0: raise Exception('Port 9991 is unavailable, aborting test.') # pylint: disable=protected-access View._NEXT_UUID = 0 yield shutil.rmtree(Path(__file__).parent / _DIRECTORY) @pytest.fixture def chrome_driver(): """Set up chrome driver.""" options = webdriver.ChromeOptions() if platform == 'darwin': options.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' else: options.binary_location = '/usr/bin/google-chrome-stable' options.add_argument('headless') options.add_argument('window-size=1200x800') driver = webdriver.Chrome(options=options) yield driver driver.quit() PK4LMd߿bowtie/tests/test_app.py"""App and View testing.""" import pytest from bowtie import App def test_subscribe_error(): """Subscribe with incorrect argument order.""" app = App() with pytest.raises(IndexError): app.subscribe() PK4LMfbowtie/tests/test_cache.py"""Test markdown and text widgets.""" # pylint: disable=unused-argument,redefined-outer-name,invalid-name import time import pytest from bowtie import App, cache from bowtie.control import Button from bowtie.tests.utils import reset_uuid, server_check reset_uuid() button = Button() def click(): """Save and load cache.""" cache['a'] = 3 assert cache[b'a'] == 3 assert cache[u'a'] == 3 cache[b'b'] = True assert cache[b'b'] assert cache[u'b'] def test_keys(): """Test invalid keys.""" with pytest.raises(KeyError): cache[0] = 0 with pytest.raises(KeyError): cache[True] = 0 with pytest.raises(KeyError): cache[3, 4] = 0 @pytest.fixture def dummy(build_reset, monkeypatch): """Create basic app.""" app = App(__name__) app.add(button) app.subscribe(button.on_click)(click) app._build() # pylint: disable=protected-access with server_check(app) as server: yield server def test_cache(dummy, chrome_driver): """Test cache works.""" chrome_driver.get('http://localhost:9991') chrome_driver.implicitly_wait(5) assert chrome_driver.title == 'Bowtie App' btx = chrome_driver.find_element_by_class_name('ant-btn') btx.click() time.sleep(2) PKECMbowtie/tests/test_command.py#!/usr/bin/env python """Command testing.""" from bowtie._command import numargs def test_numargs(): """Numargs testing.""" # pylint: disable=missing-docstring def onearg(x): return x def zeroarg(): pass def varargs(*y): return y def onevarargs(x, *y): return x, y assert numargs(onearg) == 1 assert numargs(onevarargs) == 2 assert numargs(zeroarg) == 0 assert numargs(varargs) == 1 PK4LMlvvbowtie/tests/test_compile.py#!/usr/bin/env python """Compile tests.""" from bowtie import App from bowtie.control import Nouislider from bowtie.visual import Plotly from bowtie.tests.utils import reset_uuid def callback(*args): """dummy function""" # pylint: disable=unused-argument pass # pylint: disable=unused-argument def test_build(build_reset, monkeypatch): """Tests the build process.""" reset_uuid() ctrl = Nouislider() viz = Plotly() app = App(__name__, sidebar=True) app.add_sidebar(ctrl) app.add(viz) app.subscribe(ctrl.on_change)(callback) # pylint: disable=protected-access app._build() PK4LMѸ bowtie/tests/test_components.py"""Test all components for instatiation issues.""" # pylint: disable=unused-argument,redefined-outer-name from inspect import isclass import pytest from bowtie import App from bowtie import control, visual, html from bowtie._component import COMPONENT_REGISTRY from bowtie.tests.utils import reset_uuid, server_check def create_components(): """Create components for this test.""" reset_uuid() # pylint: disable=protected-access controllers = [] for compstr in dir(control): comp = getattr(control, compstr) if (compstr[0] != '_' and isclass(comp) and issubclass(comp, control._Controller) and compstr != 'Upload'): controllers.append(comp()) for controller in controllers: assert COMPONENT_REGISTRY[controller._uuid] == controller visuals = [] for compstr in dir(visual): comp = getattr(visual, compstr) if compstr[0] != '_' and isclass(comp) and issubclass(comp, visual._Visual): visuals.append(comp()) for vis in visuals: assert COMPONENT_REGISTRY[vis._uuid] == vis htmls = [] for compstr in dir(html): comp = getattr(html, compstr) if compstr[0] != '_' and isclass(comp) and issubclass(comp, html._HTML): htmls.append(comp()) for htm in htmls: assert COMPONENT_REGISTRY[htm._uuid] == htm return controllers, visuals, htmls create_components() @pytest.fixture def components(build_reset, monkeypatch): """App with all components.""" controllers, visuals, htmls = create_components() app = App(__name__, rows=len(visuals), sidebar=True) for controller in controllers: # pylint: disable=protected-access assert COMPONENT_REGISTRY[controller._uuid] == controller app.add_sidebar(controller) for vis in visuals: # pylint: disable=protected-access assert COMPONENT_REGISTRY[vis._uuid] == vis app.add(vis) for htm in htmls: # pylint: disable=protected-access assert COMPONENT_REGISTRY[htm._uuid] == htm app.add_sidebar(htm) assert len(COMPONENT_REGISTRY) == len(controllers) + 2 * len(visuals) + len(htmls) # pylint: disable=protected-access app._build() # run second time to make sure nothing weird happens with subsequent builds app._build() with server_check(app) as server: yield server def test_components(components, chrome_driver): """Test that no components cause an error.""" chrome_driver.get('http://localhost:9991') chrome_driver.implicitly_wait(5) logs = chrome_driver.get_log('browser') for log in logs: if log['level'] == 'SEVERE': raise Exception(log['message']) PK4LMN"d33bowtie/tests/test_editor.py"""Test markdown and text widgets.""" # pylint: disable=redefined-outer-name,unused-argument,invalid-name import time import pytest from bowtie import App from bowtie.html import Markdown from bowtie.control import Textbox from bowtie.tests.utils import reset_uuid, server_check reset_uuid() mark = Markdown(''' # top ## middle [link]('hello.html') ''') side = Markdown(''' # sideheader ''') text = Textbox(area=True) def write(txt): """Update markdown text.""" mark.do_text(txt) @pytest.fixture def markdown(build_reset, monkeypatch): """Create markdown and text widgets.""" app = App(__name__, sidebar=True) app.add(mark) app.add_sidebar(side) app.add_sidebar(text) app.subscribe(text.on_change)(write) # pylint: disable=protected-access app._build() with server_check(app) as server: yield server def test_markdown(markdown, chrome_driver): """Test markdown and text widgets.""" chrome_driver.get('http://localhost:9991') chrome_driver.implicitly_wait(5) assert chrome_driver.title == 'Bowtie App' txtctrl = chrome_driver.find_element_by_class_name('ant-input') output = chrome_driver.find_element_by_xpath( "//div[@style='grid-area: 1 / 2 / 2 / 3; position: relative;']" ) assert 'top' in output.text assert 'middle' in output.text assert 'link' in output.text txtctrl.send_keys('apple') time.sleep(1) assert 'apple' in output.text txtctrl.send_keys('banana') time.sleep(1) assert 'apple' in output.text assert 'banana' in output.text PKBiFM8Tbowtie/tests/test_layout.py"""Test layout functionality.""" # pylint: disable=redefined-outer-name,protected-access import pytest from bowtie import App from bowtie.control import Button from bowtie.exceptions import GridIndexError, NoUnusedCellsError, SpanOverlapError def check_all_cells_used(view): """Check if all cells are used.""" with pytest.raises(NoUnusedCellsError): view._available_cell() def count_used_cells(view): """Count number of used cells.""" return sum(len(list(x.cells)) for x in view._spans.keys()) @pytest.fixture(scope='module') def buttons(): """Four buttons.""" return [Button() for _ in range(4)] def test_add_list(buttons): """Append button to existing cell.""" app = App() app[0, 0] = buttons[0] app[0, 0] += buttons[1] def test_set_tuple(buttons): """Set tuple of components to cell.""" app = App() app[0, 0] = buttons[0], buttons[1] def test_set_list(buttons): """Set list of components to cell.""" app = App() app[0, 0] = [buttons[0], buttons[1]] def test_append_no_init(buttons): """Append button to cell without component.""" app = App() with pytest.raises(KeyError): app[0, 0] += buttons[0] def test_append_to_partial(buttons): """Append button to partial cell.""" app = App(columns=2) app[0] = buttons[0] with pytest.raises(KeyError): app[0, 0] += buttons[1] with pytest.raises(SpanOverlapError): app[0, 0] = buttons[1] def test_append_to_partial_superset(buttons): """Append button to partial cell.""" app = App(columns=2) app[0, 0] = buttons[0] with pytest.raises(Exception): app[0] += buttons[1] def test_all_used(buttons): """Test all cells are used.""" app = App(rows=2, columns=2) for i in range(4): app.add(buttons[i]) check_all_cells_used(app._root) app = App(rows=2, columns=2) app[0, 0] = buttons[0] app[0, 1] = buttons[1] app[1, 0] = buttons[2] app[1, 1] = buttons[3] check_all_cells_used(app._root) app.add(buttons[2]) assert len(app[1, 1]) == 2 app = App(rows=2, columns=2) app[0] = buttons[0] app[1, 0] = buttons[2] app[1, 1] = buttons[3] check_all_cells_used(app._root) app.add(buttons[2]) assert len(app[1, 1]) == 2 def test_used(buttons): """Test cell usage checks.""" app = App(rows=2, columns=2) for i in range(3): app.add(buttons[i]) app[0, 0] = buttons[3] app[0:1, 1] = buttons[3] app[1, 0:1] = buttons[3] app[1, 1] = buttons[3] def test_grid_index(buttons): """Test grid indexing checks.""" app = App(rows=2, columns=2) with pytest.raises(GridIndexError): app[-5] = buttons[0] app[-1] = buttons[0] with pytest.raises(GridIndexError): app[2] = buttons[0] app[1] = buttons[0] def test_getitem(buttons): """Test grid indexing checks.""" but = buttons[0] app = App(rows=2, columns=2) with pytest.raises(GridIndexError): app[3] = but with pytest.raises(GridIndexError): app[1, 2, 3] = but with pytest.raises(GridIndexError): # pylint: disable=invalid-slice-index app['a':3] = but with pytest.raises(GridIndexError): app['a'] = but with pytest.raises(GridIndexError): app[3, 'a'] = but with pytest.raises(GridIndexError): app['a', 3] = but with pytest.raises(GridIndexError): app[0, 0::2] = but with pytest.raises(GridIndexError): app[0, 1:-1:-1] = but app[1, ] = but assert count_used_cells(app._root) == 2 app[0, :] = but assert count_used_cells(app._root) == 4 app = App(rows=2, columns=2) app[0:1, 1:2] = but assert count_used_cells(app._root) == 1 app[1:, 0:] = but assert count_used_cells(app._root) == 3 app = App(rows=2, columns=2) app[-1, :2] = but assert count_used_cells(app._root) == 2 app = App(rows=1, columns=2) app[0, :2] = but assert count_used_cells(app._root) == 2 app = App(rows=1, columns=2) app[0] = but assert count_used_cells(app._root) == 2 app = App(rows=2, columns=2) app[:2] = but assert count_used_cells(app._root) == 4 PK4LMO  bowtie/tests/test_multiple.py"""Multiple views testing.""" # pylint: disable=unused-argument,redefined-outer-name,invalid-name from time import sleep from numpy import random as rng import pandas as pd import pytest from bowtie import App, View from bowtie.control import Nouislider, Button from bowtie.visual import Table from bowtie.tests.utils import reset_uuid, server_check reset_uuid() table = Table() ctrl = Nouislider() ctrl2 = Button() def callback(*args): """dummy function""" df = pd.DataFrame(rng.randn(10, 10)) table.do_data(df) @pytest.fixture def multiple_views(build_reset, monkeypatch): """Create multiple views app.""" app = App(__name__, sidebar=True) view1 = View() # pylint: disable=unused-variable assert view1._uuid == 2 # pylint: disable=protected-access view2 = View() view2.add(table) app.add_route(view2, 'view2') app.add(table) app.add_sidebar(ctrl) app.add_sidebar(ctrl2) app.subscribe(ctrl.on_change)(app.subscribe(ctrl2.on_click)(callback)) app._build() # pylint: disable=protected-access with server_check(app) as server: yield server # pylint: disable=redefined-outer-name,unused-argument def test_multiple(multiple_views, chrome_driver): """Test multiple views app.""" chrome_driver.get('http://localhost:9991') chrome_driver.implicitly_wait(5) assert chrome_driver.title == 'Bowtie App' button = chrome_driver.find_element_by_class_name('ant-btn') data = chrome_driver.find_element_by_class_name('ant-table-body').text assert len(data.split('\n')) == 1 button.click() sleep(2) data = chrome_driver.find_element_by_class_name('ant-table-body').text assert len(data.split('\n')) == 20 logs = chrome_driver.get_log('browser') for log in logs: if log['level'] == 'SEVERE': raise Exception(log['message']) chrome_driver.get('http://localhost:9991/view2') data = chrome_driver.find_element_by_class_name('ant-table-body').text assert len(data.split('\n')) == 20 chrome_driver.implicitly_wait(5) logs = chrome_driver.get_log('browser') for log in logs: if log['level'] == 'SEVERE': raise Exception(log['message']) chrome_driver.get('http://localhost:9991/view1') assert chrome_driver.title == '404 Not Found' PK4LMettbowtie/tests/test_plotly.py"""Plotly testing.""" # pylint: disable=redefined-outer-name,unused-argument,invalid-name import pytest from plotly import graph_objs as go from bowtie import App from bowtie.control import Nouislider, Button from bowtie.visual import Plotly from bowtie.tests.utils import reset_uuid, server_check reset_uuid() viz = Plotly() ctrl = Nouislider() ctrl_range = Nouislider(start=(20, 200)) ctrl2 = Button() def callback(*args): """dummy function""" chart = go.Figure() chart.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[4, 1, 3, 7])) chart.layout.title = 'hello world' viz.do_all(chart.to_dict()) @pytest.fixture def plotly(build_reset, monkeypatch): """Create plotly app.""" app = App(__name__, sidebar=True) app.add(viz) app.add_sidebar(ctrl) app.add_sidebar(ctrl_range) app.add_sidebar(ctrl2) app.subscribe(ctrl.on_change)(app.subscribe(ctrl2.on_click)(callback)) # pylint: disable=protected-access app._build() with server_check(app) as server: yield server def test_plotly(plotly, chrome_driver): """Test plotly component.""" chrome_driver.get('http://localhost:9991') chrome_driver.implicitly_wait(5) assert chrome_driver.title == 'Bowtie App' button = chrome_driver.find_element_by_class_name('ant-btn') points = chrome_driver.find_elements_by_class_name('point') assert not points button.click() points = chrome_driver.find_elements_by_class_name('point') logs = chrome_driver.get_log('browser') for log in logs: if log['level'] == 'SEVERE': raise Exception(log['message']) assert len(points) == 4 PKECMg>mbowtie/tests/test_serialize.py#!/usr/bin/env python """Serialization testing.""" import numpy as np import pandas as pd from bowtie._component import jdumps, pack NPARRAY = np.array([5, 6]) NPSCALAR = np.int32(5) DATES = pd.date_range('2017-01-01', periods=2) def test_json(): """Tests json encoding numpy and pandas.""" assert jdumps(NPARRAY) == jdumps([5, 6]) assert jdumps(NPSCALAR) == jdumps(5) assert jdumps(DATES) == jdumps(['2017-01-01T00:00:00', '2017-01-02T00:00:00']) def test_msgpack(): """Tests msgpack encoding numpy and pandas.""" assert pack(NPARRAY) == pack([5, 6]) assert pack(NPSCALAR) == pack(5) assert pack(DATES) == pack(['2017-01-01T00:00:00', '2017-01-02T00:00:00']) PKBiFMF2|aabowtie/tests/test_tags.py"""Test tag instantation for components.""" from bowtie.html import Markdown def test_markdown(): """Test tags for the Markdown widget.""" # pylint: disable=protected-access next_uuid = Markdown._NEXT_UUID mark = Markdown() assert mark._instantiate == ( f"" ) next_uuid = Markdown._NEXT_UUID mark = Markdown(initial='#hi\n##hello') assert mark._instantiate == ( "hi\\n

hello

'} " f"socket={{socket}} uuid={{'{next_uuid + 1}'}} />" ) PKBiFM8bowtie/tests/test_utils.py"""Compat testing.""" from bowtie._utils import func_name def hello(): """A function.""" return func_name() def test_function_names(): """Test we get the correct function name.""" assert hello() == 'hello' PK4LMDy}}bowtie/tests/utils.py"""Utility functions for testing only.""" from contextlib import contextmanager from multiprocessing import Process import time from bowtie import View from bowtie._component import Component def reset_uuid(): """Reset the uuid counter for components.""" # pylint: disable=protected-access Component._NEXT_UUID = 0 View._NEXT_UUID = 0 @contextmanager def server_check(app): """Context manager for testing Bowtie apps and verifying no errors happened.""" process = Process(target=app._serve) # pylint: disable=protected-access process.start() time.sleep(5) yield process process.terminate() PKECMd~q77bowtie-0.11.0.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2016 Jacques Kvam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H>*RQbowtie-0.11.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UrPK!H.! bowtie-0.11.0.dist-info/METADATAX]s۸}ǯ@Ӿ4$$M&qu2kowH$,dv\eَi5HyE: 'KcnYND#FF(!r$)4t\, $/*N>|}RNyPp0 7]͔ٟ3l[qѥMڒm#f?XB~X'CpV|{Q.Znm\"o/T{!mZ\/[+ cKgĔǭn%c:Qs~fG:Xnd3+6?νb]%/ƪ|i:Oi]֒06ѣK@M${H cކ^9?KW7=\/EZ<xt.3b,yuf8kot69lt+Uj:KAyM l09'4zɖ2/A(sqXS_O!/bKRƀPܰ!m]=pyH>pP%_>sx=pV%O/OfR4гc^tς[)[d *eٌ2{wrNFXxB% y5>Y%9׽rfe8d*5x_*63t6f塞eiyʜIQ$-t|df~)lAߴ~ea!@Azޒܩ!ز^ -g]$oa6nS7/)|>M#Z B|LvFmfD@IVoNl!f~#ʍm+^ޣ"IN0 gB׵^ٸѷ at8>OPx a}=Qz!țj|cn-`%S SJБokYj3(+"Jr N;$9|xC9"ըk2*@)O'*I7b.C VFD}QcH.#qV%Pj1j8 ?z`|*91&֩ѳQ>vh'Cf|6ͱъ ~Z@SS=L^&$wƦɨHNQdzז>T vF^R @k Pl茬0SHJCUs 䡗Zhdfc!ü$Tv&X y'mݪ)c[)Y>P~ߟFֽ߁^@w, zu^ ,r`qtǡAf޼mB(S޸eBaz EUX w$x7oZw-.z~P0ֺv~i Sv&l2׈%='7Oq-':>ҚW; eE!+JD71inY\.)Ž;$h:ݜ%z9ll S`PK!H J bowtie-0.11.0.dist-info/RECORDuɖXIqT" T5H)l*xNdU͵ _{^~.>Q?) ,'_gQ? DҏBuhī#PxONӁsl%6y顿os 30ǘ/NY!IfNsOo`ܤTП+D7*6(.-\nj<*0"eNLtM !-mST^T0t듈Ŷg"'f7R!0B|gXinInZnI^ŠXbCqRW{O͖NQa]vqoXWKSǵOqLРH聺\ĭPX`i:ݐTZ܂1yȧ`Ϭ57㇣Hv?4F9N1 )Ӕ!^RrČFJrWWdJd yr[9gT"Gȷ9Bm{,æ]SnH2pWH"WCj5GZ֮2ahDs omvnTT5lz<^U@;!?Y1풡t(ڣP2&BeY?"cЯ"~wyFͱBv^Yצ:=]DbO̘g`5ּWRaTb+;ĐOPQO{?;nO8qaτ ;K0icul7' p䳭Y24AܡB`_¿Oʮ,`:2 _Wj0+xD/I[X?h3ӺDAOy/rFoH->HgA+V"ˋ,hkl:t"Ң/<#Uӛt%bc%Oa򍕏[K[ꑹ 'BvU7]5 &bz!Yx`wPoifzoZy mu M,lz!6u10[Gɢ-SynȚp}.fj;-.hÆf'BNKb!Oig^]Eu17-סj`N'$mC|[ZCkԼoh0}8(Tz A?~n`wV+P.oPB@#K̅w_~@>ⓦ"PT#M\_o'R>R0H|U+%B %ߢ  \d!sWdI58u۬cEe_ˆO@<`Y˩GZ#J顟rf[L4w쟦yQir`U~^ 0FTaMxA?]{If6@pF#~`V[@688Y=v4!1QG xaowK` /ρ\=uꀝCwC:+e|m>"&-RrAf/Ę[kOQz]01ېf3Ŋ0h(|r{|hW\N:y tN?%+5jcĘYy:j=VOw|S`ZQHxkʸ'nR8]ĺ(G9h]zpћSVK?R5 oߊCJ贈,УıWT~ 3;:oP־Q}p>U/ѭKjxzO>r_Púz=rzFf/l2ay񜖦wSbymbowtie/tests/test_serialize.pyPKBiFMF2|aaުbowtie/tests/test_tags.pyPKBiFM8vbowtie/tests/test_utils.pyPK4LMDy}}bowtie/tests/utils.pyPKECMd~q77Abowtie-0.11.0.dist-info/LICENSEPK!H>*RQbowtie-0.11.0.dist-info/WHEELPK!H.! Bbowtie-0.11.0.dist-info/METADATAPK!H J Dbowtie-0.11.0.dist-info/RECORDPK@@7