PKJ=bowtie/__init__.py"""Interactive dashboard toolkit.""" __version__ = '0.3.1' from bowtie._layout import Layout from bowtie._command import command PKdJzj bowtie/_command.py# -*- coding: utf-8 -*- """ Decorates a function for Bowtie. Reference --------- https://gist.github.com/carlsmith/800cbe3e11f630ac8aa0 """ import sys import inspect from subprocess import call import click from bowtie._compat import numargs class WrongNumberOfArguments(TypeError): """The "build" function accepts an incorrect number of arguments.""" pass 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='[-p ] [--help]') @click.option('--path', '-p', default='build', type=str, help='Path to build the app.') @click.pass_context def cmd(ctx, path): """Bowtie CLI to help build and run your app.""" ctx.obj = path # pylint: disable=unused-variable @cmd.command(add_help_option=False) @click.pass_context def build(ctx): """Write the app, downloads the packages, and bundles it with Webpack.""" nargs = numargs(func) if nargs == 0: func() elif nargs == 1: func(ctx.obj) else: raise WrongNumberOfArguments( 'Function "{}" should have 0 or 1 argument, it has {}.' .format(func.__name__, nargs) ) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) @click.pass_context def serve(ctx, extra): """Serve the Bowtie app.""" line = ('./{}/src/server.py'.format(ctx.obj),) + extra call(line) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) @click.pass_context def dev(ctx, extra): """Recompile the app for development.""" line = ('webpack', '-d') + extra call(line, cwd=ctx.obj) @cmd.command(context_settings=dict(ignore_unknown_options=True), add_help_option=False) @click.argument('extra', nargs=-1, type=click.UNPROCESSED) @click.pass_context def prod(ctx, extra): """Recompile the app for production.""" line = ('webpack', '--define', 'process.env.NODE_ENV="production"', '--progress') + extra call(line, cwd=ctx.obj) locale = inspect.stack()[1][0].f_locals module = locale.get("__name__", None) if module == "__main__": try: arg = sys.argv[1:] except IndexError: arg = ('--help',) # pylint: disable=no-value-for-parameter sys.exit(cmd(arg)) return cmd PKdJ/',bowtie/_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 PKpJmJJbowtie/_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 inspect 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 def varname(variable): """Return the name of the given variable.""" frame = inspect.stack()[2][0] for name, var in frame.f_locals.items(): if variable is var: return name for name, var in frame.f_globals.items(): if variable is var: return name 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.Index): 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) 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.Index): 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.""" return bytes(msgpack.packb(x, default=encoders)) 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}#{event}'.format(uuid=self._uuid, event=name) objname = varname(self) try: # the getter post processing function # is preserved with an underscore getter = event(self).__name__ except AttributeError: getter = None return ename, objname, 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}#{event}'.format(uuid=self._uuid, 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}#{event}'.format(uuid=self._uuid, 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) # 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__() PKJ u<<bowtie/_layout.py# -*- coding: utf-8 -*- """Defines the Layout class.""" from __future__ import print_function import os from os import path from itertools import product import inspect import shutil import stat from collections import namedtuple, defaultdict, OrderedDict from subprocess import Popen from flask import Markup from jinja2 import Environment, FileSystemLoader from markdown import markdown from bowtie._compat import makedirs from bowtie.control import _Controller from bowtie.visual import _Visual _Import = namedtuple('_Import', ['module', 'component']) _Control = namedtuple('_Control', ['instantiate', 'caption']) _Schedule = namedtuple('_Control', ['seconds', 'function']) 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(Exception): """Invalid index into the grid layout.""" pass class NoUnusedCellsError(Exception): """All cells are used.""" pass class UsedCellsError(Exception): """All cells are used.""" pass class NoSidebarError(Exception): """Cannot add to the sidebar when it doesn't exist.""" pass 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 2 to then ends because they start counting from 1 # and they are exclusive if row_end is None: self.row_end = row_start + 2 else: self.row_end = row_end + 2 if column_end is None: self.column_end = column_start + 2 else: self.column_end = column_end + 2 class Size(object): """Size of rows and columns in grid. 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. """ 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 Layout(object): """Core class to layout, connect, build a Bowtie app.""" def __init__(self, rows=1, columns=1, sidebar=True, title='Bowtie App', description='Bowtie App\n---', basic_auth=False, username='username', password='password', background_color='White', directory='build', host='0.0.0.0', port=9991, debug=False): """Create a Bowtie App. Parameters ---------- title : str, optional Title of the HTML. description : str, optional Describe the app in Markdown, inserted in control pane. basic_auth : bool, optional Enable basic authentication. username : str, optional Username for basic authentication. password : str, optional Password for basic authentication. background_color : str, optional Background color of the control pane. directory : str, optional Location where app is compiled. host : str, optional Host IP address. port : int, optional Host port number. debug : bool, optional Enable debugging in Flask. Disable in production! """ self.background_color = background_color self.basic_auth = basic_auth self.controllers = [] self.debug = debug self.description = Markup(markdown(description)) self.directory = directory self.functions = [] self.host = host self.imports = set() self.init = None self.packages = set([]) self.password = password self.port = port self.schedules = [] self.subscriptions = defaultdict(list) self.templates = set(['progress.jsx']) self.title = title self.username = username self.used = OrderedDict(((key, False) for key in product(range(rows), range(columns)))) self.widgets = [] self.spans = [] self.rows = [Size() for _ in range(rows)] self.columns = [Size() for _ in range(columns)] self.sidebar = sidebar def add(self, widget, row_start=None, column_start=None, row_end=None, column_end=None): """Add a widget to the grid. Parameters ---------- visual : bowtie._Component A Bowtie widget instance. next_row : bool, optional Add this visual to the next row. min_width : number, optional Minimum width of the visual in pixels. min_height : number, optional Minimum height of the visual in pixels. """ for index in [row_start, row_end]: if index is not None and (index < 0 or index >= len(self.rows)): raise GridIndexError('Invalid Row Index') for index in [column_start, column_end]: if index is not None and (index < 0 or index >= len(self.columns)): raise GridIndexError('Invalid Column Index') if row_start is not None and row_end is not None and row_start > row_end: raise GridIndexError('Invalid Column Index') if column_start is not None and column_end is not None and column_start > column_end: raise GridIndexError('Invalid Column Index') # 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: 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 if column_end is None: column_end = column_end for row, col in product(range(row_start, row_end + 1), range(column_start, column_end + 1)): 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 + 1), range(column_start, column_end + 1)): self.used[row_start, column_start] = True span = Span(row_start, column_start, row_end, column_end) self.widgets.append(widget) self.spans.append(span) def add_sidebar(self, control): """Add a controller to the sidebar. Parameters ---------- control : bowtie._Controller A Bowtie controller instance. """ if not self.sidebar: raise NoSidebarError('Set sidebar=True if you want to use the sidebar.') assert isinstance(control, _Controller) # pylint: disable=protected-access self.packages.add(control._PACKAGE) self.templates.add(control._TEMPLATE) self.imports.add(_Import(component=control._COMPONENT, module=control._TEMPLATE[:control._TEMPLATE.find('.')])) self.controllers.append(_Control(instantiate=control._instantiate, caption=control.caption)) 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 -------- >>> dd = Dropdown() >>> slide = Slider() >>> def callback(dd_item, slide_value): >>> pass >>> layout.subscribe(callback, dd.on_change, slide.on_change) """ all_events = [event] all_events.extend(events) 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__)) def build(self): """Compile the Bowtie application.""" file_dir = path.dirname(__file__) env = Environment( loader=FileSystemLoader(path.join(file_dir, 'templates')), trim_blocks=True, lstrip_blocks=True ) server = env.get_template('server.py.j2') index = env.get_template('index.html.j2') react = env.get_template('index.jsx.j2') src, app, templates = create_directories(directory=self.directory) webpack_src = path.join(file_dir, 'src/webpack.config.js') shutil.copy(webpack_src, self.directory) server_path = path.join(src, server.name[:-3]) # [1] grabs the parent stack and [1] grabs the filename source_filename = inspect.stack()[1][1] with open(server_path, 'w') as f: f.write( server.render( basic_auth=self.basic_auth, username=self.username, password=self.password, source_module=os.path.basename(source_filename)[:-3], subscriptions=self.subscriptions, schedules=self.schedules, initial=self.init, 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) with open(path.join(templates, index.name[:-3]), 'w') as f: f.write( index.render(title=self.title) ) for template in self.templates: template_src = path.join(file_dir, 'src', template) shutil.copy(template_src, app) for i, widget in enumerate(self.widgets): # pylint: disable=protected-access winst = widget._instantiate() if isinstance(widget, _Visual): progress = widget.progress._instantiate() close_progress = '' self.widgets[i] = ''.join((progress, winst, close_progress)) else: self.widgets[i] = winst columns = [] if self.sidebar: columns.append('18em') columns += self.columns with open(path.join(app, react.name[:-3]), 'w') as f: f.write( react.render( description=self.description, sidebar=self.sidebar, columns=columns, rows=self.rows, background_color=self.background_color, components=self.imports, controls=self.controllers, widgets=zip(self.widgets, self.spans) ) ) init = Popen('yarn init -y', shell=True, cwd=self.directory).wait() if init != 0: raise YarnError('Error running "yarn init -y"') self.packages.discard(None) packages = path.join(file_dir, 'src/package.json') shutil.copy(packages, self.directory) install = Popen('yarn add package.json', shell=True, cwd=self.directory).wait() if install > 1: raise YarnError('Error install node packages') packages = ' '.join(self.packages) install = Popen('yarn add {}'.format(packages), shell=True, cwd=self.directory).wait() if install > 1: raise YarnError('Error install node packages') elif install == 1: print('Yarn error but trying to continue build') dev = Popen('webpack -d', shell=True, cwd=self.directory).wait() if dev != 0: raise WebpackError('Error building with webpack') def create_directories(directory='build'): """Create all the necessary subdirectories for the build.""" src = path.join(directory, 'src') templates = path.join(src, 'templates') app = path.join(src, 'app') makedirs(app, exist_ok=True) makedirs(templates, exist_ok=True) return src, app, templates PKdJi;" " bowtie/_progress.py# -*- coding: utf-8 -*- """Progress component. Not for direct use by user. """ from bowtie._component import Component class Progress(Component): """A progress indicator. This component is used by all visual components and 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 -------- >>> 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/ """ _TEMPLATE = 'progress.jsx' _COMPONENT = 'AntProgress' _PACKAGE = None _TAG = ('') def _instantiate(self): return self._TAG.format( uuid="'{}'".format(self._uuid) ) # 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 PKdJEuubowtie/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 save(key, value): """Store the key value pair. Parameters ---------- key : object 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 """ 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() def load(key): """Load the value stored with the key. Parameters ---------- key : object The key to lookup the value stored. Returns ------- object The value if the key exists in the cache, otherwise None. """ 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') PKdJGFFbowtie/control.py# -*- coding: utf-8 -*- """Control components.""" from collections import Iterable from bowtie._component import Component, jdumps def _jsbool(x): """Convert Python bool to Javascript bool.""" return repr(x).lower() # 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. """ pass class Button(_Controller): """An Ant design button.""" _TEMPLATE = 'button.jsx' _COMPONENT = 'SimpleButton' _PACKAGE = None _TAG = ('') def __init__(self, label='', caption=''): """Create a button. Parameters ---------- label : str, optional Label on the button. caption : str, optional Heading text. """ super(Button, self).__init__() self._instantiate = self._TAG.format( label="'{}'".format(label), uuid="'{}'".format(self._uuid) ) self.caption = caption def on_click(self): """Emit an event when the button is clicked. | **Payload:** ``None``. Returns ------- str Name of click event. """ pass class Dropdown(_Controller): """Dropdown based on react-select.""" _TEMPLATE = 'dropdown.jsx' _COMPONENT = 'Dropdown' _PACKAGE = 'react-select@1.0.0-rc.3' _TAG = ('') def __init__(self, labels=None, values=None, multi=False, default=None, caption=''): """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__() 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._instantiate = self._TAG.format( options=jdumps(options), multi='true' if multi else 'false', default=jdumps(default), uuid="'{}'".format(self._uuid) ) self.caption = caption 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 = 'antd' _TAG = ('') def __init__(self, initial=False, caption=''): """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__() self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), defaultChecked=_jsbool(initial) ) self.caption = caption 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 = 'antd' _TAG = ('') def __init__(self, date_type=False, month_type=False, range_type=False, caption=''): super(_DatePickers, self).__init__() self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), date_type=_jsbool(date_type), month_type=_jsbool(month_type), range_type=_jsbool(range_type) ) self.caption = caption class DatePicker(_DatePickers): """A Date Picker. Let's you choose an individual day. """ def __init__(self, caption=''): """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=''): """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=''): """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 = 'antd' _TAG = ('') def __init__(self, start=0, minimum=-1e100, maximum=1e100, step=1, size='default', caption=''): """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 textbox. caption : str, optional Heading text. References ---------- https://ant.design/components/input/ """ super(Number, self).__init__() self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), start=start, minimum=minimum, maximum=maximum, step=step, size="'{}'".format(size) ) self.caption = caption 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 = 'antd' _TAG = ('') def __init__(self, placeholder='Enter text', size='default', caption=''): """Create a textbox. Parameters ---------- placeholder : str, optional Initial text that appears. size : 'default', 'large', 'small', optional Size of the textbox. caption : str, optional Heading text. References ---------- https://ant.design/components/input/ """ super(Textbox, self).__init__() self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), placeholder="'{}'".format(placeholder), size="'{}'".format(size) ) self.caption = caption def on_enter(self): """Emit an event when enter is pressed in the textbox. | **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 = 'antd' _TAG = ('') def __init__(self, start=None, ranged=False, minimum=0, maximum=100, step=1, caption=''): """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. caption : str, optional Heading text. References ---------- https://ant.design/components/slider/ """ super(Slider, self).__init__() if not start: start = [0, 0] if ranged else 0 elif isinstance(start, Iterable): start = list(start) ranged = True self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), range=_jsbool(ranged), minimum=minimum, maximum=maximum, start=start, step=step, marks={minimum: str(minimum), maximum: str(maximum)} ) self.caption = caption 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 # 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 class Nouislider(_Controller): """A lightweight JavaScript range slider library.""" _TEMPLATE = 'nouislider.jsx' _COMPONENT = 'Nouislider' _PACKAGE = 'nouislider@9.2.0' _TAG = ('') def __init__(self, start=0, minimum=0, maximum=100, tooltips=True, caption=''): """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__() if not isinstance(start, Iterable): start = [start] else: start = list(start) self._instantiate = self._TAG.format( uuid="'{}'".format(self._uuid), min=minimum, max=maximum, start=start, tooltips='true' if tooltips else 'false' ) self.caption = caption 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 PKdJZtc<'<'bowtie/visual.py# -*- coding: utf-8 -*- """Visual components.""" from bowtie._component import Component, jdumps 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__() class Table(_Visual): """Ant Design table with filtering and sorting.""" _TEMPLATE = 'table.jsx' _COMPONENT = 'AntTable' _PACKAGE = None _TAG = ('') def __init__(self, data=None, columns=None, results_per_page=10): """Create a table and optionally intialize 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. """ 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 super(Table, self).__init__() def _instantiate(self): return self._TAG.format( uuid="'{}'".format(self._uuid), 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@version0' _TAG = ('') def __init__(self, columns=None, results_per_page=10): """Create the table, optionally set the columns. Parameters ---------- columns : list, optional List of column names to display. results_per_page : int, optional Number of rows on each pagination of the table. """ if columns is None: columns = [] self.columns = columns self.results_per_page = results_per_page super(SmartGrid, self).__init__() def _instantiate(self): return self._TAG.format( uuid="'{}'".format(self._uuid), columns=jdumps(self.columns), results_per_page=self.results_per_page ) # pylint: disable=no-self-use def do_update(self, data): """Update the data of the table. Parameters ---------- data : list of dicts Each entry in the list must be a dict with the same keys which are the columns of the table. Returns ------- None """ return data 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 _TAG = ('') 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. """ self.preserve_aspect_ratio = preserve_aspect_ratio super(SVG, self).__init__() def _instantiate(self): return self._TAG.format( uuid="'{}'".format(self._uuid), preserve_aspect_ratio='true' if self.preserve_aspect_ratio else 'false' ) # 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 -------- >>> 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('>> s = s[idx:] >>> image.do_image(s) """ return image class Plotly(_Visual): """Plotly component. Useful for many kinds of plots. """ _TEMPLATE = 'plotly.jsx' _COMPONENT = 'PlotlyPlot' _PACKAGE = 'plotly.js' _TAG = ('') def __init__(self, init=None): """Create a Plotly component. Parameters ---------- init : dict, optional Initial Plotly data to plot. """ if init is None: init = dict(data=[], layout={'autosize': False}) self.init = init super(Plotly, self).__init__() def _instantiate(self): return self._TAG.format( uuid="'{}'".format(self._uuid), init=jdumps(self.init), ) ## Events def on_click(self): """Plotly click event. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_click def on_beforehover(self): """Emit an event before hovering over a point. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_hover(self): """Emit an event after hovering over a point. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_unhover(self): """Emit an event when hover is removed. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_hover def on_select(self): """Emit an event when points are selected with a tool. | **Payload:** TODO. Returns ------- str Name of event. """ return self.get_select ## Commands # pylint: disable=no-self-use def do_all(self, plot): """Replace the entire plot. Parameters ---------- plot : dict Dict that can be plotted with Plotly. It should have this structure: ``{data: [], layout: {}}``. Returns ------- None """ return plot def do_data(self, data): """Replace the data portion of the plot. Parameters ---------- data : list of traces List of data to replace the old data. Returns ------- None """ return data def do_layout(self, layout): """Update the layout. Parameters ---------- layout : dict Contains layout information. Returns ------- None """ return layout def do_config(self, config): """Update the configuration of the plot. Parameters ---------- config : dict Plotly config information. Returns ------- None """ return config def get(self, data): """ Get the current selection of points. Returns ------- list """ return data def get_select(self, data): """ Get the current selection of points. Returns ------- list """ return data def get_click(self, data): """ Get the current selection of points. Returns ------- list """ return data def get_hover(self, data): """ Get the current selection of points. Returns ------- list """ return data PKUgJ*Ơbowtie/src/button.jsximport React from 'react'; import { Button } from 'antd'; import 'antd/dist/antd.css'; export default class SimpleButton extends React.Component { constructor(props) { super(props); } handleClick = event => { this.props.socket.emit(this.props.uuid + '#click'); } render() { return ( ); } } SimpleButton.propTypes = { label: React.PropTypes.string.isRequired, uuid: React.PropTypes.string.isRequired, socket: React.PropTypes.object.isRequired }; PKUgJnmGGbowtie/src/datagrid.jsximport 'react-datagrid/index.css'; 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: React.PropTypes.string.isRequired, socket: React.PropTypes.object.isRequired }; PKUgJ bowtie/src/date.jsximport React from 'react'; import { DatePicker, LocaleProvider } from 'antd'; import enUS from 'antd/lib/locale-provider/en_US'; import 'antd/dist/antd.css'; // import 'antd/lib/date-picker/style/index.css'; const { MonthPicker, RangePicker } = DatePicker; var msgpack = require('msgpack-lite'); export default class PickDates extends React.Component { constructor(props) { super(props); this.state = {value: null}; } handleChange = (moment, ds) => { this.setState({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: React.PropTypes.string.isRequired, socket: React.PropTypes.object.isRequired, date: React.PropTypes.bool.isRequired, month: React.PropTypes.bool.isRequired, range: React.PropTypes.bool.isRequired, }; PKdJlbowtie/src/dropdown.jsximport 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'; var msgpack = require('msgpack-lite'); export default class Dropdown extends React.Component { constructor(props) { super(props); this.state = {value: this.props.default, options: this.props.initOptions}; } handleChange = value => { this.setState({value}); this.props.socket.emit(this.props.uuid + '#change', msgpack.encode(value)); } choose = data => { var arr = new Uint8Array(data['data']); this.setState({value: arr}); } newOptions = data => { var arr = new Uint8Array(data['data']); this.setState({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: React.PropTypes.string.isRequired, socket: React.PropTypes.object.isRequired, placeholder: React.PropTypes.string.isRequired, size: React.PropTypes.string.isRequired, }; PK5wzJ#WD D bowtie/src/webpack.config.jsconst 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 config = { // context: path.resolve(__dirname, './src'), context: __dirname, entry: APP_DIR + '/index.jsx', output: { path: BUILD_DIR, filename: 'bundle.js' }, module: { rules: [ { test: /\.(js|jsx)$/, include: APP_DIR, loader: 'babel-loader', exclude: /node_modules/, query: { presets: [ ['latest', { 'modules': false}], 'react', 'stage-0'], plugins: [ 'transform-object-rest-spread', ] } }, { test: /\.scss$/, loaders: ['style-loader', 'css-loader', 'sass-loader'], }, { test: /\.css$/, loader: 'style-loader!css-loader!sass-loader', }, { test: /\.less$/, loader: 'style-loader!css-loader!less-loader?strictMath&noIeCompat&', }, ], 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; PKJպbowtie/templates/index.html.j2 {{ title }}
PKJS bowtie/templates/index.jsx.j2import 'normalize.css'; import React from 'react'; import {render} from 'react-dom'; import io from 'socket.io-client'; import 'antd/dist/antd.css' import AntProgress from './progress'; {% for component in components %} import {{ component.component }} from './{{ component.module }}'; {% endfor %} var msgpack = require('msgpack-lite'); var 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); this.cache[key] = data['data']; } loadValue = (data, fn) => { var arr = new Uint8Array(data['data']); var key = msgpack.decode(arr); if (this.cache.hasOwnProperty(key)) { fn(this.cache[key]); } else { var buffer = new ArrayBuffer(1); var x = new DataView(buffer, 0); // msgpack encodes null to 0xc0 x.setUint8(0, 0xc0); fn(buffer); } } componentDidMount() { socket.on('cache_save', this.saveValue); socket.on('cache_load', this.loadValue); } render() { return (
{% if sidebar %}
{{ description }} {% for control in controls %}
{{ control.caption }}
{{ control.instantiate }}
{% endfor %}
{% endif %} {% for widget, span in widgets %}
{{ widget }}
{% endfor %}
); } } // // minWidth: '{{ min_width }}px', render(, document.getElementById('app')); PKJN $$bowtie/templates/server.py.j2#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import traceback from functools import wraps from builtins import bytes import click import msgpack import flask from flask import Flask, render_template, copy_current_request_context from flask import request, Response from flask_socketio import SocketIO, emit import eventlet 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) # 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() @app.route('/') {% 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' 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 event, functions in subscriptions.items() %} # @socketio.on({{ event }}) # def _(*args): # {% for func in functions %} # foo = copy_current_request_context({{ source_module }}.{{ func }}) # eventlet.spawn(foo, *(msgpack.unpackb(bytes(a['data']), encoding='utf8') for a in args)) # {% endfor %} # {% endfor %} #} {# {% for event, (events, functions) in subscriptions.items() %} #} {% 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 }}) {% set post = event[2] %} event_data = {} for ev in uniq_events: comp = getattr({{ source_module }}, 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 post is not none %} event_data['{{ event[0] }}'] = {{ source_module }}.{{event[1]}}.{{ '_' ~ post }}( msgpack.unpackb(bytes(args[0]['data']), encoding='utf8') ) {% endif %} {% 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 %} {{ 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): scheds = [] {% for schedule in schedules %} sched = Scheduler({{ schedule.seconds }}, {{ source_module }}.{{ schedule.function }}) scheds.append(sched) {% endfor %} for sched in scheds: sched.start() socketio.run(app, host=host, port=port) for sched in scheds: sched.stop() if __name__ == '__main__': main() PKdJBxbowtie/tests/__init__.py"""Test directory.""" PKdJh844bowtie/tests/conftest.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Pytest configuration.""" import os import shutil import pytest @pytest.fixture def remove_build(): """Remove build directory after use.""" yield path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'build') shutil.rmtree(path) PKpJc.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 PKpJE܅bowtie/tests/test_compile.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Compile tests.""" import os from bowtie import Layout from bowtie.control import Nouislider from bowtie.visual import Plotly def callback(*args): """dummy function""" # pylint: disable=unused-argument pass # pylint: disable=unused-argument def test_build(remove_build): """Tests the build process.""" ctrl = Nouislider() viz = Plotly() path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'build') layout = Layout(directory=path) layout.add_sidebar(ctrl) layout.add(viz) layout.subscribe(callback, ctrl.on_change) layout.build() PKpJm̥bowtie/tests/test_plotly.py#!/usr/bin/env python # -*- coding: utf-8 -*- """Plotly testing.""" import os import subprocess import time from selenium.webdriver import PhantomJS # from selenium.webdriver import ActionChains from bowtie import Layout from bowtie.control import Nouislider, Button from bowtie.visual import Plotly def callback(*args): """dummy function""" # pylint: disable=unused-argument pass # pylint: disable=unused-argument def test_plotly(remove_build): """Tests plotly.""" viz = Plotly() ctrl = Nouislider() ctrl2 = Button() path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'build') layout = Layout(directory=path) layout.add(viz) layout.add_sidebar(ctrl) layout.add_sidebar(ctrl2) layout.subscribe(callback, ctrl.on_change) layout.subscribe(callback, ctrl2.on_click) layout.build() env = os.environ env['PYTHONPATH'] = '{}:{}'.format(os.getcwd(), os.environ.get('PYTHONPATH', '')) server = subprocess.Popen(os.path.join(path, 'src/server.py'), env=env) time.sleep(5) driver = PhantomJS() driver.get('http://localhost:9991') assert driver.title == 'Bowtie App' server.kill() PKpJPbowtie/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!H|&Ubbowtie-0.3.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,Q034 /, (-JLR()*M ILR(4KM̫#DPK!HdqJ%bowtie-0.3.1.dist-info/METADATAn0E 醙}H^R zHdHl?i>sif KZ ޏ,uНTiJ[Zi],Fůu`o۬OaMJbÃӵN~J&9T疨)<qPvNc_Vtõ KɣY%IN$ìϓ=W1bɗq׼X>3UqwQK镙9 ?A| 6׬AdX Kw7o]BaַΡ22)Mu֩/_Yԛw˟i2QvB\~haVws*7-{k0b/~\ cNRKgi9LA?r1H뢟Fp|5U^$X*`CVK|L)$b“FLuŜ֙NYPϖe=FLUBc׺0.3ܶTS_Lf*U(]K#')z ڱZCTug2ԾrhJIMeWTJ\PVy1Í+9(/<(sLcl!˭pdLxMVzM4X'lORmu"xT_au9[q}Jt%+nJU aP$DУˁk+~t֣X:uSt%4\y?jG?͛g2/ʬ& `:Z*(uո5ҩmZn/x(q Kc>܁x ֘mf&ay\S+ۥa[J4,mB%NTKP*zF{2!mE}cZw6_yϏXW>ȂP/@"gzN-Dн%bȟ-#c5ANӒ5Sٯ|hVyyTJ֝ WܩeZEs@]%$l7o~%LCh)IS)\)rL(l}k%ZƏ0Y;^Ǖ)^s̎I::be[;ϝ,AQJȍ $@,YƩ;}!xȼ/dHoapPLE֐ *وۅt@`3bܨDh>jm:W^(cn *OZD6d?s8L] U1AdRˑCHA?|e/s 0]ODŽ׭peGq2{)h-O̚<|Z?gٖљϱh¡"*2"`u'PKJ=bowtie/__init__.pyPKdJzj 큳bowtie/_command.pyPKdJ/', bowtie/_compat.pyPKpJmJJbowtie/_component.pyPKJ u<<,(bowtie/_layout.pyPKdJi;" " Zebowtie/_progress.pyPKdJEuuobowtie/cache.pyPKdJGFFOubowtie/control.pyPKdJZtc<'<'bowtie/visual.pyPKUgJ*Ơbowtie/src/button.jsxPKUgJnmGGGbowtie/src/datagrid.jsxPKUgJ bowtie/src/date.jsxPKdJlbowtie/src/dropdown.jsxPKUgJ-Q ybowtie/src/fixedtable.jsxPKUgJzbowtie/src/griddle.jsxPKyR{JZNll bowtie/src/nouislider.jsxPK|kJ*ڼ`bowtie/src/number.jsxPKdJX3O%bowtie/src/package.jsonPK5wzJKA )bowtie/src/plotly.jsxPKdJ"<bowtie/src/progress.jsxPKUgJeٚ#Ebowtie/src/slider.jsxPKUgJ6Kbowtie/src/svg.jsxPKUgJd Qbowtie/src/switch.jsxPK|kJ~oyyVbowtie/src/table.jsxPK|kJOr2\bowtie/src/textbox.jsxPK5wzJ#WD D bbowtie/src/webpack.config.jsPKJպ9nbowtie/templates/index.html.j2PKJS gobowtie/templates/index.jsx.j2PKJN $$Z{bowtie/templates/server.py.j2PKdJBxbowtie/tests/__init__.pyPKdJh844bowtie/tests/conftest.pyPKpJc.obowtie/tests/test_compat.pyPKpJE܅큍bowtie/tests/test_compile.pyPKpJm̥Lbowtie/tests/test_plotly.pyPKpJP*bowtie/tests/test_serialize.pyPK!H|&Ub:bowtie-0.3.1.dist-info/WHEELPK!HdqJ%ɠbowtie-0.3.1.dist-info/METADATAPK!H Pbowtie-0.3.1.dist-info/RECORDPK&&9 X