PKKLpdbowtie/__init__.py"""Interactive Dashboard Toolkit.""" __version__ = '0.8.1' from bowtie._app import App, View from bowtie._command import command from bowtie.pager import Pager from bowtie._cache import cache PKIL^mǑdidibowtie/_app.py# -*- coding: utf-8 -*- """Defines the App class.""" from __future__ import print_function import os import json from itertools import product import inspect import shutil import stat from collections import namedtuple, defaultdict, OrderedDict from subprocess import Popen import warnings from jinja2 import Environment, FileSystemLoader from bowtie._compat import makedirs from bowtie._component import Component, SEPARATOR from bowtie.exceptions import ( GridIndexError, MissingRowOrColumn, NoSidebarError, NotStatefulEvent, NoUnusedCellsError, SizeError, UsedCellsError, WebpackError, YarnError ) _Import = namedtuple('_Import', ['module', 'component']) _Control = namedtuple('_Control', ['instantiate', 'caption']) _Schedule = namedtuple('_Schedule', ['seconds', 'function']) _DIRECTORY = 'build' _WEBPACK = './node_modules/.bin/webpack' def raise_not_number(x): """Raise ``SizeError`` if ``x`` is not a number``.""" try: float(x) except ValueError: raise SizeError('Must pass a number, received {}'.format(x)) class Span(object): """Define the location of a widget.""" # pylint: disable=too-few-public-methods def __init__(self, row_start, column_start, row_end=None, column_end=None): """Create a span for a widget. Indexing starts at 0. Both start and end are inclusive. Parameters ---------- row_start : int column_start : int row_end : int, optional column_end : int, optional """ self.row_start = row_start + 1 self.column_start = column_start + 1 # 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 + 1 if column_end is None: self.column_end = self.column_start + 1 else: self.column_end = column_end + 1 def __repr__(self): """Show the starting and ending points.""" return '({}, {}) to ({}, {})'.format( self.row_start, self.column_start, self.row_end, self.column_end ) class Size(object): """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) >>> app.rows[1].fraction(2) """ def __init__(self): """Create a default row or column size with fraction = 1.""" self.minimum = None self.maximum = None self.fraction(1) def auto(self): """Set the size to auto or content based.""" self.maximum = 'auto' def min_auto(self): """Set the minimum size to auto or content based.""" self.minimum = 'auto' def pixels(self, value): """Set the size in pixels.""" raise_not_number(value) self.maximum = '{}px'.format(value) def min_pixels(self, value): """Set the minimum size in pixels.""" raise_not_number(value) self.minimum = '{}px'.format(value) def fraction(self, value): """Set the fraction of free space to use as an integer.""" raise_not_number(value) self.maximum = '{}fr'.format(int(value)) def percent(self, value): """Set the percentage of free space to use.""" raise_not_number(value) self.maximum = '{}%'.format(value) def min_percent(self, value): """Set the minimum percentage of free space to use.""" raise_not_number(value) self.minimum = '{}%'.format(value) def __repr__(self): """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(object): """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) """ def __init__(self): """Create a default margin of zero.""" self.gap = None self.pixels(0) def pixels(self, value): """Set the margin in pixels.""" raise_not_number(value) self.gap = '{}px'.format(value) def percent(self, value): """Set the margin as a percentage.""" raise_not_number(value) self.gap = '{}%'.format(value) def __repr__(self): """Represent the margin to be inserted into a JSX template.""" return self.gap def _check_index(value, length, bound): if value is not None: 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, length): 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 View(object): """Grid of widgets.""" _NEXT_UUID = 0 @classmethod def _next_uuid(cls): cls._NEXT_UUID += 1 return cls._NEXT_UUID def __init__(self, rows=1, columns=1, sidebar=True, background_color='White'): """Create a new grid.""" self._uuid = View._next_uuid() self.used = OrderedDict(((key, False) for key in product(range(rows), range(columns)))) self.column_gap = Gap() self.row_gap = Gap() self.rows = [Size() for _ in range(rows)] self.columns = [Size() for _ in range(columns)] self.sidebar = sidebar self.background_color = background_color self.packages = set() self.templates = set() self.imports = set() self.controllers = [] self.widgets = [] self.spans = [] @property def _name(self): return 'view{}.jsx'.format(self._uuid) def __getitem__(self, key): """Get item from the view.""" raise NotImplementedError('Accessor is not implemented.') def __setitem__(self, key, widget): """Add widget to the view.""" if isinstance(key, tuple): if len(key) == 1: self[key[0]] = widget else: 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 = row_key row_end = None elif isinstance(row_key, slice): row_start, row_end = _slice_to_start_end(row_key, len(self.rows)) else: raise GridIndexError( 'Cannot index with {}, pass in a int or a slice.'.format(row_key) ) if isinstance(column_key, int): column_start = column_key column_end = None elif isinstance(column_key, slice): column_start, column_end = _slice_to_start_end(column_key, len(self.columns)) else: raise GridIndexError( 'Cannot index with {}, pass in a int or a slice.'.format(column_key) ) self._add( widget, row_start=row_start, column_start=column_start, row_end=row_end, column_end=column_end ) elif isinstance(key, slice): start, end = _slice_to_start_end(key, len(self.rows)) self._add( widget, row_start=start, column_start=0, row_end=end, column_end=len(self.columns) ) elif isinstance(key, int): self._add(widget, row_start=key, column_start=0, column_end=len(self.columns)) else: raise GridIndexError('Invalid index {}'.format(key)) def add(self, widget): """Add a widget to the grid in the next available cell. Searches over columns then rows for available cells. Parameters ---------- widget : bowtie._Component A Bowtie widget instance. """ self._add(widget) def _add(self, widget, row_start=None, column_start=None, row_end=None, column_end=None): """Add a widget to the grid. Zero-based index and exclusive. Parameters ---------- widget : bowtie._Component A Bowtie widget instance. row_start : int, optional Starting row for the widget. column_start : int, optional Starting column for the widget. row_end : int, optional Ending row for the widget. column_end : int, optional Ending column for the widget. """ assert isinstance(widget, Component) row_start = _check_index(row_start, len(self.rows), False) column_start = _check_index(column_start, len(self.columns), False) row_end = _check_index(row_end, len(self.rows), True) column_end = _check_index(column_end, len(self.columns), True) if row_start is not None and row_end is not None and row_start >= row_end: raise GridIndexError('row_start: {} must be less than row_end: {}' .format(row_start, row_end)) if column_start is not None and column_end is not None and column_start >= column_end: raise GridIndexError('column_start: {} must be less than column_end: {}' .format(column_start, column_end)) # pylint: disable=protected-access self.packages.add(widget._PACKAGE) self.templates.add(widget._TEMPLATE) self.imports.add(_Import(component=widget._COMPONENT, module=widget._TEMPLATE[:widget._TEMPLATE.find('.')])) if row_start is None or column_start is None: if row_start is not None: raise MissingRowOrColumn( 'Only row_start was defined. ' 'Please specify both column_start and row_start or neither.' ) if column_start is not None: raise MissingRowOrColumn( 'Only column_start was defined. ' 'Please specify both column_start and row_start or neither.' ) if row_end is not None or column_end is not None: raise MissingRowOrColumn( 'If you specify an end index you must ' 'specify both row_start and column_start.' ) row, col = None, None for (row, col), use in self.used.items(): if not use: break else: raise NoUnusedCellsError() span = Span(row, col) self.used[row, col] = True elif row_end is None and column_end is None: if self.used[row_start, column_start]: raise UsedCellsError('Cell at {}, {} is already used.' .format(row_start, column_start)) span = Span(row_start, column_start) self.used[row_start, column_start] = True else: if row_end is None: row_end = row_start + 1 if column_end is None: column_end = column_start + 1 for row, col in product(range(row_start, row_end), range(column_start, column_end)): if self.used[row, col]: raise UsedCellsError('Cell at {}, {} is already used.'.format(row, col)) for row, col in product(range(row_start, row_end), range(column_start, column_end)): self.used[row, col] = True span = Span(row_start, column_start, row_end, column_end) self.widgets.append(widget) self.spans.append(span) def add_sidebar(self, widget): """Add a widget to the sidebar. Parameters ---------- widget : bowtie._Component Add this widget 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.') assert isinstance(widget, Component) # pylint: disable=protected-access self.packages.add(widget._PACKAGE) self.templates.add(widget._TEMPLATE) self.imports.add(_Import(component=widget._COMPONENT, module=widget._TEMPLATE[:widget._TEMPLATE.find('.')])) self.controllers.append(_Control(instantiate=widget._instantiate, caption=getattr(widget, 'caption', None))) def _render(self, path, env): """TODO: Docstring for _render. Parameters ---------- path : TODO Returns ------- TODO """ jsx = env.get_template('view.jsx.j2') # pylint: disable=protected-access self.widgets = [w._instantiate for w in self.widgets] columns = [] if self.sidebar: columns.append('18em') columns += self.columns with open(os.path.join(path, self._name), 'w') as f: f.write( jsx.render( uuid=self._uuid, sidebar=self.sidebar, columns=columns, rows=self.rows, column_gap=self.column_gap, row_gap=self.row_gap, background_color=self.background_color, components=self.imports, controls=self.controllers, widgets=zip(self.widgets, self.spans) ) ) Route = namedtuple('Route', ['view', 'path', 'exact']) class App(object): """Core class to layout, connect, build a Bowtie app.""" def __init__(self, rows=1, columns=1, sidebar=True, title='Bowtie App', basic_auth=False, username='username', password='password', theme=None, background_color='White', host='0.0.0.0', port=9991, socketio='', debug=False): """Create a Bowtie App. Parameters ---------- 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 widgets. title : str, optional Title of the HTML. basic_auth : bool, optional Enable basic authentication. username : str, optional Username for basic authentication. password : str, optional Password for basic authentication. theme : str, optional Color for Ant Design components. background_color : str, optional Background color of the control pane. host : str, optional Host IP address. port : int, optional Host port number. socketio : string, optional Socket.io path prefix, only change this for advanced deployments. debug : bool, optional Enable debugging in Flask. Disable in production! """ self.background_color = background_color self.basic_auth = basic_auth self.debug = debug self.functions = [] self.host = host self.imports = set() self.init = None self.password = password self.port = port self.socketio = socketio self.schedules = [] self.subscriptions = defaultdict(list) self.pages = {} self.title = title self.username = username self.uploads = {} self.theme = theme self.root = View(rows=rows, columns=columns, sidebar=sidebar, background_color=background_color) self.routes = [Route(view=self.root, path='/', exact=True)] self._package_dir = os.path.dirname(__file__) self._jinjaenv = Environment( loader=FileSystemLoader(os.path.join(self._package_dir, 'templates')), trim_blocks=True, lstrip_blocks=True ) def __getattr__(self, name): """Export attributes from root view.""" if name == 'columns': return self.root.columns elif name == 'rows': return self.root.rows elif name == 'column_gap': return self.root.column_gap elif name == 'row_gap': return self.root.row_gap else: raise AttributeError(name) def __getitem__(self, key): """Get item from root view.""" self.root.__getitem__(key) def __setitem__(self, key, value): """Add widget to the root view.""" self.root.__setitem__(key, value) def add(self, widget): """Add a widget to the grid in the next available cell. Searches over columns then rows for available cells. Parameters ---------- widget : bowtie._Component A Bowtie widget instance. """ # pylint: disable=protected-access self.root._add(widget) def add_sidebar(self, widget): """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, path, exact=True): """Add a view to the app. Parameters ---------- view : View path : str exact : bool, optional """ assert path[0] == '/' 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)) def respond(self, pager, func): """Call a function in response to a page. When the pager calls notify, the function will be called. Parameters ---------- pager : Pager Pager that to signal when func is called. func : callable Function to be called. Examples -------- Using the pager to run a callback function. >>> from bowtie.pager import Pager >>> app = App() >>> pager = Pager() >>> def callback(): ... pass >>> def scheduledtask(): ... pager.notify() >>> app.respond(pager, callback) """ self.pages[pager] = func.__name__ def subscribe(self, func, event, *events): """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. Parameters ---------- func : callable Function to be called. event : event A Bowtie event. *events : Each is an event, optional Additional events. Examples -------- Subscribing a function to multiple events. >>> from bowtie.control import Dropdown, Slider >>> app = App() >>> dd = Dropdown() >>> slide = Slider() >>> def callback(dd_item, slide_value): ... pass >>> app.subscribe(callback, dd.on_change, slide.on_change) """ all_events = [event] all_events.extend(events) if len(all_events) > 1: # check if we are using any non stateful events for evt in all_events: if evt[2] is None: name = evt[0].split(SEPARATOR)[1] msg = '{}.on_{} is not a stateful event. It must be used alone.' raise NotStatefulEvent(msg.format(evt[1], name)) if event[0].split(SEPARATOR)[1] == 'upload': # evt = event[0] uuid = event[0].split(SEPARATOR)[0] if uuid in self.uploads: warnings.warn( ('Overwriting function "{func1}" with function ' '"{func2}" for upload object "{obj}".').format( func1=self.uploads[uuid], func2=func.__name__, obj=event[1] ), Warning) self.uploads[uuid] = func.__name__ for evt in all_events: self.subscriptions[evt].append((all_events, func.__name__)) def load(self, func): """Call a function on page load. Parameters ---------- func : callable Function to be called. """ self.init = func.__name__ def schedule(self, seconds, func): """Call a function periodically. Parameters ---------- seconds : float Minimum interval of function calls. func : callable Function to be called. """ self.schedules.append(_Schedule(seconds, func.__name__)) # pylint: disable=no-self-use def _sourcefile(self): # [-1] grabs the top of the stack and [1] grabs the filename return os.path.basename(inspect.stack()[-1][1])[:-3] def _write_templates(self): server = self._jinjaenv.get_template('server.py.j2') indexhtml = self._jinjaenv.get_template('index.html.j2') indexjsx = self._jinjaenv.get_template('index.jsx.j2') webpack = self._jinjaenv.get_template('webpack.config.js.j2') src, app, templates = create_directories() webpack_path = os.path.join(_DIRECTORY, webpack.name[:-3]) with open(webpack_path, 'w') as f: f.write( webpack.render(color=self.theme) ) server_path = os.path.join(src, server.name[:-3]) with open(server_path, 'w') as f: f.write( server.render( socketio=self.socketio, basic_auth=self.basic_auth, username=self.username, password=self.password, source_module=self._sourcefile(), subscriptions=self.subscriptions, uploads=self.uploads, schedules=self.schedules, initial=self.init, routes=self.routes, pages=self.pages, host="'{}'".format(self.host), port=self.port, debug=self.debug ) ) perms = os.stat(server_path) os.chmod(server_path, perms.st_mode | stat.S_IEXEC) template_src = os.path.join(self._package_dir, 'src', 'progress.jsx') shutil.copy(template_src, app) template_src = os.path.join(self._package_dir, 'src', 'utils.js') shutil.copy(template_src, app) for route in self.routes: for template in route.view.templates: template_src = os.path.join(self._package_dir, 'src', template) shutil.copy(template_src, app) packages = set() for route in self.routes: # pylint: disable=protected-access route.view._render(app, self._jinjaenv) packages |= route.view.packages with open(os.path.join(templates, indexhtml.name[:-3]), 'w') as f: f.write( indexhtml.render( title=self.title, ) ) with open(os.path.join(app, indexjsx.name[:-3]), 'w') as f: f.write( indexjsx.render( # pylint: disable=protected-access maxviewid=View._NEXT_UUID, socketio=self.socketio, pages=self.pages, routes=self.routes ) ) return packages def _build(self): """Compile the Bowtie application.""" packages = self._write_templates() if not os.path.isfile(os.path.join(_DIRECTORY, 'package.json')): packagejson = os.path.join(self._package_dir, 'src/package.json') shutil.copy(packagejson, _DIRECTORY) install = Popen('yarn install', shell=True, cwd=_DIRECTORY).wait() if install > 1: raise YarnError('Error install node packages') packages.discard(None) if packages: installed = installed_packages() packages = [x for x in packages if x.split('@')[0] not in installed] if packages: packagestr = ' '.join(packages) install = Popen('yarn add {}'.format(packagestr), shell=True, cwd=_DIRECTORY).wait() if install > 1: raise YarnError('Error install node packages') elif install == 1: print('Yarn error but trying to continue build') dev = Popen('{} -d'.format(_WEBPACK), shell=True, cwd=_DIRECTORY).wait() if dev != 0: raise WebpackError('Error building with webpack') def installed_packages(): """Extract installed packages as list from `package.json`.""" with open(os.path.join(_DIRECTORY, 'package.json'), 'r') as f: packagejson = json.load(f) return packagejson['dependencies'].keys() def create_directories(): """Create all the necessary subdirectories for the build.""" src = os.path.join(_DIRECTORY, 'src') templates = os.path.join(src, 'templates') app = os.path.join(src, 'app') makedirs(app, exist_ok=True) makedirs(templates, exist_ok=True) return src, app, templates PK QIL4׊k k bowtie/_cache.py# -*- coding: utf-8 -*- """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(object): """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 """ 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() PK$VEL*22bowtie/_command.py# -*- coding: utf-8 -*- """ Decorates a function for Bowtie. Reference --------- https://gist.github.com/carlsmith/800cbe3e11f630ac8aa0 """ from __future__ import print_function import os import sys import inspect from subprocess import call import click from bowtie._compat import numargs from bowtie._app import _DIRECTORY, _WEBPACK, App class WrongNumberOfArguments(TypeError): """The "build" function accepts an incorrect number of arguments.""" pass def _build(app): if app is None: print(('No `App` instance was returned. ' 'In the function decorated with @command, ' 'return the `App` instance so it can be built.')) else: if not isinstance(app, App): raise Exception(('Returned value {} is of type {}, ' 'it needs to be a bowtie.App instance.'.format(app, type(app)))) # pylint:disable=protected-access app._build() def command(func): """Command line interface decorator. Decorate a function for building a Bowtie application and turn it into a command line interface. """ @click.group(options_metavar='[--help]') def cmd(): """Bowtie CLI to help build and run your app.""" pass # pylint: disable=unused-variable @cmd.command(add_help_option=False) def build(): """Write the app, downloads the packages, and bundles it with Webpack.""" nargs = numargs(func) if nargs == 0: app = func() else: raise WrongNumberOfArguments( 'Decorated function "{}" should have no arguments, it has {}.' .format(func.__name__, nargs) ) _build(app) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) def run(extra): """Build the app and serve it.""" nargs = numargs(func) if nargs == 0: app = func() else: raise WrongNumberOfArguments( 'Decorated function "{}" should have no arguments, it has {}.' .format(func.__name__, nargs) ) # pylint:disable=protected-access _build(app) filepath = './{}/src/server.py'.format(_DIRECTORY) line = (filepath,) + extra call(line) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) def serve(extra): """Serve the Bowtie app.""" filepath = './{}/src/server.py'.format(_DIRECTORY) if os.path.isfile(filepath): line = (filepath,) + extra call(line) else: print("Cannot find '{}'. Did you build the app?".format(filepath)) @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, '-d') + extra call(line, cwd=_DIRECTORY) @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, '--define', 'process.env.NODE_ENV="production"', '--progress') + extra call(line, cwd=_DIRECTORY) 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 PK$=L%2bowtie/_compat.py# -*- coding: utf-8 -*- """Python 2/3 compatability.""" import inspect import sys from os import makedirs IS_PY2 = sys.version_info < (3, 0) if IS_PY2: # pylint: disable=invalid-name makedirs_lib = makedirs # pylint: disable=function-redefined,missing-docstring def makedirs(name, mode=0o777, exist_ok=False): """Create directories recursively.""" try: makedirs_lib(name, mode=mode) except OSError: if not exist_ok: raise def numargs(func): """Get number of arguments in Python 3.""" return len(inspect.signature(func).parameters) if IS_PY2: # pylint: disable=function-redefined def numargs(func): """Get number of arguments in Python 2.""" count = 0 # pylint: disable=deprecated-method for args in inspect.getargspec(func)[:2]: try: count += len(args) except TypeError: pass return count PK QILtbowtie/_component.py# -*- coding: utf-8 -*- """Bowtie abstract component classes. All visual and control components inherit these. """ # need this for get commands on python2 from __future__ import unicode_literals # pylint: disable=redefined-builtin from builtins import bytes import string from functools import wraps import json from datetime import datetime, date, time import msgpack import flask from flask_socketio import emit from future.utils import with_metaclass import eventlet from eventlet.queue import LightQueue from bowtie.exceptions import SerializationError SEPARATOR = '#' COMPONENT_REGISTRY = {} def jsbool(x): """Convert Python bool to Javascript bool.""" return repr(x).lower() def json_conversion(obj): """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): """Encode Python object to JSON with additional encoders.""" return json.dumps(data, default=json_conversion) # pylint: disable=too-many-return-statements def encoders(obj): """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): """Encode ``x`` into msgpack with additional encoders.""" try: return bytes(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): """Decode ``x`` from msgpack into Python object.""" return msgpack.unpackb(bytes(x['data']), encoding='utf8') def make_event(event): """Create an event from a method signature.""" # pylint: disable=missing-docstring @property @wraps(event) def actualevent(self): name = event.__name__[3:] # pylint: disable=protected-access ename = '{uuid}{sep}{event}'.format( uuid=self._uuid, sep=SEPARATOR, event=name ) try: # the getter post processing function # is preserved with an underscore getter = event(self).__name__ except AttributeError: getter = None return ename, self._uuid, getter return actualevent def is_event(attribute): """Test if a method is an event.""" return attribute.startswith('on_') def make_command(command): """Create an command from a method signature.""" # pylint: disable=missing-docstring @wraps(command) def actualcommand(self, *args, **kwds): data = command(self, *args, **kwds) name = command.__name__[3:] # pylint: disable=protected-access signal = '{uuid}{sep}{event}'.format( uuid=self._uuid, 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): """Test if a method is an command.""" return attribute.startswith('do_') def make_getter(getter): """Create an command from a method signature.""" # pylint: disable=missing-docstring def get(self, timeout=10): name = getter.__name__ # pylint: disable=protected-access signal = '{uuid}{sep}{event}'.format( uuid=self._uuid, 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): """Test if a method is a getter. It can be `get` or `get_*`. """ return attribute.startswith('get') class _Maker(type): def __new__(mcs, name, parents, dct): for k in list(dct.keys()): if is_event(k): dct[k] = make_event(dct[k]) if is_command(k): dct[k] = make_command(dct[k]) if is_getter(k): # preserve the post-processor with an underscore dct['_' + k] = dct[k] dct[k] = make_getter(dct[k]) return super(_Maker, mcs).__new__(mcs, name, parents, dct) class FormatDict(dict): """Dict to replace missing keys.""" def __missing__(self, key): """Replace missing key with "{key"}".""" return "{" + key + "}" # pylint: disable=too-few-public-methods class Component(with_metaclass(_Maker, object)): """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 @classmethod def _next_uuid(cls): cls._NEXT_UUID += 1 return cls._NEXT_UUID def __init__(self): """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(Component, self).__init__() self._tagbase = " socket={{socket}} uuid={{'{uuid}'}} />".format(uuid=self._uuid) self._tag = '<' + self._COMPONENT if self._ATTRS: self._tag += ' ' + self._ATTRS COMPONENT_REGISTRY[self._uuid] = self @staticmethod def _insert(wrap, tag): """Insert the component tag into the wrapper html. This ignores other tags already created like ``{socket}``. https://stackoverflow.com/a/11284026/744520 """ formatter = string.Formatter() mapping = FormatDict(component=tag) return formatter.vformat(wrap, (), mapping) PKAL6X bowtie/_progress.py# -*- coding: utf-8 -*- """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): """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(Progress, self).__init__() self._tagbase = self._tagbase[:-3] + '>' self._tags = '<' + self._COMPONENT + self._tagbase, '' # 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 """ pass def do_success(self): """Display the progress indicator as done. Returns ------- None """ pass def do_error(self): """Display an error in the progress indicator. Returns ------- None """ pass PK$=LΣkbowtie/_utils.py# -*- coding: utf-8 -*- """Utility functions.""" import inspect def func_name(): """Return name of calling function.""" return inspect.stack()[1][3] PKrIL.RRbowtie/control.py# -*- coding: utf-8 -*- """Control components.""" from collections import Iterable 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. """ def __init__(self, caption=None): super(_Controller, self).__init__() self.caption = caption @property def _instantiate(self): 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='', caption=None): """Create a button. Parameters ---------- label : str, optional Label on the button. caption : str, optional Heading text. """ super(Button, self).__init__(caption=caption) self._comp = self._tag.format( label=label ) def on_click(self): """Emit an event when the button is clicked. | **Payload:** ``None``. Returns ------- str Name of click event. """ pass class Link(_Controller): """An internal link. This doesn't create a page reload. """ _TEMPLATE = 'link.jsx' _COMPONENT = 'ALink' _PACKAGE = None _ATTRS = "to={{'{link}'}}" def __init__(self, link='/', caption=None): """Create a button. Parameters ---------- link : str """ super(Link, self).__init__(caption=caption) self._comp = self._tag.format( link=link ) def on_click(self): """Emit an event when the button is clicked. | **Payload:** ``None``. Returns ------- str Name of click event. """ pass class Upload(_Controller): """Draggable file upload widget.""" _TEMPLATE = 'upload.jsx' _COMPONENT = 'AntUpload' _PACKAGE = None _ATTRS = "multiple={{{multiple}}}" def __init__(self, multiple=True, caption=None): """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. caption : str, optional Heading text. """ super(Upload, self).__init__(caption=caption) self._comp = self._tag.format( multiple=jsbool(multiple) ) def on_upload(self): """Emit an event when the selection changes. | **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. """ pass class Dropdown(_Controller): """Dropdown based on react-select.""" _TEMPLATE = 'dropdown.jsx' _COMPONENT = 'Dropdown' _PACKAGE = 'react-select@1.2.1' _ATTRS = ('initOptions={{{options}}} ' 'multi={{{multi}}} ' 'default={{{default}}}') def __init__(self, labels=None, values=None, multi=False, default=None, caption=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. caption : str, optional Heading text. """ super(Dropdown, self).__init__(caption=caption) if labels is None and values is None: labels = [] values = [] options = [dict(value=value, label=str(label)) for value, label in zip(values, labels)] self._comp = self._tag.format( options=jdumps(options), multi=jsbool(multi), default=jdumps(default), ) def on_change(self): """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=False, caption=None): """Create a toggle switch. Parameters ---------- initial : bool, optional Starting state of the switch. caption : str, optional Label appearing above the widget. """ super(Switch, self).__init__(caption=caption) 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=False, month_type=False, range_type=False, caption=None): super(_DatePickers, self).__init__(caption=caption) 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, caption=None): """Create a date picker. Parameters ---------- caption : str, optional Heading text. """ super(DatePicker, self).__init__(date_type=True, caption=caption) 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, caption=None): """Create month picker. Parameters ---------- caption : str, optional Heading text. """ super(MonthPicker, self).__init__(month_type=True, caption=caption) 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, caption=None): """Create a date range picker. Parameters ---------- caption : str, optional Heading text. """ super(RangePicker, self).__init__(range_type=True, caption=caption) 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=0, minimum=-1e100, maximum=1e100, step=1, size='default', caption=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. caption : str, optional Heading text. References ---------- https://ant.design/components/input/ """ super(Number, self).__init__(caption=caption) 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='Enter text', size='default', area=False, autosize=False, disabled=False, caption=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. caption : str, optional Heading text. References ---------- https://ant.design/components/input/ """ super(Textbox, self).__init__(caption=caption) area = 'textarea' if area else 'text' self._comp = self._tag.format( area=area, 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): """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=None, ranged=False, minimum=0, maximum=100, step=1, vertical=False, caption=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 caption : str, optional Heading text. References ---------- https://ant.design/components/slider/ """ super(Slider, self).__init__(caption=caption) if not start: start = [0, 0] if ranged else 0 elif isinstance(start, Iterable): 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): """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.0.3' _ATTRS = ('range={{{{min: {min}, max: {max}}}}} ' 'socket={{socket}} ' 'start={{{start}}} ' 'tooltips={{{tooltips}}}') def __init__(self, start=0, minimum=0, maximum=100, tooltips=True, caption=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. caption : str, optional Heading text. References ---------- https://refreshless.com/nouislider/events-callbacks/ """ super(Nouislider, self).__init__(caption=caption) if not isinstance(start, Iterable): start = [start] else: start = list(start) self._comp = self._tag.format( min=minimum, max=maximum, start=start, 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): """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 PKmDL9-bowtie/exceptions.py# -*- coding: utf-8 -*- """Bowtie exceptions.""" class YarnError(Exception): """Errors from ``Yarn``.""" pass class WebpackError(Exception): """Errors from ``Webpack``.""" pass class SizeError(Exception): """Size values must be a number.""" pass class GridIndexError(IndexError): """Invalid index into the grid layout.""" pass class NoUnusedCellsError(Exception): """All cells are used.""" pass class UsedCellsError(Exception): """At least one cell is used, when placing the widget.""" pass class MissingRowOrColumn(Exception): """Missing a row or column.""" pass class NoSidebarError(Exception): """Cannot add to the sidebar when it doesn't exist.""" pass class NotStatefulEvent(Exception): """This event is not stateful and cannot be paired with other events.""" pass class SerializationError(TypeError): """Cannot serialize the data for command.""" pass PK$=L U,bowtie/pager.py# -*- coding: utf-8 -*- """Bowtie pager.""" import flask from flask_socketio import emit import eventlet class Pager(object): """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('page#' + str(self._uuid)) else: sio = flask.current_app.extensions['socketio'] sio.emit('page#' + str(self._uuid)) eventlet.sleep() PKICLg((bowtie/visual.py# -*- coding: utf-8 -*- """Visual components.""" from flask import Markup from markdown import markdown 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. """ def __init__(self): self.progress = Progress() super(_Visual, self).__init__() @property def _instantiate(self): # pylint: disable=protected-access begin, end = self.progress._tags tagwrap = begin + '{component}' + self._tagbase + end return self._insert(tagwrap, self._comp) class Markdown(_Visual): """Display Markdown.""" _TEMPLATE = 'markdown.jsx' _COMPONENT = 'Markdown' _PACKAGE = None _ATTRS = "initial={{'{initial}'}}" def __init__(self, initial=''): """Create a Markdown widget. Parameters ---------- initial : str, optional Default markdown for the widget. """ super(Markdown, self).__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 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=None, results_per_page=10): """Create a table and optionally initialize the data. Parameters ---------- columns : list, optional List of column names to display. results_per_page : int, optional Number of rows on each pagination of the table. """ super(Table, self).__init__() self.data = [] self.columns = [] 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): """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): """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.1' _ATTRS = None def __init__(self): """Create the table, optionally set the columns.""" super(SmartGrid, self).__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=False): """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(SVG, self).__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(' { this.props.socket.emit(this.props.uuid + '#click'); } render() { return ( ); } } SimpleButton.propTypes = { label: PropTypes.string.isRequired, uuid: PropTypes.string.isRequired, socket: PropTypes.object.isRequired }; PK$=L3__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 }; PKDLkF 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'; 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}; } else { this.state = JSON.parse(local); } } handleChange = (moment, ds) => { this.setState({value: moment}); storeState(this.props.uuid, this.state, {value: moment}); 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.value)); } 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, }; PK$=L3GGbowtie/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, }; PKDLջ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, }; PK$=Lbowtie/src/utils.jsexport const storeState = (uuid, state, data) => { sessionStorage.setItem(uuid, JSON.stringify(Object.assign(state, data))); }; PKECLobowtie/templates/index.html.j2 {{ title }}
PKl{IL g? bowtie/templates/index.jsx.j2import 'normalize.css'; import React from 'react'; import ReactDOM from 'react-dom'; import io from 'socket.io-client'; import {message} from 'antd'; import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'; {% for viewid in range(1, maxviewid + 1) %} import View{{ viewid }} from './view{{ viewid }}'; {% endfor %} var msgpack = require('msgpack-lite'); var socket = io({path: '/{{ socketio }}socket.io'}); 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')); // vim: set ft=javascript.jsx: PKbUKL}Xbowtie/templates/server.py.j2#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import socket import traceback from functools import wraps import eventlet {% if debug %} eventlet.monkey_patch(thread=True, time=True) {% endif %} from builtins import bytes import click import msgpack import flask from flask import (Flask, render_template, make_response, copy_current_request_context, jsonify, request, Response) from flask_socketio import SocketIO from bowtie._component import COMPONENT_REGISTRY # python 2 compatibility try: FileNotFoundError except NameError: FileNotFoundError = OSError class GetterNotDefined(AttributeError): pass def check_auth(username, password): """This function is called to check if a username / password combination is valid. """ return username == '{{ username }}' and password == '{{ password }}' def authenticate(): """Sends a 401 response that enables basic auth""" 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"'}) def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated # import the user created module sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) import {{source_module}} app = Flask(__name__) app.debug = {{ debug|default(False) }} socketio = SocketIO(app, binary=True, path='{{ socketio }}' + 'socket.io') # not sure if this is secure or how much it matters app.secret_key = os.urandom(256) def context(func): def foo(): with app.app_context(): func() return foo class Scheduler(object): def __init__(self, seconds, func): self.seconds = seconds self.func = func self.thread = None def start(self): self.thread = eventlet.spawn(self.run) def run(self): ret = eventlet.spawn(context(self.func)) eventlet.sleep(self.seconds) try: ret.wait() except: traceback.print_exc() self.thread = eventlet.spawn(self.run) def stop(self): if self.thread: self.thread.cancel() {% for route in routes %} @app.route('{{ route.path }}') {% endfor %} {% if basic_auth %} @requires_auth {% endif %} def index(): return render_template('index.html') {% if login %} @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': success = {{ source_module }}.{{ login }}() if success: return redirect(url_for('index')) else: return redirect(url_for('login')) return {{ loginpage }} {% endif %} @app.route('/static/bundle.js') def getbundle(): basedir = os.path.dirname(os.path.realpath(__file__)) bundle_path = basedir + '/static/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() else: 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 else: return open(bundle_path, 'r').read() {% if initial %} @socketio.on('INITIALIZE') def _(): foo = copy_current_request_context({{ source_module }}.{{ initial }}) eventlet.spawn(foo) {% endif %} {% for uuid, function in uploads.items() %} @app.route('/upload{{ uuid }}', methods=['POST']) def _(): upfile = request.files['file'] retval = {{ source_module }}.{{ function }}(upfile.filename, upfile.stream) if retval: return make_response(jsonify(), 400) return make_response(jsonify(), 200) {% endfor %} {% for page, function in pages.items() %} @socketio.on('resp#{{ page._uuid }}') def _(): foo = copy_current_request_context({{ source_module }}.{{ function }}) eventlet.spawn(foo) {% endfor %} {% for event, supports in subscriptions.items() %} @socketio.on('{{ event[0] }}') def _(*args): def wrapuser(): uniq_events = set() {% for support in supports %} uniq_events.update({{ support[0] }}) {% endfor %} uniq_events.remove({{ event }}) event_data = {} for ev in uniq_events: comp = COMPONENT_REGISTRY[ev[1]] if ev[2] is None: ename = ev[0] raise GetterNotDefined('{ctype} has no getter associated with event "on_{ename}"' .format(ctype=type(comp), ename=ename[ename.find('#') + 1:])) getter = getattr(comp, ev[2]) event_data[ev[0]] = 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 {% set post = event[2] %} {% if post is not none %} event_data['{{ event[0] }}'] = COMPONENT_REGISTRY[{{event[1]}}].{{ '_' ~ post }}( msgpack.unpackb(bytes(args[0]['data']), encoding='utf8') ) {% endif %} # gather the remaining data from the other events through their getter methods {% for support in supports %} user_args = [] {% if post is not none %} {% for ev in support[0] %} user_args.append(event_data['{{ ev[0] }}']) {% endfor %} {% endif %} # finally call the user method {{ source_module }}.{{ support[1] }}(*user_args) {% endfor %} foo = copy_current_request_context(wrapuser) eventlet.spawn(foo) {% endfor %} @click.command() @click.option('--host', '-h', default={{host}}, help='Host IP') @click.option('--port', '-p', default={{port}}, help='port number') def main(host, port): scheduled = not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true' if scheduled: scheds = [] {% for schedule in schedules %} sched = Scheduler({{ schedule.seconds }}, {{ source_module }}.{{ schedule.function }}) scheds.append(sched) {% endfor %} for sched in scheds: sched.start() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((host, port)) if result == 0: raise Exception('Port {} is unavailable on host {}, aborting.'.format(port, host)) socketio.run(app, host=host, port=port) if scheduled: for sched in scheds: sched.stop() if __name__ == '__main__': main() PKDL4wbowtie/templates/view.jsx.j2import 'normalize.css'; import React from 'react'; import PropTypes from 'prop-types'; import {render} from 'react-dom'; import {message} from 'antd'; import AntProgress from './progress'; {% for component in components %} import {{ component.component }} from './{{ component.module }}'; {% endfor %} export default class View{{ uuid }} extends React.Component { render() { var socket = this.props.socket; return (
{% if sidebar %}
{% for control in controls %} {% if control.caption is not none %}
{{ control.caption }}
{% endif %}
{{ control.instantiate }}
{% endfor %}
{% endif %} {% for widget, span in widgets %}
{{ widget }}
{% endfor %}
); } } View{{ uuid }}.propTypes = { socket: PropTypes.object.isRequired, } // vim: set ft=javascript.jsx: PKDL⏼i66%bowtie/templates/webpack.config.js.j2const webpack = require('webpack'); // this is an ugly hack const prod = process.argv.indexOf('--define') !== -1; var path = require('path'); var CompressionPlugin = require('compression-webpack-plugin'); var BUILD_DIR = path.resolve(__dirname, 'src/static'); var APP_DIR = path.resolve(__dirname, 'src/app'); var HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); var config = { // context: path.resolve(__dirname, './src'), context: __dirname, entry: APP_DIR + '/index.jsx', output: { path: BUILD_DIR, filename: 'bundle.js' }, plugins: [ new HardSourceWebpackPlugin() ], module: { rules: [ { test: /\.(js|jsx)$/, include: APP_DIR, loader: 'babel-loader', exclude: /node_modules/, options: { presets: [ ['env', {'modules': false}], 'react', 'stage-0'], plugins: [ ["import", { "libraryName": "antd", "style": true }], 'transform-object-rest-spread', ], 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, 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' ] } }; // for production // https://github.com/webpack/webpack/issues/2537#issuecomment-250950677 if (prod) { config.devtool = 'cheap-module-source-map'; config.plugins = [ new webpack.LoaderOptionsPlugin({ minimize: true, debug: false }), // https://facebook.github.io/react/docs/optimizing-performance.html new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), new webpack.optimize.UglifyJsPlugin({ beautify: false, mangle: { screw_ie8: true, keep_fnames: false }, compress: { warnings: false, booleans: true, screw_ie8: true, conditionals: true, loops: true, unused: true, comparisons: true, sequences: true, dead_code: true, evaluate: true, join_vars: true, if_return: true }, comments: false, }), new CompressionPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', }) ]; } module.exports = config; // vim: set ft=javascript: PK$=LBxbowtie/tests/__init__.py"""Test directory.""" PKSHL ]]bowtie/tests/conftest.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Pytest configuration.""" from sys import platform import socket import shutil from selenium import webdriver import pytest from bowtie import View from bowtie._app import _DIRECTORY @pytest.fixture def build_path(): """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 _DIRECTORY shutil.rmtree(_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(chrome_options=options) yield driver driver.quit() PK QILXEbowtie/tests/test_cache.py# -*- coding: utf-8 -*- """Test markdown and text widgets.""" 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() # pylint: disable=invalid-name button = Button() store = {} def click(): """Update markdown text.""" 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_path, monkeypatch): """Create markdown and text widgets.""" monkeypatch.setattr(App, '_sourcefile', lambda self: 'bowtie.tests.test_cache') app = App() app.add(button) app.subscribe(click, button.on_click) # pylint: disable=protected-access app._build() with server_check(build_path) as server: yield server # pylint: disable=redefined-outer-name,unused-argument def test_cache(dummy, chrome_driver): """Test markdown and text widgets.""" 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) PK$=Lc.bowtie/tests/test_compat.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Compat testing.""" from bowtie._compat 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 PKmBLcbowtie/tests/test_compile.py#!/usr/bin/env python # -*- coding: utf-8 -*- """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_path, monkeypatch): """Tests the build process.""" monkeypatch.setattr(App, '_sourcefile', lambda self: 'bowtie.tests.test_compile') reset_uuid() ctrl = Nouislider() viz = Plotly() app = App() app.add_sidebar(ctrl) app.add(viz) app.subscribe(callback, ctrl.on_change) # pylint: disable=protected-access app._build() PK QILvX< bowtie/tests/test_components.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Test all components for instatiation issues.""" import os from os import environ as env import subprocess import time import pytest from bowtie import App from bowtie import control, visual from bowtie._component import COMPONENT_REGISTRY from bowtie.tests.utils import reset_uuid def create_components(): """Create components for this test.""" reset_uuid() # pylint: disable=protected-access controllers = [getattr(control, comp)() for comp in dir(control) if comp[0].isupper() and issubclass(getattr(control, comp), control._Controller) and comp != 'Upload'] for controller in controllers: assert COMPONENT_REGISTRY[controller._uuid] == controller visuals = [getattr(visual, comp)() for comp in dir(visual) if comp[0].isupper() and issubclass(getattr(visual, comp), visual._Visual)] for vis in visuals: assert COMPONENT_REGISTRY[vis._uuid] == vis return controllers, visuals create_components() @pytest.fixture def components(build_path, monkeypatch): """App with all components.""" monkeypatch.setattr(App, '_sourcefile', lambda self: 'bowtie.tests.test_components') controllers, visuals = create_components() app = App(rows=len(visuals)) 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) # pylint: disable=protected-access app._build() env['PYTHONPATH'] = '{}:{}'.format(os.getcwd(), os.environ.get('PYTHONPATH', '')) server = subprocess.Popen(os.path.join(build_path, 'src/server.py'), env=env) time.sleep(5) yield server.kill() # pylint: disable=redefined-outer-name,unused-argument 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']) PKGLffbowtie/tests/test_editor.py# -*- coding: utf-8 -*- """Test markdown and text widgets.""" import os from os import environ as env import subprocess import time import pytest from bowtie import App from bowtie.visual import Markdown from bowtie.control import Textbox from bowtie.tests.utils import reset_uuid reset_uuid() # pylint: disable=invalid-name 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_path, monkeypatch): """Create markdown and text widgets.""" monkeypatch.setattr(App, '_sourcefile', lambda self: 'bowtie.tests.test_editor') app = App() app.add(mark) app.add_sidebar(side) app.add_sidebar(text) app.subscribe(write, text.on_change) # pylint: disable=protected-access app._build() env['PYTHONPATH'] = '{}:{}'.format(os.getcwd(), os.environ.get('PYTHONPATH', '')) server = subprocess.Popen(os.path.join(build_path, 'src/server.py'), env=env) time.sleep(5) yield server.kill() # pylint: disable=redefined-outer-name,unused-argument 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 PKICLn bowtie/tests/test_layout.py# -*- coding: utf-8 -*- """Test layout functionality.""" # pylint: disable=redefined-outer-name import pytest from bowtie import App from bowtie.control import Button from bowtie._app import GridIndexError, UsedCellsError, NoUnusedCellsError @pytest.fixture(scope='module') def buttons(): """Four buttons.""" return [Button() for _ in range(4)] def app(): """Simple app.""" return App(rows=2, columns=2) 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]) assert list(app.root.used.values()) == 4 * [True] 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] assert list(app.root.used.values()) == 4 * [True] with pytest.raises(NoUnusedCellsError): app.add(buttons[2]) app = App(rows=2, columns=2) app[0] = buttons[0] app[1, 0] = buttons[2] app[1, 1] = buttons[3] assert list(app.root.used.values()) == 4 * [True] with pytest.raises(NoUnusedCellsError): app.add(buttons[2]) def test_used(buttons): """Test cell usage checks.""" app = App(rows=2, columns=2) for i in range(3): app.add(buttons[i]) with pytest.raises(UsedCellsError): app[0, 0] = buttons[3] with pytest.raises(UsedCellsError): app[0:1, 1] = buttons[3] with pytest.raises(UsedCellsError): 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] with pytest.raises(UsedCellsError): 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 sum(app.root.used.values()) == 2 app[0, :] = but assert sum(app.root.used.values()) == 4 app = App(rows=2, columns=2) app[0:1, 1:2] = but assert sum(app.root.used.values()) == 1 app[1:, 0:] = but assert sum(app.root.used.values()) == 3 app = App(rows=2, columns=2) app[-1, :2] = but assert sum(app.root.used.values()) == 2 app = App(rows=1, columns=2) app[0, :2] = but assert sum(app.root.used.values()) == 2 app = App(rows=1, columns=2) app[0] = but assert sum(app.root.used.values()) == 2 app = App(rows=2, columns=2) app[:2] = but assert sum(app.root.used.values()) == 4 PK6ELFbowtie/tests/test_plotly.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Plotly testing.""" import os from os import environ as env import subprocess import time import pytest from plotly.graph_objs import Scatter from plotly.graph_objs import Layout as PlotLayout from bowtie import App from bowtie.control import Nouislider, Button from bowtie.visual import Plotly from bowtie.tests.utils import reset_uuid reset_uuid() # pylint: disable=invalid-name viz = Plotly() ctrl = Nouislider() ctrl2 = Button() def callback(*args): """dummy function""" # pylint: disable=unused-argument chart = { "data": [ Scatter(x=[1, 2, 3, 4], y=[4, 1, 3, 7]) ], "app": PlotLayout( title="hello world" ) } viz.do_all(chart) # pylint: disable=unused-argument @pytest.fixture def plotly(build_path, monkeypatch): """Create plotly app.""" monkeypatch.setattr(App, '_sourcefile', lambda self: 'bowtie.tests.test_plotly') app = App() app.add(viz) app.add_sidebar(ctrl) app.add_sidebar(ctrl2) app.subscribe(callback, ctrl.on_change) app.subscribe(callback, ctrl2.on_click) # pylint: disable=protected-access app._build() env['PYTHONPATH'] = '{}:{}'.format(os.getcwd(), os.environ.get('PYTHONPATH', '')) server = subprocess.Popen(os.path.join(build_path, 'src/server.py'), env=env) time.sleep(5) yield server.kill() # pylint: disable=redefined-outer-name,unused-argument 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') 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 PK$=LPbowtie/tests/test_serialize.py#!/usr/bin/env python # -*- coding: utf-8 -*- """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']) PK$=LHbowtie/tests/test_tags.py# -*- coding: utf-8 -*- """Test tag instantation for components.""" from bowtie.visual 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 == ("" "" "").format(next_uuid=next_uuid + 1, next_uuid1=next_uuid + 2) next_uuid = Markdown._NEXT_UUID mark = Markdown(initial='#hi\n##hello') assert mark._instantiate == ("" "hi\\n

hello

'}} " "socket={{socket}} uuid={{'{next_uuid1}'}} />" "
").format(next_uuid=next_uuid + 1, next_uuid1=next_uuid + 2) PK$=L"bowtie/tests/test_utils.py#!/usr/bin/env python # -*- coding: utf-8 -*- """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' PK QILظqttbowtie/tests/utils.py# -*- coding: utf-8 -*- """Utility functions for testing only.""" import os from os.path import join as pjoin from os import environ as env from contextlib import contextmanager from subprocess import Popen, PIPE import time from bowtie._component import Component def reset_uuid(): """Reset the uuid counter for components.""" # pylint: disable=protected-access Component._NEXT_UUID = 0 @contextmanager def server_check(build_path): """Context manager for testing Bowtie apps and verifying no errors happened.""" env['PYTHONPATH'] = '{}:{}'.format(os.getcwd(), os.environ.get('PYTHONPATH', '')) server = Popen(['python', '-u', pjoin(build_path, 'src/server.py')], env=env, stderr=PIPE) time.sleep(5) yield server server.terminate() _, stderr = server.communicate() assert b'Error' not in stderr assert b'Traceback' not in stderr PK$=Ld~q77bowtie-0.8.1.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!HxyUbbowtie-0.8.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,Q034 /, (-JLR()*M ILR(4KM̫#DPK!H]?bowtie-0.8.1.dist-info/METADATAXmo8_p*Ik5.WMMME$ƒZ %+4 .(3ό+8 'J+~SQ'z,\켭*a?!#lhaREr\ F䨨p(ʕ+$.덨'%(_OGڤvQFVo&wol]͊{ D#aPtz^Z)˃7|O˦1z)=zeڕ3s#J9$ŸIC5vP7?Ƃ[eou+.K%ڕ3vCV5`aom]k`BpaNqnFu@OjDYz %hVt/D HHJ%S_]]a+E'$xK趙\GLƎ{շ._L9K~y=;;᛾S1y!u Ro. CKjkY6 n-7k=Gxrd4:͞Ο-GKs"XΟ$Q\>K*|%J4cH9FX0aC\Y.x1]|k#DbGyCҋ/Q5}(UnE*xPKR]qW(b5J^g:*a1h0 D'aCXDc. k[Ɓy~VU4X {! PbNiZl~.U$i$+c>#)h#r/4 d Ȑ'Ǟܨ>(2p`԰9-fBW>5+DM4^S;A+zboa+J'7脧@B-yA1NS L*aRoy<34=[C'r&>DC>ZSV nD&>"g}B[&Pmpj%fc?Ʈz;Vttzfz$)a my#V SU,Ҡc_tPAjr qr Ǎ &}r>ȷ[Uyh e壠_zy8ko-Ϯލw}(!X/@"w\(aWAn^sF@*ܵ4=նq"&;` AqP2F۷ZC[:o w?`6Tí~ܯl ^/$.{Dϼvd3// q4Ő6PK!HLN< bowtie-0.8.1.dist-info/RECORDuǒ|[ #̆Ix|&PwFՉ{3o&a3Ygu6v9 icjaogteuusAҌ Y}L +W v<{eBX q/1Q*:l{݉/GR'<8E_NDid-j5 ZjYH/v8rF!j*}jv D>"~a*]zHo12/0n=N*EO.Qrra5A934uQޠN@ |ʼn'Ko߂|кc@P}aY0'D|c,2aj*F&Lؗ5YVIjLIc*.|| !c4$ 뤢Gj8"ZSf;w !rW-"O*yEI;fMKFy,{vNb>ͪBczb`1D0'>6x$Yd!E2D]˹.W'f _20اUd}l}r#Z!^ŮA q2@0 vb^פϘx^C"t5 (:j_s<9U1ISk2}H/ vZŪŋqQU\_1n]ԟ/uexa /7̳.H'x -߶lry;F( IS2 ĭ{1H9@nA<#I18fhcFRAp >7KqMϽMȐ7EU;fh gyÓ0o2<{*erOt N)ܩ^E)R7Ԑ o<{M4M_8\ ^I h$z7?,*v V"c7ã@bZ-^ţKy`4f]t3R[k מΈb_pCmoɛz+/my{榭BGN"3IpXڭ+\HyF!h2mNd·Ȳ YQpTNpmP)8B4!_gL﬎ׯt_9U$+^qK g"p/C,V/e+P28JJƞ<2q^%e[Aީs\P*!4[*xяC R ^2no%oVs%k|.Cu8i{lz1Ko':'Kk[Gbi( eS^ޱ #p! iun5B~8#8`1 Z\oJ!heIwF6ߎ@xӟ$686j\g^ A`w{!cm/G(Լ -d IV>+ZnH~"ߺ\rEКfRfȠbԓ+E}Kj4$ vm(, .Г?wj+s| ,O4DHH>V5  Q0?I~kEԤ iV$jv1dm?w{/OYK3폔'9Ki" B";fC\2rw~.KM`עIwv'Fim ۷"?[gACS:=[ fe5< SS#%7X 퇢 }BC$n%A6Ks.mvBo1yL_sATSz֥N @6m׺_Я8F ߊ@8r1B+ ߈KRY [cֻɱm'7PF )յM^J&F"^懛=3h`1*omҟ|6;p6ʝ,C W8cl2߱WٳPKKLpdbowtie/__init__.pyPKIL^mǑdidibowtie/_app.pyPK QIL4׊k k jbowtie/_cache.pyPK$VEL*22tbowtie/_command.pyPK$=L%2}bowtie/_compat.pyPK QILtbowtie/_component.pyPKAL6X צbowtie/_progress.pyPK$=LΣkbowtie/_utils.pyPKrIL.RR߲bowtie/control.pyPKmDL9-bowtie/exceptions.pyPK$=L U, bowtie/pager.pyPKICLg((6 bowtie/visual.pyPK$=LJE++4bowtie/feedback/__init__.pyPK$=Lª""V5bowtie/feedback/message.pyPKDL^~;bowtie/src/button.jsxPK$=L3__w>bowtie/src/datagrid.jsxPKDLkF  Gbowtie/src/date.jsxPK$=L3GG Qbowtie/src/dropdown.jsxPK$=LY[Ybowtie/src/griddle.jsxPK$=L.1nnM`bowtie/src/link.jsxPK$=Labowtie/src/markdown.jsxPK$=L"N##fbowtie/src/nouislider.jsxPKDL_J@xbowtie/src/number.jsxPKDL+(^~bowtie/src/package.jsonPK@CL:KK&bowtie/src/plotly.jsxPKDLbowtie/src/progress.jsxPKrIL邽 bowtie/src/slider.jsxPK$=Lbowtie/src/svg.jsxPKDL;Ӊbowtie/src/switch.jsxPKDL(3ۼbowtie/src/table.jsxPKDLuOj  bowtie/src/textbox.jsxPKDLջp]@@Tbowtie/src/upload.jsxPK$=Lbowtie/src/utils.jsPKECLo|bowtie/templates/index.html.j2PKl{IL g? bowtie/templates/index.jsx.j2PKbUKL}Xbowtie/templates/server.py.j2PKDL4wbowtie/templates/view.jsx.j2PKDL⏼i66%bowtie/templates/webpack.config.js.j2PK$=LBxfbowtie/tests/__init__.pyPKSHL ]]큲bowtie/tests/conftest.pyPK QILXEEbowtie/tests/test_cache.pyPK$=Lc.&"bowtie/tests/test_compat.pyPKmBLcD$bowtie/tests/test_compile.pyPK QILvX< K'bowtie/tests/test_components.pyPKGLff큨0bowtie/tests/test_editor.pyPKICLn G8bowtie/tests/test_layout.pyPK6ELF9Ebowtie/tests/test_plotly.pyPK$=LPEMbowtie/tests/test_serialize.pyPK$=LHUPbowtie/tests/test_tags.pyPK$=L""Ubowtie/tests/test_utils.pyPK QILظqttkVbowtie/tests/utils.pyPK$=Ld~q77Zbowtie-0.8.1.dist-info/LICENSEPK!HxyUb^bowtie-0.8.1.dist-info/WHEELPK!H]?_bowtie-0.8.1.dist-info/METADATAPK!HLN< Mfbowtie-0.8.1.dist-info/RECORDPK77Ep