From 3560e974c4f4773eec6cd295be6252cd89010880 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 13 May 2017 15:24:31 +0200 Subject: [PATCH 001/143] Allow processors to retrieve their own value from the yield value, making it one line shorter each time the processor does not need its value before the teardown phase. --- bonobo/config/processors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 2d3de8f..f7bd601 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -49,6 +49,7 @@ class ContextCurrifier: self.wrapped = wrapped self.context = tuple(initial_context) self._stack = [] + self._stack_values = [] def setup(self, *context): if len(self._stack): @@ -56,6 +57,7 @@ class ContextCurrifier: for processor in resolve_processors(self.wrapped): _processed = processor(self.wrapped, *context, *self.context) _append_to_context = next(_processed) + self._stack_values.append(_append_to_context) if _append_to_context is not None: self.context += ensure_tuple(_append_to_context) self._stack.append(_processed) @@ -68,7 +70,7 @@ class ContextCurrifier: processor = self._stack.pop() try: # todo yield from ? how to ? - next(processor) + processor.send(self._stack_values.pop()) except StopIteration as exc: # This is normal, and wanted. pass From dd1c49fde091cba1723d46c2b848a4b563494b44 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 13 May 2017 15:25:22 +0200 Subject: [PATCH 002/143] File writers now return NOT_MODIFIED to allow chaining things after them. --- bonobo/io/csv.py | 6 +++++- bonobo/io/file.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bonobo/io/csv.py b/bonobo/io/csv.py index 8640633..30ad3a4 100644 --- a/bonobo/io/csv.py +++ b/bonobo/io/csv.py @@ -2,6 +2,7 @@ import csv from bonobo.config import Option from bonobo.config.processors import ContextProcessor, contextual +from bonobo.constants import NOT_MODIFIED from bonobo.util.objects import ValueHolder from .file import FileHandler, FileReader, FileWriter @@ -54,7 +55,7 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) yield dict(zip(headers.value, row)) @@ -72,3 +73,6 @@ class CsvWriter(CsvHandler, FileWriter): writer.writerow(headers.value) writer.writerow(row[header] for header in headers.value) lineno.value += 1 + return NOT_MODIFIED + + diff --git a/bonobo/io/file.py b/bonobo/io/file.py index 8337490..3298fd9 100644 --- a/bonobo/io/file.py +++ b/bonobo/io/file.py @@ -1,6 +1,7 @@ from bonobo.config import Option, Service from bonobo.config.configurables import Configurable from bonobo.config.processors import ContextProcessor, contextual +from bonobo.constants import NOT_MODIFIED from bonobo.util.objects import ValueHolder __all__ = [ @@ -94,6 +95,7 @@ class FileWriter(Writer): """ self._write_line(file, (self.eol if lineno.value else '') + row) lineno.value += 1 + return NOT_MODIFIED def _write_line(self, file, line): return file.write(line) From 6b3e5932e82c1f1119b9ca2c150ee9a0fb3ab0d3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 13 May 2017 15:25:57 +0200 Subject: [PATCH 003/143] Do not display transformations as finished before the teardown phase has completed. --- bonobo/execution/base.py | 8 +++++--- bonobo/execution/node.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index 85666ac..e1b9bb0 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -81,10 +81,12 @@ class LoopingExecutionContext(Wrapper): if self._stopped: return - self._stopped = True + try: + with unrecoverable(self.handle_error): + self._stack.teardown() + finally: + self._stopped = True - with unrecoverable(self.handle_error): - self._stack.teardown() def handle_error(self, exc, trace): return print_error(exc, trace, context=self.wrapped) diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 5969edd..02a866e 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -22,7 +22,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): @property def alive(self): """todo check if this is right, and where it is used""" - return self.input.alive and self._started and not self._stopped + return self._started and not self._stopped @property def alive_str(self): From 697b3e539e061aefcf8690849f3890b34d3571b5 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 13 May 2017 15:26:24 +0200 Subject: [PATCH 004/143] Rename component to node in console plugin for more consistency with naming elsewhere. --- bonobo/ext/console/plugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bonobo/ext/console/plugin.py b/bonobo/ext/console/plugin.py index 63497f9..d76b3af 100644 --- a/bonobo/ext/console/plugin.py +++ b/bonobo/ext/console/plugin.py @@ -73,20 +73,20 @@ class ConsoleOutputPlugin(Plugin): def write(context, prefix='', rewind=True, append=None, debug=False, profile=False): t_cnt = len(context) - for i, component in enumerate(context): - if component.alive: + for i, node in enumerate(context): + if node.alive: _line = ''.join( ( Fore.BLACK, '({})'.format(i + 1), Style.RESET_ALL, ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', - component.name, ' ', component.get_statistics_as_string(debug=debug, + node.name, ' ', node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', ) ) else: _line = ''.join( ( - Fore.BLACK, '({})'.format(i + 1), ' - ', component.name, ' ', - component.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', + Fore.BLACK, '({})'.format(i + 1), ' - ', node.name, ' ', + node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', ) ) print(prefix + _line + '\033[0K') From aa6da3aa2b7d4781ec0c3d94ea68c11d75b76506 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 13 May 2017 15:26:51 +0200 Subject: [PATCH 005/143] Allow to specify output of a chain in the Graph class. --- bonobo/structs/graphs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index d2d755e..8fcc0e6 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -21,11 +21,17 @@ class Graph: self.nodes.append(c) return i - def add_chain(self, *nodes, _input=BEGIN): + def add_chain(self, *nodes, _input=BEGIN, _output=None): for node in nodes: _next = self.add_node(node) self.outputs_of(_input, create=True).add(_next) _input = _next + if _output: + if not _output in self.nodes: + raise ValueError('Output not found.') + self.outputs_of(_input, create=True).add(self.nodes.index(_output)) + + return self def __len__(self): return len(self.nodes) From e747bc1da8cb3c42c2a23435be8db2681c480c6c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 19 May 2017 13:28:31 +0200 Subject: [PATCH 006/143] Topological sort of a graph, allowing better console (and other) outputs. Uses algorithm borrowed from networkx graph library to sort a graph in topological order. The method is only used by output plugins, as internal plumbery does not really care about the node order. Also includes a bonobo.util.python.require function that helps importing thing in a package-less context, or when there are conflict with site package names. --- .gitignore | 1 + bonobo/config/configurables.py | 5 ++ bonobo/examples/__init__.py | 18 +++-- bonobo/execution/base.py | 1 - bonobo/execution/graph.py | 4 +- bonobo/ext/console/plugin.py | 10 +-- bonobo/io/csv.py | 4 +- bonobo/structs/bags.py | 6 +- bonobo/structs/graphs.py | 116 +++++++++++++++++++++++++++----- bonobo/util/objects.py | 4 +- bonobo/util/python.py | 22 ++++++ setup.py | 37 +++++----- tests/structs/test_bags.py | 14 ++-- tests/structs/test_graphs.py | 29 ++++++++ tests/util/requireable/dummy.py | 1 + tests/util/test_python.py | 6 ++ 16 files changed, 214 insertions(+), 64 deletions(-) create mode 100644 bonobo/util/python.py create mode 100644 tests/util/requireable/dummy.py create mode 100644 tests/util/test_python.py diff --git a/.gitignore b/.gitignore index a88bd7b..d48b40b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ /examples/private /htmlcov/ /sdist/ +/tags celerybeat-schedule parts/ pip-delete-this-directory.txt diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index c1486ee..64b4adc 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -85,3 +85,8 @@ class Configurable(metaclass=ConfigurableMeta): # set option values. for name, value in kwargs.items(): setattr(self, name, value) + + def __call__(self, *args, **kwargs): + """ You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override. + """ + return self.call(*args, **kwargs) diff --git a/bonobo/examples/__init__.py b/bonobo/examples/__init__.py index cf3d84d..49b1544 100644 --- a/bonobo/examples/__init__.py +++ b/bonobo/examples/__init__.py @@ -5,9 +5,19 @@ def require(package, requirement=None): return __import__(package) except ImportError: from colorama import Fore, Style - print(Fore.YELLOW, 'This example requires the {!r} package. Install it using:'.format(requirement), - Style.RESET_ALL, sep='') + print( + Fore.YELLOW, + 'This example requires the {!r} package. Install it using:'. + format(requirement), + Style.RESET_ALL, + sep='' + ) print() - print(Fore.YELLOW, ' $ pip install {!s}'.format(requirement), Style.RESET_ALL, sep='') + print( + Fore.YELLOW, + ' $ pip install {!s}'.format(requirement), + Style.RESET_ALL, + sep='' + ) print() - raise \ No newline at end of file + raise diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index e1b9bb0..6ca22f2 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -87,7 +87,6 @@ class LoopingExecutionContext(Wrapper): finally: self._stopped = True - def handle_error(self, exc, trace): return print_error(exc, trace, context=self.wrapped) diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 2e55492..1f2671a 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -21,7 +21,7 @@ class GraphExecutionContext: def __init__(self, graph, plugins=None, services=None): self.graph = graph - self.nodes = [NodeExecutionContext(node, parent=self) for node in self.graph.nodes] + self.nodes = [NodeExecutionContext(node, parent=self) for node in self.graph] self.plugins = [PluginExecutionContext(plugin, parent=self) for plugin in plugins or ()] self.services = Container(services) if services else Container() @@ -65,4 +65,4 @@ class GraphExecutionContext: def stop(self): # todo use strategy for node in self.nodes: - node.stop() \ No newline at end of file + node.stop() diff --git a/bonobo/ext/console/plugin.py b/bonobo/ext/console/plugin.py index d76b3af..8884a73 100644 --- a/bonobo/ext/console/plugin.py +++ b/bonobo/ext/console/plugin.py @@ -73,19 +73,19 @@ class ConsoleOutputPlugin(Plugin): def write(context, prefix='', rewind=True, append=None, debug=False, profile=False): t_cnt = len(context) - for i, node in enumerate(context): + for i in context.graph.topologically_sorted_indexes: + node = context[i] if node.alive: _line = ''.join( ( - Fore.BLACK, '({})'.format(i + 1), Style.RESET_ALL, ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', - node.name, ' ', node.get_statistics_as_string(debug=debug, - profile=profile), Style.RESET_ALL, ' ', + ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', node.name, '(', str(i), ') ', + node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', ) ) else: _line = ''.join( ( - Fore.BLACK, '({})'.format(i + 1), ' - ', node.name, ' ', + ' ', Fore.BLACK, '-', ' ', node.name, '(', str(i), ') ', node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', ) ) diff --git a/bonobo/io/csv.py b/bonobo/io/csv.py index 30ad3a4..647925b 100644 --- a/bonobo/io/csv.py +++ b/bonobo/io/csv.py @@ -55,7 +55,7 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) yield dict(zip(headers.value, row)) @@ -74,5 +74,3 @@ class CsvWriter(CsvHandler, FileWriter): writer.writerow(row[header] for header in headers.value) lineno.value += 1 return NOT_MODIFIED - - diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index e1fc442..3414d00 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -43,7 +43,7 @@ class Bag: def args(self): if self._parent is None: return self._args - return (*self._parent.args, *self._args,) + return (*self._parent.args, *self._args, ) @property def kwargs(self): @@ -85,7 +85,7 @@ class Bag: @classmethod def inherit(cls, *args, **kwargs): - return cls(*args, _flags=(INHERIT_INPUT,), **kwargs) + return cls(*args, _flags=(INHERIT_INPUT, ), **kwargs) def __eq__(self, other): return isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs @@ -93,7 +93,7 @@ class Bag: def __repr__(self): return '<{} ({})>'.format( type(self).__name__, ', '. - join(itertools.chain( + join(itertools.chain( map(repr, self.args), ('{}={}'.format(k, repr(v)) for k, v in self.kwargs.items()), )) diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index 8fcc0e6..ccafb6b 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -3,35 +3,115 @@ from bonobo.constants import BEGIN class Graph: """ - Represents a coherent directed acyclic graph of components. + Represents a directed graph of nodes. """ def __init__(self, *chain): + self.edges = {BEGIN: set()} + self.named = {} self.nodes = [] - self.graph = {BEGIN: set()} self.add_chain(*chain) + def __iter__(self): + yield from self.nodes + + def __len__(self): + """ Node count. + """ + return len(self.nodes) + + def __getitem__(self, key): + return self.nodes[key] + def outputs_of(self, idx, create=False): - if create and not idx in self.graph: - self.graph[idx] = set() - return self.graph[idx] + """ Get a set of the outputs for a given node index. + """ + if create and not idx in self.edges: + self.edges[idx] = set() + return self.edges[idx] def add_node(self, c): - i = len(self.nodes) + """ Add a node without connections in this graph and returns its index. + """ + idx = len(self.nodes) + self.edges[idx] = set() self.nodes.append(c) - return i + return idx - def add_chain(self, *nodes, _input=BEGIN, _output=None): - for node in nodes: - _next = self.add_node(node) - self.outputs_of(_input, create=True).add(_next) - _input = _next - if _output: - if not _output in self.nodes: - raise ValueError('Output not found.') - self.outputs_of(_input, create=True).add(self.nodes.index(_output)) + def add_chain(self, *nodes, _input=BEGIN, _output=None, _name=None): + """ Add a chain in this graph. + """ + if len(nodes): + _input = self._resolve_index(_input) + _output = self._resolve_index(_output) + + for i, node in enumerate(nodes): + _next = self.add_node(node) + if not i and _name: + if _name in self.named: + raise KeyError('Duplicate name {!r} in graph.'.format(_name)) + self.named[_name] = _next + self.outputs_of(_input, create=True).add(_next) + _input = _next + + if _output is not None: + self.outputs_of(_input, create=True).add(_output) + + if hasattr(self, '_topologcally_sorted_indexes_cache'): + del self._topologcally_sorted_indexes_cache return self - def __len__(self): - return len(self.nodes) + @property + def topologically_sorted_indexes(self): + """Iterate in topological order, based on networkx's topological_sort() function. + """ + try: + return self._topologcally_sorted_indexes_cache + except AttributeError: + seen = set() + order = [] + explored = set() + + for i in self.edges: + if i in explored: + continue + fringe = [i] + while fringe: + w = fringe[-1] # depth first search + if w in explored: # already looked down this branch + fringe.pop() + continue + seen.add(w) # mark as seen + # Check successors for cycles and for new nodes + new_nodes = [] + for n in self.outputs_of(w): + if n not in explored: + if n in seen: # CYCLE !! + raise RuntimeError("Graph contains a cycle.") + new_nodes.append(n) + if new_nodes: # Add new_nodes to fringe + fringe.extend(new_nodes) + else: # No new nodes so w is fully explored + explored.add(w) + order.append(w) + fringe.pop() # done considering this node + self._topologcally_sorted_indexes_cache = tuple(filter(lambda i: type(i) is int, reversed(order))) + return self._topologcally_sorted_indexes_cache + + def _resolve_index(self, mixed): + """ Find the index based on various strategies for a node, probably an input or output of chain. Supported inputs are indexes, node values or names. + """ + if mixed is None: + return None + + if type(mixed) is int or mixed in self.edges: + return mixed + + if isinstance(mixed, str) and mixed in self.named: + return self.named[mixed] + + if mixed in self.nodes: + return self.nodes.index(mixed) + + raise ValueError('Cannot find node matching {!r}.'.format(mixed)) diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index 8b5db00..1e0015a 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -200,6 +200,9 @@ class ValueHolder: def __invert__(self): return ~self.value + def __len__(self): + return len(self.value) + def get_attribute_or_create(obj, attr, default): try: @@ -207,4 +210,3 @@ def get_attribute_or_create(obj, attr, default): except AttributeError: setattr(obj, attr, default) return getattr(obj, attr) - diff --git a/bonobo/util/python.py b/bonobo/util/python.py new file mode 100644 index 0000000..a496e19 --- /dev/null +++ b/bonobo/util/python.py @@ -0,0 +1,22 @@ +import inspect +import os +import runpy + + +class _RequiredModule: + def __init__(self, dct): + self.__dict__ = dct + + +class _RequiredModulesRegistry(dict): + def require(self, name): + if name not in self: + bits = name.split('.') + pathname = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.stack()[1][0]))) + filename = os.path.join(pathname, *bits[:-1], bits[-1] + '.py') + self[name] = _RequiredModule(runpy.run_path(filename, run_name=name)) + return self[name] + + +registry = _RequiredModulesRegistry() +require = registry.require diff --git a/setup.py b/setup.py index 4cd8d82..844240a 100644 --- a/setup.py +++ b/setup.py @@ -36,44 +36,41 @@ else: setup( name='bonobo', - description= - ('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' - 'python 3.5+.'), + description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' + 'python 3.5+.'), license='Apache License, Version 2.0', install_requires=[ - 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', - 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' + 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' ], version=version, long_description=long_description, classifiers=classifiers, packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, - data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map' - ])], + data_files=[ + ( + 'share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map' + ] + ) + ], extras_require={ 'dev': [ - 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', - 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', + 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', 'sphinx_rtd_theme', 'yapf' ], 'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5'] }, entry_points={ 'bonobo.commands': [ - 'init = bonobo.commands.init:register', - 'run = bonobo.commands.run:register', + 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], - 'edgy.project.features': - ['bonobo = ' - 'bonobo.ext.edgy.project.feature:BonoboFeature'] + 'edgy.project.features': ['bonobo = ' + 'bonobo.ext.edgy.project.feature:BonoboFeature'] }, url='https://www.bonobo-project.org/', - download_url= - 'https://github.com/python-bonobo/bonobo/tarball/{version}'.format( - version=version), ) + download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), +) diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index 1d44026..fbb4f5b 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -5,7 +5,7 @@ from bonobo import Bag from bonobo.constants import INHERIT_INPUT from bonobo.structs import Token -args = ('foo', 'bar',) +args = ('foo', 'bar', ) kwargs = dict(acme='corp') @@ -34,29 +34,29 @@ def test_inherit(): bag3 = bag.extend('c', c=3) bag4 = Bag('d', d=4) - assert bag.args == ('a',) + assert bag.args == ('a', ) assert bag.kwargs == {'a': 1} assert bag.flags is () - assert bag2.args == ('a', 'b',) + assert bag2.args == ('a', 'b', ) assert bag2.kwargs == {'a': 1, 'b': 2} assert INHERIT_INPUT in bag2.flags - assert bag3.args == ('a', 'c',) + assert bag3.args == ('a', 'c', ) assert bag3.kwargs == {'a': 1, 'c': 3} assert bag3.flags is () - assert bag4.args == ('d',) + assert bag4.args == ('d', ) assert bag4.kwargs == {'d': 4} assert bag4.flags is () bag4.set_parent(bag) - assert bag4.args == ('a', 'd',) + assert bag4.args == ('a', 'd', ) assert bag4.kwargs == {'a': 1, 'd': 4} assert bag4.flags is () bag4.set_parent(bag3) - assert bag4.args == ('a', 'c', 'd',) + assert bag4.args == ('a', 'c', 'd', ) assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4} assert bag4.flags is () diff --git a/tests/structs/test_graphs.py b/tests/structs/test_graphs.py index c1c29c2..3afeaee 100644 --- a/tests/structs/test_graphs.py +++ b/tests/structs/test_graphs.py @@ -1,5 +1,7 @@ import pytest +from unittest.mock import sentinel + from bonobo.constants import BEGIN from bonobo.structs import Graph @@ -41,3 +43,30 @@ def test_graph_add_chain(): g.add_chain(identity, identity, identity) assert len(g.nodes) == 3 assert len(g.outputs_of(BEGIN)) == 1 + + +def test_graph_topological_sort(): + g = Graph() + + g.add_chain( + sentinel.a1, + sentinel.a2, + sentinel.a3, + _input=None, + _output=None, + ) + + assert g.topologically_sorted_indexes == (0, 1, 2) + assert g[0] == sentinel.a1 + assert g[1] == sentinel.a2 + assert g[2] == sentinel.a3 + + g.add_chain( + sentinel.b1, + sentinel.b2, + _output=sentinel.a2, + ) + + assert g.topologically_sorted_indexes == (0, 3, 4, 1, 2) + assert g[3] == sentinel.b1 + assert g[4] == sentinel.b2 diff --git a/tests/util/requireable/dummy.py b/tests/util/requireable/dummy.py new file mode 100644 index 0000000..1ce8ef1 --- /dev/null +++ b/tests/util/requireable/dummy.py @@ -0,0 +1 @@ +foo = 'bar' diff --git a/tests/util/test_python.py b/tests/util/test_python.py new file mode 100644 index 0000000..6b1b591 --- /dev/null +++ b/tests/util/test_python.py @@ -0,0 +1,6 @@ +from bonobo.util.python import require + + +def test_require(): + dummy = require('requireable.dummy') + assert dummy.foo == 'bar' From e22f1bfb59f7ea7b719178e5930efaabe8709fc9 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 19 May 2017 18:35:39 +0200 Subject: [PATCH 007/143] Settings for debug/profile and use them in console plugin. --- bonobo/ext/console/plugin.py | 19 ++++++++++--------- bonobo/settings.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 bonobo/settings.py diff --git a/bonobo/ext/console/plugin.py b/bonobo/ext/console/plugin.py index 8884a73..8fd8cdf 100644 --- a/bonobo/ext/console/plugin.py +++ b/bonobo/ext/console/plugin.py @@ -19,6 +19,7 @@ import sys from colorama import Fore, Style +from bonobo import settings from bonobo.plugins import Plugin from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP @@ -27,7 +28,7 @@ from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.get_memory_info()[0] / float(2**20) + return process.memory_info()[0] / float(2**20) # @lru_cache(64) @@ -50,15 +51,14 @@ class ConsoleOutputPlugin(Plugin): self.prefix = '' def _write(self, graph_context, rewind): - profile, debug = False, False - if profile: + if settings.PROFILE: append = ( ('Memory', '{0:.2f} Mb'.format(memory_usage())), # ('Total time', '{0} s'.format(execution_time(harness))), ) else: append = () - self.write(graph_context, prefix=self.prefix, append=append, debug=debug, profile=profile, rewind=rewind) + self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind) def run(self): if sys.stdout.isatty(): @@ -70,23 +70,24 @@ class ConsoleOutputPlugin(Plugin): self._write(self.context.parent, rewind=False) @staticmethod - def write(context, prefix='', rewind=True, append=None, debug=False, profile=False): + def write(context, prefix='', rewind=True, append=None): t_cnt = len(context) for i in context.graph.topologically_sorted_indexes: node = context[i] + name_suffix = '({})'.format(i) if settings.DEBUG else '' if node.alive: _line = ''.join( ( - ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', node.name, '(', str(i), ') ', - node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', + ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', node.name, name_suffix, ' ', + node.get_statistics_as_string(), Style.RESET_ALL, ' ', ) ) else: _line = ''.join( ( - ' ', Fore.BLACK, '-', ' ', node.name, '(', str(i), ') ', - node.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ', + ' ', Fore.BLACK, '-', ' ', node.name, name_suffix, ' ', node.get_statistics_as_string(), + Style.RESET_ALL, ' ', ) ) print(prefix + _line + '\033[0K') diff --git a/bonobo/settings.py b/bonobo/settings.py new file mode 100644 index 0000000..aec203d --- /dev/null +++ b/bonobo/settings.py @@ -0,0 +1,16 @@ +import os + + +def to_bool(s): + if len(s): + if s.lower() in ('f', 'false', 'n', 'no', '0'): + return False + return True + return False + + +# Debug mode. +DEBUG = to_bool(os.environ.get('BONOBO_DEBUG', 'f')) + +# Profile mode. +PROFILE = to_bool(os.environ.get('BONOBO_PROFILE', 'f')) From d5cfa0281da68bad8cce3b8a9dfb6df0ad538dc2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Fri, 19 May 2017 18:38:41 +0200 Subject: [PATCH 008/143] Topological sort is non deterministic, only check for logic in test and not exact result so it will pass tests under py3.5 --- tests/structs/test_graphs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/structs/test_graphs.py b/tests/structs/test_graphs.py index 3afeaee..af1a6df 100644 --- a/tests/structs/test_graphs.py +++ b/tests/structs/test_graphs.py @@ -67,6 +67,7 @@ def test_graph_topological_sort(): _output=sentinel.a2, ) - assert g.topologically_sorted_indexes == (0, 3, 4, 1, 2) + assert g.topologically_sorted_indexes[-2:] == (1, 2) + assert g.topologically_sorted_indexes.index(3) < g.topologically_sorted_indexes.index(4) assert g[3] == sentinel.b1 assert g[4] == sentinel.b2 From cf0b982475f9fcd503f91d477d322d089c6b6a57 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:15:51 +0200 Subject: [PATCH 009/143] Adds method based options, limited to one, to allow nodes based on a specific method (think filter, join, etc)... Reimplementation of "Filter", with (rather simple) code from rdc.etl. --- bonobo/config/__init__.py | 3 +- bonobo/config/configurables.py | 18 +++++++- bonobo/config/options.py | 27 ++++++++++-- bonobo/errors.py | 4 ++ bonobo/examples/utils/filter.py | 21 ++++++++++ bonobo/execution/graph.py | 6 +-- bonobo/filter/__init__.py | 28 +++++++++++++ pytest.ini | 0 tests/test_config_method.py | 73 +++++++++++++++++++++++++++++++++ tests/test_execution.py | 13 ++++++ 10 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 bonobo/examples/utils/filter.py create mode 100644 bonobo/filter/__init__.py create mode 100644 pytest.ini create mode 100644 tests/test_config_method.py diff --git a/bonobo/config/__init__.py b/bonobo/config/__init__.py index c4ba410..9fc9971 100644 --- a/bonobo/config/__init__.py +++ b/bonobo/config/__init__.py @@ -1,5 +1,5 @@ from bonobo.config.configurables import Configurable -from bonobo.config.options import Option +from bonobo.config.options import Option, Method from bonobo.config.processors import ContextProcessor from bonobo.config.services import Container, Service @@ -8,5 +8,6 @@ __all__ = [ 'Container', 'ContextProcessor', 'Option', + 'Method', 'Service', ] diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 64b4adc..75f1e3c 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -1,5 +1,6 @@ +from bonobo.config.options import Method, Option from bonobo.config.processors import ContextProcessor -from bonobo.config.options import Option +from bonobo.errors import ConfigurationError __all__ = [ 'Configurable', @@ -17,6 +18,7 @@ class ConfigurableMeta(type): cls.__options__ = {} cls.__positional_options__ = [] cls.__processors__ = [] + cls.__wrappable__ = None for typ in cls.__mro__: for name, value in typ.__dict__.items(): @@ -24,6 +26,10 @@ class ConfigurableMeta(type): if isinstance(value, ContextProcessor): cls.__processors__.append(value) else: + if isinstance(value, Method): + if cls.__wrappable__: + raise ConfigurationError('Cannot define more than one "Method" option in a configurable. That may change in the future.') + cls.__wrappable__ = name if not value.name: value.name = name if not name in cls.__options__: @@ -43,6 +49,13 @@ class Configurable(metaclass=ConfigurableMeta): """ + def __new__(cls, *args, **kwargs): + if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'): + wrapped, args = args[0], args[1:] + return type(wrapped.__name__, (cls, ), {cls.__wrappable__: wrapped}) + + return super().__new__(cls) + def __init__(self, *args, **kwargs): super().__init__() @@ -90,3 +103,6 @@ class Configurable(metaclass=ConfigurableMeta): """ You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override. """ return self.call(*args, **kwargs) + + def call(self, *args, **kwargs): + raise NotImplementedError('Not implemented.') \ No newline at end of file diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 2ea67f9..a07ce2d 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -16,13 +16,34 @@ class Option: self._creation_counter = Option._creation_counter Option._creation_counter += 1 - def get_default(self): - return self.default() if callable(self.default) else self.default - def __get__(self, inst, typ): if not self.name in inst.__options_values__: inst.__options_values__[self.name] = self.get_default() return inst.__options_values__[self.name] + def __set__(self, inst, value): + inst.__options_values__[self.name] = self.clean(value) + + def get_default(self): + return self.default() if callable(self.default) else self.default + + def clean(self, value): + return self.type(value) if self.type else value + + +class Method(Option): + def __init__(self): + super().__init__(None, required=False, positional=True) + + def __get__(self, inst, typ): + if not self.name in inst.__options_values__: + inst.__options_values__[self.name] = getattr(inst, self.name) + return inst.__options_values__[self.name] + def __set__(self, inst, value): inst.__options_values__[self.name] = self.type(value) if self.type else value + + def clean(self, value): + if not hasattr(value, '__call__'): + raise ValueError('{} value must be callable.'.format(type(self).__name__)) + return value diff --git a/bonobo/errors.py b/bonobo/errors.py index 7718de8..4a2e9c5 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -52,3 +52,7 @@ class ValidationError(RuntimeError): class ProhibitedOperationError(RuntimeError): pass + + +class ConfigurationError(Exception): + pass \ No newline at end of file diff --git a/bonobo/examples/utils/filter.py b/bonobo/examples/utils/filter.py new file mode 100644 index 0000000..35b385c --- /dev/null +++ b/bonobo/examples/utils/filter.py @@ -0,0 +1,21 @@ +import bonobo + +from bonobo.filter import Filter + + +class OddOnlyFilter(Filter): + def filter(self, i): + return i % 2 + + +@Filter +def MultiplesOfThreeOnlyFilter(self, i): + return not (i % 3) + + +graph = bonobo.Graph( + lambda: tuple(range(50)), + OddOnlyFilter(), + MultiplesOfThreeOnlyFilter(), + print, +) diff --git a/bonobo/execution/graph.py b/bonobo/execution/graph.py index 1f2671a..00d2c43 100644 --- a/bonobo/execution/graph.py +++ b/bonobo/execution/graph.py @@ -26,11 +26,7 @@ class GraphExecutionContext: self.services = Container(services) if services else Container() for i, node_context in enumerate(self): - try: - node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)] - except KeyError: - continue - + node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)] node_context.input.on_begin = partial(node_context.send, BEGIN, _control=True) node_context.input.on_end = partial(node_context.send, END, _control=True) node_context.input.on_finalize = partial(node_context.stop) diff --git a/bonobo/filter/__init__.py b/bonobo/filter/__init__.py new file mode 100644 index 0000000..d44100c --- /dev/null +++ b/bonobo/filter/__init__.py @@ -0,0 +1,28 @@ +from bonobo.constants import NOT_MODIFIED + +from bonobo.config import Configurable, Method + + +class Filter(Configurable): + """Filter out hashes from the stream depending on the :attr:`filter` callable return value, when called with the + current hash as parameter. + + Can be used as a decorator on a filter callable. + + .. attribute:: filter + + A callable used to filter lines. + + If the callable returns a true-ish value, the input will be passed unmodified to the next items. + + Otherwise, it'll be burnt. + + """ + + filter = Method() + + def call(self, *args, **kwargs): + if self.filter(*args, **kwargs): + return NOT_MODIFIED + + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config_method.py b/tests/test_config_method.py new file mode 100644 index 0000000..1b2ecde --- /dev/null +++ b/tests/test_config_method.py @@ -0,0 +1,73 @@ +import pytest + +from bonobo.config import Configurable, Method, Option +from bonobo.errors import ConfigurationError + + +class MethodBasedConfigurable(Configurable): + handler = Method() + foo = Option(positional=True) + bar = Option() + + def call(self, *args, **kwargs): + self.handler(*args, **kwargs) + + +def test_one_wrapper_only(): + with pytest.raises(ConfigurationError): + class TwoMethods(Configurable): + h1 = Method() + h2 = Method() + + +def test_define_with_decorator(): + calls = [] + + @MethodBasedConfigurable + def Concrete(self, *args, **kwargs): + calls.append((args, kwargs,)) + + t = Concrete('foo', bar='baz') + assert len(calls) == 0 + t() + assert len(calls) == 1 + + +def test_define_with_argument(): + calls = [] + + def concrete_handler(*args, **kwargs): + calls.append((args, kwargs,)) + + t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler) + assert len(calls) == 0 + t() + assert len(calls) == 1 + +def test_define_with_inheritance(): + calls = [] + + class Inheriting(MethodBasedConfigurable): + def handler(self, *args, **kwargs): + calls.append((args, kwargs,)) + + t = Inheriting('foo', bar='baz') + assert len(calls) == 0 + t() + assert len(calls) == 1 + + +def test_inheritance_then_decorate(): + calls = [] + + class Inheriting(MethodBasedConfigurable): + pass + + @Inheriting + def Concrete(self, *args, **kwargs): + calls.append((args, kwargs,)) + + t = Concrete('foo', bar='baz') + assert len(calls) == 0 + t() + assert len(calls) == 1 diff --git a/tests/test_execution.py b/tests/test_execution.py index 5194903..97f6735 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -59,11 +59,24 @@ def test_simple_execution_context(): assert ctx[i].wrapped is node assert not ctx.alive + assert not ctx.started + assert not ctx.stopped ctx.recv(BEGIN, Bag(), END) assert not ctx.alive + assert not ctx.started + assert not ctx.stopped ctx.start() assert ctx.alive + assert ctx.started + assert not ctx.stopped + + ctx.stop() + + assert not ctx.alive + assert ctx.started + assert ctx.stopped + From 3f2cfb620db3927e9c81375d4d043cc4bde3b00e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:18:22 +0200 Subject: [PATCH 010/143] Remove deprecated helpers, which are not tested anyway. use bonobo.run() instead of jupyter_run and console_run. --- bonobo/util/helpers.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 bonobo/util/helpers.py diff --git a/bonobo/util/helpers.py b/bonobo/util/helpers.py deleted file mode 100644 index 227fdbd..0000000 --- a/bonobo/util/helpers.py +++ /dev/null @@ -1,17 +0,0 @@ -from bonobo.util.compat import deprecated - - -@deprecated -def console_run(*chain, output=True, plugins=None, strategy=None): - from bonobo import run - from bonobo.ext.console import ConsoleOutputPlugin - - return run(*chain, plugins=(plugins or []) + [ConsoleOutputPlugin()] if output else [], strategy=strategy) - - -@deprecated -def jupyter_run(*chain, plugins=None, strategy=None): - from bonobo import run - from bonobo.ext.jupyter import JupyterOutputPlugin - - return run(*chain, plugins=(plugins or []) + [JupyterOutputPlugin()], strategy=strategy) From e9e9477ef6674c79f5e71fc30f7cd32a030ce822 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:21:04 +0200 Subject: [PATCH 011/143] Move all builtin transformations to the bonobo.nodes package, as the number grows. --- bonobo/_api.py | 20 +++++++++++-------- bonobo/config/configurables.py | 3 ++- bonobo/core/inputs.py | 5 +++-- bonobo/nodes/__init__.py | 9 +++++++++ bonobo/{ => nodes}/basics.py | 0 .../{filter/__init__.py => nodes/filter.py} | 2 -- bonobo/{ => nodes}/io/__init__.py | 0 bonobo/{ => nodes}/io/csv.py | 0 bonobo/{ => nodes}/io/file.py | 0 bonobo/{ => nodes}/io/json.py | 0 10 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 bonobo/nodes/__init__.py rename bonobo/{ => nodes}/basics.py (100%) rename bonobo/{filter/__init__.py => nodes/filter.py} (99%) rename bonobo/{ => nodes}/io/__init__.py (100%) rename bonobo/{ => nodes}/io/csv.py (100%) rename bonobo/{ => nodes}/io/file.py (100%) rename bonobo/{ => nodes}/io/json.py (100%) diff --git a/bonobo/_api.py b/bonobo/_api.py index 97678f5..a4ea7ee 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,10 +1,10 @@ import warnings -from bonobo.basics import Limit, PrettyPrint, Tee, count, identity, noop, pprint +from bonobo.structs import Bag, Graph, Token +from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ + PrettyPrint, Tee, count, identity, noop, pprint from bonobo.strategies import create_strategy -from bonobo.structs import Bag, Graph from bonobo.util.objects import get_name -from bonobo.io import CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter __all__ = [] @@ -63,7 +63,7 @@ def run(graph, *chain, strategy=None, plugins=None, services=None): # bonobo.structs -register_api_group(Bag, Graph) +register_api_group(Bag, Graph, Token) # bonobo.strategies register_api(create_strategy) @@ -88,8 +88,15 @@ def open_fs(fs_url, *args, **kwargs): return _open_fs(str(fs_url), *args, **kwargs) -# bonobo.basics +# bonobo.nodes register_api_group( + CsvReader, + CsvWriter, + FileReader, + FileWriter, + Filter, + JsonReader, + JsonWriter, Limit, PrettyPrint, Tee, @@ -99,9 +106,6 @@ register_api_group( pprint, ) -# bonobo.io -register_api_group(CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter) - def _is_interactive_console(): import sys diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 75f1e3c..77e8e4e 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -54,6 +54,7 @@ class Configurable(metaclass=ConfigurableMeta): wrapped, args = args[0], args[1:] return type(wrapped.__name__, (cls, ), {cls.__wrappable__: wrapped}) + # XXX is that correct ??? how does it pass args/kwargs to __init__ ??? return super().__new__(cls) def __init__(self, *args, **kwargs): @@ -105,4 +106,4 @@ class Configurable(metaclass=ConfigurableMeta): return self.call(*args, **kwargs) def call(self, *args, **kwargs): - raise NotImplementedError('Not implemented.') \ No newline at end of file + raise NotImplementedError('Not implemented.') diff --git a/bonobo/core/inputs.py b/bonobo/core/inputs.py index f41ff01..cf9a6ec 100644 --- a/bonobo/core/inputs.py +++ b/bonobo/core/inputs.py @@ -15,11 +15,12 @@ # limitations under the License. from abc import ABCMeta, abstractmethod + from queue import Queue -from bonobo.errors import AbstractError, InactiveWritableError, InactiveReadableError from bonobo.constants import BEGIN, END -from bonobo.basics import noop +from bonobo.errors import AbstractError, InactiveReadableError, InactiveWritableError +from bonobo.nodes import noop BUFFER_SIZE = 8192 diff --git a/bonobo/nodes/__init__.py b/bonobo/nodes/__init__.py new file mode 100644 index 0000000..c25b580 --- /dev/null +++ b/bonobo/nodes/__init__.py @@ -0,0 +1,9 @@ +from bonobo.nodes.io import __all__ as _all_io +from bonobo.nodes.io import * + +from bonobo.nodes.basics import __all__ as _all_basics +from bonobo.nodes.basics import * + +from bonobo.nodes.filter import Filter + +__all__ = _all_basics + _all_io + ['Filter'] diff --git a/bonobo/basics.py b/bonobo/nodes/basics.py similarity index 100% rename from bonobo/basics.py rename to bonobo/nodes/basics.py diff --git a/bonobo/filter/__init__.py b/bonobo/nodes/filter.py similarity index 99% rename from bonobo/filter/__init__.py rename to bonobo/nodes/filter.py index d44100c..2ec0130 100644 --- a/bonobo/filter/__init__.py +++ b/bonobo/nodes/filter.py @@ -24,5 +24,3 @@ class Filter(Configurable): def call(self, *args, **kwargs): if self.filter(*args, **kwargs): return NOT_MODIFIED - - diff --git a/bonobo/io/__init__.py b/bonobo/nodes/io/__init__.py similarity index 100% rename from bonobo/io/__init__.py rename to bonobo/nodes/io/__init__.py diff --git a/bonobo/io/csv.py b/bonobo/nodes/io/csv.py similarity index 100% rename from bonobo/io/csv.py rename to bonobo/nodes/io/csv.py diff --git a/bonobo/io/file.py b/bonobo/nodes/io/file.py similarity index 100% rename from bonobo/io/file.py rename to bonobo/nodes/io/file.py diff --git a/bonobo/io/json.py b/bonobo/nodes/io/json.py similarity index 100% rename from bonobo/io/json.py rename to bonobo/nodes/io/json.py From 58a15806685987a4912c44ee5f06f31f2605f0bf Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:40:44 +0200 Subject: [PATCH 012/143] Fix iterable problems due to context currifier, and some examples with wrong import paths. --- bonobo/config/configurables.py | 4 +- bonobo/config/processors.py | 6 + bonobo/examples/datasets/coffeeshops.json | 286 +++++++++---------- bonobo/examples/datasets/coffeeshops.txt | 284 +++++++++--------- bonobo/examples/{utils => nodes}/__init__.py | 0 bonobo/examples/{utils => nodes}/count.py | 0 bonobo/examples/{utils => nodes}/filter.py | 2 +- bonobo/execution/node.py | 4 +- docs/reference/examples.rst | 2 +- tests/test_config_method.py | 10 +- tests/test_execution.py | 1 - 11 files changed, 304 insertions(+), 295 deletions(-) rename bonobo/examples/{utils => nodes}/__init__.py (100%) rename bonobo/examples/{utils => nodes}/count.py (100%) rename bonobo/examples/{utils => nodes}/filter.py (89%) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 77e8e4e..895c4ee 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -28,7 +28,9 @@ class ConfigurableMeta(type): else: if isinstance(value, Method): if cls.__wrappable__: - raise ConfigurationError('Cannot define more than one "Method" option in a configurable. That may change in the future.') + raise ConfigurationError( + 'Cannot define more than one "Method" option in a configurable. That may change in the future.' + ) cls.__wrappable__ = name if not value.name: value.name = name diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index f7bd601..76614fb 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -1,6 +1,7 @@ import functools import types +from collections import Iterable from bonobo.util.compat import deprecated_alias, deprecated @@ -62,7 +63,12 @@ class ContextCurrifier: self.context += ensure_tuple(_append_to_context) self._stack.append(_processed) + def __iter__(self): + yield from self.wrapped + def __call__(self, *args, **kwargs): + if not callable(self.wrapped) and isinstance(self.wrapped, Iterable): + return self.__iter__() return self.wrapped(*self.context, *args, **kwargs) def teardown(self): diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json index ad754d4..ab07fba 100644 --- a/bonobo/examples/datasets/coffeeshops.json +++ b/bonobo/examples/datasets/coffeeshops.json @@ -1,182 +1,182 @@ -{"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France", -"Le Sully": "6 Bd henri IV, 75004 Paris, France", -"O q de poule": "53 rue du ruisseau, 75018 Paris, France", +{"O q de poule": "53 rue du ruisseau, 75018 Paris, France", +"Le chantereine": "51 Rue Victoire, 75009 Paris, France", +"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France", "Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France", "La Renaissance": "112 Rue Championnet, 75018 Paris, France", -"Le chantereine": "51 Rue Victoire, 75009 Paris, France", -"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", +"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France", +"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", +"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", +"Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France", +"D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France", +"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", +"Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France", +"Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France", +"Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France", +"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", +"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", +"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", +"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", +"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", +"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", +"Le Bellerive": "71 quai de Seine, 75019 Paris, France", +"La Bauloise": "36 rue du hameau, 75015 Paris, France", +"Le Dellac": "14 rue Rougemont, 75009 Paris, France", +"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", +"Le Sully": "6 Bd henri IV, 75004 Paris, France", +"Le Felteu": "1 rue Pecquay, 75004 Paris, France", "Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", "Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France", -"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", -"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", -"Le Brio": "216, rue Marcadet, 75018 Paris, France", "Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France", "Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France", -"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", -"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", -"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", -"Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France", -"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", -"Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France", -"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", -"Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France", -"D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France", -"La Bauloise": "36 rue du hameau, 75015 Paris, France", -"Le Bellerive": "71 quai de Seine, 75019 Paris, France", -"Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France", -"Le Dellac": "14 rue Rougemont, 75009 Paris, France", -"Le Felteu": "1 rue Pecquay, 75004 Paris, France", -"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", -"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", -"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", -"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", -"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", -"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", -"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France", -"l'Usine": "1 rue d'Avron, 75020 Paris, France", -"La Bricole": "52 rue Liebniz, 75018 Paris, France", -"le ronsard": "place maubert, 75005 Paris, France", -"Face Bar": "82 rue des archives, 75003 Paris, France", -"American Kitchen": "49 rue bichat, 75010 Paris, France", -"La Marine": "55 bis quai de valmy, 75010 Paris, France", -"Le Bloc": "21 avenue Brochant, 75017 Paris, France", -"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", -"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", -"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", -"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", -"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", -"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", -"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", +"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", +"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", "La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France", +"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", +"Au bon coin": "49 rue des Cloys, 75018 Paris, France", +"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", +"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", +"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", "Le Supercoin": "3, rue Baudelique, 75018 Paris, France", "Populettes": "86 bis rue Riquet, 75018 Paris, France", -"Au bon coin": "49 rue des Cloys, 75018 Paris, France", "Le Couvent": "69 rue Broca, 75013 Paris, France", -"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", -"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", +"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", "Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France", -"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", -"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", -"La Perle": "78 rue vieille du temple, 75003 Paris, France", -"Le Descartes": "1 rue Thouin, 75005 Paris, France", -"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", "Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France", -"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", "Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France", -"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", -"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", -"Le Village": "182 rue de Courcelles, 75017 Paris, France", -"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", -"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", -"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", -"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", -"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France", +"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", +"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", +"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", +"La Perle": "78 rue vieille du temple, 75003 Paris, France", +"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", +"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", +"Le Descartes": "1 rue Thouin, 75005 Paris, France", "Br\u00fblerie San Jos\u00e9": "30 rue des Petits-Champs, 75002 Paris, France", "Caf\u00e9 de la Mairie (du VIII)": "rue de Lisbonne, 75008 Paris, France", +"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", +"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", +"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France", +"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", +"Le Village": "182 rue de Courcelles, 75017 Paris, France", +"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", +"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", +"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", +"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", +"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", +"La Marine": "55 bis quai de valmy, 75010 Paris, France", +"American Kitchen": "49 rue bichat, 75010 Paris, France", +"Face Bar": "82 rue des archives, 75003 Paris, France", +"Le Bloc": "21 avenue Brochant, 75017 Paris, France", +"La Bricole": "52 rue Liebniz, 75018 Paris, France", +"le ronsard": "place maubert, 75005 Paris, France", +"l'Usine": "1 rue d'Avron, 75020 Paris, France", +"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", +"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", +"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", +"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France", +"Le Germinal": "95 avenue Emile Zola, 75015 Paris, France", "Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France", "Etienne": "14 rue Turbigo, Paris, 75001 Paris, France", "L'ing\u00e9nu": "184 bd Voltaire, 75011 Paris, France", -"L'Olive": "8 rue L'Olive, 75018 Paris, France", -"Le Biz": "18 rue Favart, 75002 Paris, France", -"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", -"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", -"Le Germinal": "95 avenue Emile Zola, 75015 Paris, France", -"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France", "Le refuge": "72 rue lamarck, 75018 Paris, France", +"Le Biz": "18 rue Favart, 75002 Paris, France", +"L'Olive": "8 rue L'Olive, 75018 Paris, France", "Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France", -"L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France", -"Le bal du pirate": "60 rue des bergers, 75015 Paris, France", -"zic zinc": "95 rue claude decaen, 75012 Paris, France", -"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", -"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", -"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France", -"Le Dunois": "77 rue Dunois, 75013 Paris, France", -"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France", -"Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France", -"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France", -"Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France", -"Le Centenaire": "104 rue amelot, 75011 Paris, France", -"La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France", -"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France", -"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", -"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", -"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", -"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", -"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", -"Caves populaires": "22 rue des Dames, 75017 Paris, France", -"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", -"Tamm Bara": "7 rue Clisson, 75013 Paris, France", -"L'anjou": "1 rue de Montholon, 75009 Paris, France", +"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", +"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France", +"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", +"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", +"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", "Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France", -"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", -"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", -"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", -"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", +"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France", +"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France", +"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", +"Le Centenaire": "104 rue amelot, 75011 Paris, France", "Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France", "La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 Paris, France", -"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", +"Le bal du pirate": "60 rue des bergers, 75015 Paris, France", +"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", "Le Killy Jen": "28 bis boulevard Diderot, 75012 Paris, France", +"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", "Les Artisans": "106 rue Lecourbe, 75015 Paris, France", "Peperoni": "83 avenue de Wagram, 75001 Paris, France", -"le lutece": "380 rue de vaugirard, 75015 Paris, France", -"Brasiloja": "16 rue Ganneron, 75018 Paris, France", -"Rivolux": "16 rue de Rivoli, 75004 Paris, France", -"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", -"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", -"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", -"O'Paris": "1 Rue des Envierges, 75020 Paris, France", -"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", +"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France", +"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", +"zic zinc": "95 rue claude decaen, 75012 Paris, France", +"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France", +"Le Brio": "216, rue Marcadet, 75018 Paris, France", +"Le Dunois": "77 rue Dunois, 75013 Paris, France", +"La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France", +"Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France", +"Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France", +"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", +"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", +"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", +"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", +"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", +"Caves populaires": "22 rue des Dames, 75017 Paris, France", +"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", +"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", +"L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France", +"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", +"L'anjou": "1 rue de Montholon, 75009 Paris, France", +"Tamm Bara": "7 rue Clisson, 75013 Paris, France", "La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", "Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", +"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", +"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", +"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", "La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France", "Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France", -"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", -"La Brocante": "10 rue Rossini, 75009 Paris, France", -"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", "Les caves populaires": "22 rue des Dames, 75017 Paris, France", -"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", -"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", -"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", -"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", -"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France", -"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", -"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", -"Botak cafe": "1 rue Paul albert, 75018 Paris, France", -"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", +"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", "Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France", +"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", +"La Brocante": "10 rue Rossini, 75009 Paris, France", +"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", "Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France", -"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", "Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France", -"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", +"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", +"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", "maison du vin": "52 rue des plantes, 75014 Paris, France", -"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France", -"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France", -"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France", -"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", -"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", -"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", -"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France", -"Panem": "18 rue de Crussol, 75011 Paris, France", -"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", -"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France", -"L'Angle": "28 rue de Ponthieu, 75008 Paris, France", +"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", +"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", +"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", +"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", +"le lutece": "380 rue de vaugirard, 75015 Paris, France", +"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", +"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", +"O'Paris": "1 Rue des Envierges, 75020 Paris, France", +"Rivolux": "16 rue de Rivoli, 75004 Paris, France", +"Brasiloja": "16 rue Ganneron, 75018 Paris, France", +"Botak cafe": "1 rue Paul albert, 75018 Paris, France", +"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France", "Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France", +"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", "Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France", -"L'Entracte": "place de l'opera, 75002 Paris, France", -"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", -"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France", -"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France", -"Le Brigadier": "12 rue Blanche, 75009 Paris, France", -"L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France", -"Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France", -"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", -"L'horizon": "93, rue de la Roquette, 75011 Paris, France", -"Waikiki": "10 rue d\"Ulm, 75005 Paris, France", -"Au pays de Vannes": "34 bis rue de Wattignies, 75012 Paris, France", -"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", -"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France", +"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", +"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", +"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France", +"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France", +"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France", "Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France", -"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France", -"le Zango": "58 rue Daguerre, 75014 Paris, France", +"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", +"Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France", +"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", +"Le Brigadier": "12 rue Blanche, 75009 Paris, France", "Melting Pot": "3 rue de Lagny, 75020 Paris, France", -"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France"} \ No newline at end of file +"L'Entracte": "place de l'opera, 75002 Paris, France", +"le Zango": "58 rue Daguerre, 75014 Paris, France", +"Panem": "18 rue de Crussol, 75011 Paris, France", +"Waikiki": "10 rue d\"Ulm, 75005 Paris, France", +"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France", +"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France", +"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France", +"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", +"Au pays de Vannes": "34 bis rue de Wattignies, 75012 Paris, France", +"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France", +"L'Angle": "28 rue de Ponthieu, 75008 Paris, France", +"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France", +"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France", +"L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France", +"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", +"L'horizon": "93, rue de la Roquette, 75011 Paris, France"} \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt index 0833c88..eb3b668 100644 --- a/bonobo/examples/datasets/coffeeshops.txt +++ b/bonobo/examples/datasets/coffeeshops.txt @@ -1,182 +1,182 @@ -Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France -Le Sully, 6 Bd henri IV, 75004 Paris, France O q de poule, 53 rue du ruisseau, 75018 Paris, France +Le chantereine, 51 Rue Victoire, 75009 Paris, France +La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France La Renaissance, 112 Rue Championnet, 75018 Paris, France -Le chantereine, 51 Rue Victoire, 75009 Paris, France -Le Müller, 11 rue Feutrier, 75018 Paris, France +Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France +Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France +les montparnos, 65 boulevard Pasteur, 75015 Paris, France +Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France +Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France +Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France +Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France +Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France +Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France +Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France +Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France +Le Saint Jean, 23 rue des abbesses, 75018 Paris, France +Le Square, 31 rue Saint-Dominique, 75007 Paris, France +Les Arcades, 61 rue de Ponthieu, 75008 Paris, France +Café Lea, 5 rue Claude Bernard, 75005 Paris, France +Le Bellerive, 71 quai de Seine, 75019 Paris, France +La Bauloise, 36 rue du hameau, 75015 Paris, France +Le Dellac, 14 rue Rougemont, 75009 Paris, France +Le Bosquet, 46 avenue Bosquet, 75007 Paris, France +Le Sully, 6 Bd henri IV, 75004 Paris, France +Le Felteu, 1 rue Pecquay, 75004 Paris, France Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France Le café des amis, 125 rue Blomet, 75015 Paris, France -Le Café Livres, 10 rue Saint Martin, 75004 Paris, France -Le Bosquet, 46 avenue Bosquet, 75007 Paris, France -Le Brio, 216, rue Marcadet, 75018 Paris, France Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France -Les Arcades, 61 rue de Ponthieu, 75008 Paris, France -Le Square, 31 rue Saint-Dominique, 75007 Paris, France -Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France -Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France -Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France -Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France -Café Lea, 5 rue Claude Bernard, 75005 Paris, France -Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France -Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France -La Bauloise, 36 rue du hameau, 75015 Paris, France -Le Bellerive, 71 quai de Seine, 75019 Paris, France -Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France -Le Dellac, 14 rue Rougemont, 75009 Paris, France -Le Felteu, 1 rue Pecquay, 75004 Paris, France -Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France -Le Saint Jean, 23 rue des abbesses, 75018 Paris, France -les montparnos, 65 boulevard Pasteur, 75015 Paris, France -Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France -Le pari's café, 104 rue caulaincourt, 75018 Paris, France -Le Poulailler, 60 rue saint-sabin, 75011 Paris, France -L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France -l'Usine, 1 rue d'Avron, 75020 Paris, France -La Bricole, 52 rue Liebniz, 75018 Paris, France -le ronsard, place maubert, 75005 Paris, France -Face Bar, 82 rue des archives, 75003 Paris, France -American Kitchen, 49 rue bichat, 75010 Paris, France -La Marine, 55 bis quai de valmy, 75010 Paris, France -Le Bloc, 21 avenue Brochant, 75017 Paris, France -La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France -Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France -La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France -Café Zen, 46 rue Victoire, 75009 Paris, France -O'Breizh, 27 rue de Penthièvre, 75008 Paris, France -Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France -Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France +Le Müller, 11 rue Feutrier, 75018 Paris, France +Le Café Livres, 10 rue Saint Martin, 75004 Paris, France La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France +Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France +Au bon coin, 49 rue des Cloys, 75018 Paris, France +La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France +Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France +O'Breizh, 27 rue de Penthièvre, 75008 Paris, France Le Supercoin, 3, rue Baudelique, 75018 Paris, France Populettes, 86 bis rue Riquet, 75018 Paris, France -Au bon coin, 49 rue des Cloys, 75018 Paris, France Le Couvent, 69 rue Broca, 75013 Paris, France -La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France -L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France +Café Zen, 46 rue Victoire, 75009 Paris, France Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France -Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France -Le Café frappé, 95 rue Montmartre, 75002 Paris, France -La Perle, 78 rue vieille du temple, 75003 Paris, France -Le Descartes, 1 rue Thouin, 75005 Paris, France -Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France -Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France -Le Malar, 88 rue Saint-Dominique, 75007 Paris, France -Au panini de la place, 47 rue Belgrand, 75020 Paris, France -Le Village, 182 rue de Courcelles, 75017 Paris, France -Pause Café, 41 rue de Charonne, 75011 Paris, France -Le Pure café, 14 rue Jean Macé, 75011 Paris, France -Extra old café, 307 fg saint Antoine, 75011 Paris, France -Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France -En attendant l'or, 3 rue Faidherbe, 75011 Paris, France +Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France +Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France +Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France +La Perle, 78 rue vieille du temple, 75003 Paris, France +Le Café frappé, 95 rue Montmartre, 75002 Paris, France +L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France +Le Descartes, 1 rue Thouin, 75005 Paris, France Brûlerie San José, 30 rue des Petits-Champs, 75002 Paris, France Café de la Mairie (du VIII), rue de Lisbonne, 75008 Paris, France +Au panini de la place, 47 rue Belgrand, 75020 Paris, France +Extra old café, 307 fg saint Antoine, 75011 Paris, France +En attendant l'or, 3 rue Faidherbe, 75011 Paris, France +Le Pure café, 14 rue Jean Macé, 75011 Paris, France +Le Village, 182 rue de Courcelles, 75017 Paris, France +Le Malar, 88 rue Saint-Dominique, 75007 Paris, France +Pause Café, 41 rue de Charonne, 75011 Paris, France +Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France +La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France +Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France +La Marine, 55 bis quai de valmy, 75010 Paris, France +American Kitchen, 49 rue bichat, 75010 Paris, France +Face Bar, 82 rue des archives, 75003 Paris, France +Le Bloc, 21 avenue Brochant, 75017 Paris, France +La Bricole, 52 rue Liebniz, 75018 Paris, France +le ronsard, place maubert, 75005 Paris, France +l'Usine, 1 rue d'Avron, 75020 Paris, France +La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France +Le General Beuret, 9 Place du General Beuret, 75015 Paris, France +Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France +Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France +Le Germinal, 95 avenue Emile Zola, 75015 Paris, France Café Martin, 2 place Martin Nadaud, 75001 Paris, France Etienne, 14 rue Turbigo, Paris, 75001 Paris, France L'ingénu, 184 bd Voltaire, 75011 Paris, France -L'Olive, 8 rue L'Olive, 75018 Paris, France -Le Biz, 18 rue Favart, 75002 Paris, France -Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France -Le General Beuret, 9 Place du General Beuret, 75015 Paris, France -Le Germinal, 95 avenue Emile Zola, 75015 Paris, France -Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France Le refuge, 72 rue lamarck, 75018 Paris, France +Le Biz, 18 rue Favart, 75002 Paris, France +L'Olive, 8 rue L'Olive, 75018 Paris, France Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France -L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France -Le bal du pirate, 60 rue des bergers, 75015 Paris, France -zic zinc, 95 rue claude decaen, 75012 Paris, France -l'orillon bar, 35 rue de l'orillon, 75011 Paris, France -Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France -L'Inévitable, 22 rue Linné, 75005 Paris, France -Le Dunois, 77 rue Dunois, 75013 Paris, France -Ragueneau, 202 rue Saint Honoré, 75001 Paris, France -Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France -Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France -Le petit Bretonneau, Le petit Bretonneau - à l'intérieur de l'Hôpital, 75018 Paris, France -Le Centenaire, 104 rue amelot, 75011 Paris, France -La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France -Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France -Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France -Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France -La chaumière gourmande, Route de la Muette à Neuilly -Club hippique du Jardin d’Acclimatation, 75016 Paris, France -Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France -Caves populaires, 22 rue des Dames, 75017 Paris, France -Caprice café, 12 avenue Jean Moulin, 75014 Paris, France -Tamm Bara, 7 rue Clisson, 75013 Paris, France -L'anjou, 1 rue de Montholon, 75009 Paris, France +Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France +L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France +Le pari's café, 104 rue caulaincourt, 75018 Paris, France +Le Poulailler, 60 rue saint-sabin, 75011 Paris, France +Chai 33, 33 Cour Saint Emilion, 75012 Paris, France Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France -Chez Prune, 36 rue Beaurepaire, 75010 Paris, France -Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France -bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France -Café beauveau, 9 rue de Miromesnil, 75008 Paris, France +Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France +Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France +Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France +Le Centenaire, 104 rue amelot, 75011 Paris, France Café Pistache, 9 rue des petits champs, 75001 Paris, France La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 Paris, France -le 1 cinq, 172 rue de vaugirard, 75015 Paris, France +Le bal du pirate, 60 rue des bergers, 75015 Paris, France +bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France Le Killy Jen, 28 bis boulevard Diderot, 75012 Paris, France +le 1 cinq, 172 rue de vaugirard, 75015 Paris, France Les Artisans, 106 rue Lecourbe, 75015 Paris, France Peperoni, 83 avenue de Wagram, 75001 Paris, France -le lutece, 380 rue de vaugirard, 75015 Paris, France -Brasiloja, 16 rue Ganneron, 75018 Paris, France -Rivolux, 16 rue de Rivoli, 75004 Paris, France -Chai 33, 33 Cour Saint Emilion, 75012 Paris, France -L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France -NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France -O'Paris, 1 Rue des Envierges, 75020 Paris, France -Café Clochette, 16 avenue Richerand, 75010 Paris, France +Ragueneau, 202 rue Saint Honoré, 75001 Paris, France +l'orillon bar, 35 rue de l'orillon, 75011 Paris, France +zic zinc, 95 rue claude decaen, 75012 Paris, France +L'Inévitable, 22 rue Linné, 75005 Paris, France +Le Brio, 216, rue Marcadet, 75018 Paris, France +Le Dunois, 77 rue Dunois, 75013 Paris, France +La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France +Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France +Le petit Bretonneau, Le petit Bretonneau - à l'intérieur de l'Hôpital, 75018 Paris, France +La chaumière gourmande, Route de la Muette à Neuilly +Club hippique du Jardin d’Acclimatation, 75016 Paris, France +Caprice café, 12 avenue Jean Moulin, 75014 Paris, France +Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France +Café beauveau, 9 rue de Miromesnil, 75008 Paris, France +Caves populaires, 22 rue des Dames, 75017 Paris, France +Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France +Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France +L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France +Chez Prune, 36 rue Beaurepaire, 75010 Paris, France +L'anjou, 1 rue de Montholon, 75009 Paris, France +Tamm Bara, 7 rue Clisson, 75013 Paris, France La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France +Café Clochette, 16 avenue Richerand, 75010 Paris, France +L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France +Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France -Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France -La Brocante, 10 rue Rossini, 75009 Paris, France -Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France Les caves populaires, 22 rue des Dames, 75017 Paris, France -Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France -Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France -Trois pièces cuisine, 101 rue des dames, 75017 Paris, France -Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France -La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France -Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France -L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France -Botak cafe, 1 rue Paul albert, 75018 Paris, France -le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France +Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France +Trois pièces cuisine, 101 rue des dames, 75017 Paris, France +La Brocante, 10 rue Rossini, 75009 Paris, France +Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France -Le Fronton, 63 rue de Ponthieu, 75008 Paris, France Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France -Le Tournebride, 104 rue Mouffetard, 75005 Paris, France +L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France +le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France maison du vin, 52 rue des plantes, 75014 Paris, France -Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France -L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France -Le café Monde et Médias, Place de la République, 75003 Paris, France -Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France -Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France -Chez Miamophile, 6 rue Mélingue, 75019 Paris, France -La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France -Panem, 18 rue de Crussol, 75011 Paris, France -Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France -Café Dupont, 198 rue de la Convention, 75015 Paris, France -L'Angle, 28 rue de Ponthieu, 75008 Paris, France +Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France +NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France +Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France +Le Tournebride, 104 rue Mouffetard, 75005 Paris, France +le lutece, 380 rue de vaugirard, 75015 Paris, France +Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France +Le Fronton, 63 rue de Ponthieu, 75008 Paris, France +O'Paris, 1 Rue des Envierges, 75020 Paris, France +Rivolux, 16 rue de Rivoli, 75004 Paris, France +Brasiloja, 16 rue Ganneron, 75018 Paris, France +Botak cafe, 1 rue Paul albert, 75018 Paris, France +La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France +Chez Miamophile, 6 rue Mélingue, 75019 Paris, France Canopy Café associatif, 19 rue Pajol, 75018 Paris, France -L'Entracte, place de l'opera, 75002 Paris, France -Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France -Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France -Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France -Le Brigadier, 12 rue Blanche, 75009 Paris, France -L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France -Café Victor, 10 boulevard Victor, 75015 Paris, France -L'empreinte, 54, avenue Daumesnil, 75012 Paris, France -L'horizon, 93, rue de la Roquette, 75011 Paris, France -Waikiki, 10 rue d"Ulm, 75005 Paris, France -Au pays de Vannes, 34 bis rue de Wattignies, 75012 Paris, France -Café Varenne, 36 rue de Varenne, 75007 Paris, France -l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France +Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France +Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France +Le café Monde et Médias, Place de la République, 75003 Paris, France +L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France +Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France -Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France -le Zango, 58 rue Daguerre, 75014 Paris, France +L'empreinte, 54, avenue Daumesnil, 75012 Paris, France +Café Victor, 10 boulevard Victor, 75015 Paris, France +Café Varenne, 36 rue de Varenne, 75007 Paris, France +Le Brigadier, 12 rue Blanche, 75009 Paris, France Melting Pot, 3 rue de Lagny, 75020 Paris, France -Pari's Café, 174 avenue de Clichy, 75017 Paris, France \ No newline at end of file +L'Entracte, place de l'opera, 75002 Paris, France +le Zango, 58 rue Daguerre, 75014 Paris, France +Panem, 18 rue de Crussol, 75011 Paris, France +Waikiki, 10 rue d"Ulm, 75005 Paris, France +l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France +Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France +Pari's Café, 174 avenue de Clichy, 75017 Paris, France +Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France +Au pays de Vannes, 34 bis rue de Wattignies, 75012 Paris, France +Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France +L'Angle, 28 rue de Ponthieu, 75008 Paris, France +Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France +Café Dupont, 198 rue de la Convention, 75015 Paris, France +L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France +Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France +L'horizon, 93, rue de la Roquette, 75011 Paris, France \ No newline at end of file diff --git a/bonobo/examples/utils/__init__.py b/bonobo/examples/nodes/__init__.py similarity index 100% rename from bonobo/examples/utils/__init__.py rename to bonobo/examples/nodes/__init__.py diff --git a/bonobo/examples/utils/count.py b/bonobo/examples/nodes/count.py similarity index 100% rename from bonobo/examples/utils/count.py rename to bonobo/examples/nodes/count.py diff --git a/bonobo/examples/utils/filter.py b/bonobo/examples/nodes/filter.py similarity index 89% rename from bonobo/examples/utils/filter.py rename to bonobo/examples/nodes/filter.py index 35b385c..bf390e9 100644 --- a/bonobo/examples/utils/filter.py +++ b/bonobo/examples/nodes/filter.py @@ -1,6 +1,6 @@ import bonobo -from bonobo.filter import Filter +from bonobo import Filter class OddOnlyFilter(Filter): diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 02a866e..ade3472 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -42,7 +42,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): name, type_name = get_name(self), get_name(type(self)) return '<{}({}{}){}>'.format(type_name, self.alive_str, name, self.get_statistics_as_string(prefix=' ')) - def write(self, *messages): # XXX write() ? ( node.write(...) ) + def write(self, *messages): """ Push a message list to this context's input queue. @@ -54,7 +54,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): # XXX deprecated alias recv = deprecated_alias('recv', write) - def send(self, value, _control=False): # XXX self.send(....) + def send(self, value, _control=False): """ Sends a message to all of this context's outputs. diff --git a/docs/reference/examples.rst b/docs/reference/examples.rst index e2f922d..e685f79 100644 --- a/docs/reference/examples.rst +++ b/docs/reference/examples.rst @@ -70,7 +70,7 @@ Utils Count ----- -.. automodule:: bonobo.examples.utils.count +.. automodule:: bonobo.examples.nodes.count :members: :undoc-members: :show-inheritance: diff --git a/tests/test_config_method.py b/tests/test_config_method.py index 1b2ecde..2798b31 100644 --- a/tests/test_config_method.py +++ b/tests/test_config_method.py @@ -15,6 +15,7 @@ class MethodBasedConfigurable(Configurable): def test_one_wrapper_only(): with pytest.raises(ConfigurationError): + class TwoMethods(Configurable): h1 = Method() h2 = Method() @@ -25,7 +26,7 @@ def test_define_with_decorator(): @MethodBasedConfigurable def Concrete(self, *args, **kwargs): - calls.append((args, kwargs,)) + calls.append((args, kwargs, )) t = Concrete('foo', bar='baz') assert len(calls) == 0 @@ -37,19 +38,20 @@ def test_define_with_argument(): calls = [] def concrete_handler(*args, **kwargs): - calls.append((args, kwargs,)) + calls.append((args, kwargs, )) t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler) assert len(calls) == 0 t() assert len(calls) == 1 + def test_define_with_inheritance(): calls = [] class Inheriting(MethodBasedConfigurable): def handler(self, *args, **kwargs): - calls.append((args, kwargs,)) + calls.append((args, kwargs, )) t = Inheriting('foo', bar='baz') assert len(calls) == 0 @@ -65,7 +67,7 @@ def test_inheritance_then_decorate(): @Inheriting def Concrete(self, *args, **kwargs): - calls.append((args, kwargs,)) + calls.append((args, kwargs, )) t = Concrete('foo', bar='baz') assert len(calls) == 0 diff --git a/tests/test_execution.py b/tests/test_execution.py index 97f6735..e6099fd 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -79,4 +79,3 @@ def test_simple_execution_context(): assert not ctx.alive assert ctx.started assert ctx.stopped - From 577a781de3466d796a8fa05b59aee9eee4a73777 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:47:57 +0200 Subject: [PATCH 013/143] Deprecation-land cleanup, before 0.3. --- bonobo/_api.py | 11 +++-------- bonobo/config/processors.py | 35 ----------------------------------- bonobo/ext/opendatasoft.py | 7 +------ bonobo/nodes/io/csv.py | 2 +- bonobo/nodes/io/file.py | 2 +- bonobo/nodes/io/json.py | 2 +- 6 files changed, 7 insertions(+), 52 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index a4ea7ee..85e5a62 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -20,7 +20,7 @@ def register_api_group(*args): @register_api -def run(graph, *chain, strategy=None, plugins=None, services=None): +def run(graph, strategy=None, plugins=None, services=None): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. @@ -40,21 +40,16 @@ def run(graph, *chain, strategy=None, plugins=None, services=None): :param dict services: The implementations of services this graph will use. :return bonobo.execution.graph.GraphExecutionContext: """ - if len(chain): - warnings.warn('DEPRECATED. You should pass a Graph instance instead of a chain.') - from bonobo import Graph - graph = Graph(graph, *chain) - strategy = create_strategy(strategy) plugins = plugins or [] - if _is_interactive_console(): + if _is_interactive_console(): # pragma: no cover from bonobo.ext.console import ConsoleOutputPlugin if ConsoleOutputPlugin not in plugins: plugins.append(ConsoleOutputPlugin) - if _is_jupyter_notebook(): + if _is_jupyter_notebook(): # pragma: no cover from bonobo.ext.jupyter import JupyterOutputPlugin if JupyterOutputPlugin not in plugins: plugins.append(JupyterOutputPlugin) diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 76614fb..61bd5dd 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -85,41 +85,6 @@ class ContextCurrifier: raise RuntimeError('Context processors should not yield more than once.') -@deprecated -def add_context_processor(cls_or_func, context_processor): - getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR).append(context_processor) - - -@deprecated -def contextual(cls_or_func): - """ - Make sure an element has the context processors collection. - - :param cls_or_func: - """ - if not add_context_processor.__name__ in cls_or_func.__dict__: - setattr(cls_or_func, add_context_processor.__name__, functools.partial(add_context_processor, cls_or_func)) - - if isinstance(cls_or_func, types.FunctionType): - try: - getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR) - except AttributeError: - setattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR, []) - return cls_or_func - - if not _CONTEXT_PROCESSORS_ATTR in cls_or_func.__dict__: - setattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR, []) - - _processors = getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR) - for processor in cls_or_func.__dict__.values(): - if isinstance(processor, ContextProcessor): - _processors.append(processor) - - # This is needed for python 3.5, python 3.6 should be fine, but it's considered an implementation detail. - _processors.sort(key=lambda proc: proc._creation_counter) - return cls_or_func - - def resolve_processors(mixed): try: yield from mixed.__processors__ diff --git a/bonobo/ext/opendatasoft.py b/bonobo/ext/opendatasoft.py index 25c1748..4d094e7 100644 --- a/bonobo/ext/opendatasoft.py +++ b/bonobo/ext/opendatasoft.py @@ -3,7 +3,7 @@ from urllib.parse import urlencode import requests # todo: make this a service so we can substitute it ? from bonobo.config import Option -from bonobo.config.processors import ContextProcessor, contextual +from bonobo.config.processors import ContextProcessor from bonobo.config.configurables import Configurable from bonobo.util.compat import deprecated from bonobo.util.objects import ValueHolder @@ -50,11 +50,6 @@ class OpenDataSoftAPI(Configurable): start.value += self.rows -@deprecated -def from_opendatasoft_api(dataset, **kwargs): - return OpenDataSoftAPI(dataset=dataset, **kwargs) - - __all__ = [ 'OpenDataSoftAPI', ] diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 647925b..6242947 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -1,7 +1,7 @@ import csv from bonobo.config import Option -from bonobo.config.processors import ContextProcessor, contextual +from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED from bonobo.util.objects import ValueHolder from .file import FileHandler, FileReader, FileWriter diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index 3298fd9..231e570 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -1,6 +1,6 @@ from bonobo.config import Option, Service from bonobo.config.configurables import Configurable -from bonobo.config.processors import ContextProcessor, contextual +from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED from bonobo.util.objects import ValueHolder diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 2c4ea79..fdb49b8 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -1,6 +1,6 @@ import json -from bonobo.config.processors import ContextProcessor, contextual +from bonobo.config.processors import ContextProcessor from .file import FileWriter, FileReader __all__ = [ From 09fefa69df4d64da1d726afcede91d1c63224ddc Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 10:54:53 +0200 Subject: [PATCH 014/143] Update README.rst --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5e7deba..65ee30f 100644 --- a/README.rst +++ b/README.rst @@ -7,11 +7,12 @@ Data-processing for humans. .. image:: https://img.shields.io/pypi/v/bonobo.svg :target: https://pypi.python.org/pypi/bonobo :alt: PyPI + .. image:: https://img.shields.io/pypi/pyversions/bonobo.svg :target: https://pypi.python.org/pypi/bonobo :alt: Versions -.. image:: https://readthedocs.org/projects/bonobo/badge/?version=0.3 +.. image:: https://readthedocs.org/projects/bonobo/badge/?version=latest :target: http://docs.bonobo-project.org/ :alt: Documentation From a018cca20eb43b624e53cad106eb4dae38cd0c72 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 13:05:07 +0200 Subject: [PATCH 015/143] Documenting transformations and configurables. --- bonobo/config/options.py | 77 ++++++++++++++++- bonobo/config/services.py | 4 + bonobo/ext/{console/plugin.py => console.py} | 37 ++------ bonobo/ext/console/__init__.py | 5 -- bonobo/ext/opendatasoft.py | 1 - bonobo/ext/pandas.py | 0 docs/_templates/index.html | 32 ++----- docs/guide/index.rst | 1 + docs/guide/services.rst | 23 +++-- docs/guide/transformations.rst | 89 ++++++++++++++++++++ docs/tutorial/index.rst | 21 +++-- 11 files changed, 210 insertions(+), 80 deletions(-) rename bonobo/ext/{console/plugin.py => console.py} (76%) delete mode 100644 bonobo/ext/console/__init__.py delete mode 100644 bonobo/ext/pandas.py create mode 100644 docs/guide/transformations.rst diff --git a/bonobo/config/options.py b/bonobo/config/options.py index a07ce2d..7fbdf54 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -1,8 +1,51 @@ class Option: """ - An Option is a descriptor for a required or optional parameter of a Configurable. - + An Option is a descriptor for Configurable's parameters. + + .. attribute:: type + + Option type allows to provide a callable used to cast, clean or validate the option value. If not provided, or + None, the option's value will be the exact value user provided. + + (default: None) + + .. attribute:: required + + If an option is required, an error will be raised if no value is provided (at runtime). If it is not, option + will have the default value if user does not override it at runtime. + + (default: False) + + .. attribute:: positional + + If this is true, it'll be possible to provide the option value as a positional argument. Otherwise, it must + be provided as a keyword argument. + + (default: False) + + .. attribute:: default + + Default value for non-required options. + + (default: None) + + Example: + + .. code-block:: python + + from bonobo.config import Configurable, Option + + class Example(Configurable): + title = Option(str, required=True, positional=True) + keyword = Option(str, default='foo') + + def call(self, s): + return self.title + ': ' + s + ' (' + self.keyword + ')' + + example = Example('hello', keyword='bar') + """ + _creation_counter = 0 def __init__(self, type=None, *, required=False, positional=False, default=None): @@ -32,6 +75,36 @@ class Option: class Method(Option): + """ + A Method is a special callable-valued option, that can be used in three different ways (but for same purpose). + + * Like a normal option, the value can be provided to the Configurable constructor. + + >>> from bonobo.config import Configurable, Method + + >>> class MethodExample(Configurable): + ... handler = Method() + + >>> example1 = MethodExample(handler=str.upper) + + * It can be used by a child class that overrides the Method with a normal method. + + >>> class ChildMethodExample(MethodExample): + ... def handler(self, s: str): + ... return s.upper() + + >>> example2 = ChildMethodExample() + + * Finally, it also enables the class to be used as a decorator, to generate a subclass providing the Method a value. + + >>> @MethodExample + ... def OtherChildMethodExample(s): + ... return s.upper() + + >>> example3 = OtherChildMethodExample() + + """ + def __init__(self): super().__init__(None, required=False, positional=True) diff --git a/bonobo/config/services.py b/bonobo/config/services.py index 86bc06b..8d6c95e 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -39,6 +39,10 @@ class Service(Option): The main goal is not to tie transformations to actual dependencies, so the same can be run in different contexts (stages like preprod, prod, or tenants like client1, client2, or anything you want). + + .. attribute:: name + + Service name will be used to retrieve the implementation at runtime. """ diff --git a/bonobo/ext/console/plugin.py b/bonobo/ext/console.py similarity index 76% rename from bonobo/ext/console/plugin.py rename to bonobo/ext/console.py index 8fd8cdf..e15453b 100644 --- a/bonobo/ext/console/plugin.py +++ b/bonobo/ext/console.py @@ -1,41 +1,13 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2012-2017 Romain Dorgueil -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import functools import sys -from colorama import Fore, Style +from colorama import Style, Fore from bonobo import settings from bonobo.plugins import Plugin from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP -@functools.lru_cache(1) -def memory_usage(): - import os, psutil - process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2**20) - - -# @lru_cache(64) -# def execution_time(harness): -# return datetime.datetime.now() - harness._started_at - - class ConsoleOutputPlugin(Plugin): """ Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor @@ -107,3 +79,10 @@ class ConsoleOutputPlugin(Plugin): if rewind: print(CLEAR_EOL) print(MOVE_CURSOR_UP(t_cnt + 2)) + + +@functools.lru_cache(1) +def memory_usage(): + import os, psutil + process = psutil.Process(os.getpid()) + return process.memory_info()[0] / float(2**20) \ No newline at end of file diff --git a/bonobo/ext/console/__init__.py b/bonobo/ext/console/__init__.py deleted file mode 100644 index 6770e35..0000000 --- a/bonobo/ext/console/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .plugin import ConsoleOutputPlugin - -__all__ = [ - 'ConsoleOutputPlugin', -] diff --git a/bonobo/ext/opendatasoft.py b/bonobo/ext/opendatasoft.py index 4d094e7..ad6dcea 100644 --- a/bonobo/ext/opendatasoft.py +++ b/bonobo/ext/opendatasoft.py @@ -5,7 +5,6 @@ import requests # todo: make this a service so we can substitute it ? from bonobo.config import Option from bonobo.config.processors import ContextProcessor from bonobo.config.configurables import Configurable -from bonobo.util.compat import deprecated from bonobo.util.objects import ValueHolder diff --git a/bonobo/ext/pandas.py b/bonobo/ext/pandas.py deleted file mode 100644 index e69de29..0000000 diff --git a/docs/_templates/index.html b/docs/_templates/index.html index 521b343..a2ee73b 100644 --- a/docs/_templates/index.html +++ b/docs/_templates/index.html @@ -3,10 +3,7 @@ {% block body %}
- Bonobo is currently ALPHA software. That means that the doc is not finished, and that - some APIs will change.
- There are a lot of missing sections, including comparison with other tools. But if you're looking for a - replacement for X, unless X is an ETL, bonobo is probably not what you want. + Bonobo is ALPHA software. Some APIs will change.

@@ -16,26 +13,12 @@

{% trans %} - Bonobo is a line-by-line data-processing toolkit for python 3.5+ emphasizing simple and - atomic data transformations defined using a directed graph of plain old python callables (functions and - generators). + Bonobo is a line-by-line data-processing toolkit for python 3.5+ (extract-transform-load + framework) emphasizing simple and atomic data transformations defined using a directed graph of plain old + python objects (functions, iterables, generators, ...). {% endtrans %}

-

- {% trans %} - Bonobo is a extract-transform-load framework that uses python code to define transformations. - {% endtrans %} -

- -

- {% trans %} - Bonobo is your own data-monkey army. Tedious and repetitive data-processing incoming? Give - it a try! - {% endtrans %} -

- -

{% trans %}Documentation{% endtrans %}

@@ -95,8 +78,9 @@
  • {% trans %} - Dependency injection: Abstract the transformation dependencies to easily switch data sources and - used libraries, allowing to easily test your transformations. + Service injection: Abstract the transformation dependencies to easily switch data sources and + dependant libraries. You'll be able to specify the concrete implementations or configurations at + runtime, for example to switch a database connection string or an API endpoint. {% endtrans %}
  • @@ -107,7 +91,7 @@
  • {% trans %} - Work in progress: read the roadmap. + Bonobo is young, and the todo-list is huge. Read the roadmap. {% endtrans %}
  • diff --git a/docs/guide/index.rst b/docs/guide/index.rst index dab1b44..18e5565 100644 --- a/docs/guide/index.rst +++ b/docs/guide/index.rst @@ -10,6 +10,7 @@ There are a few things that you should know while writing transformations graphs :maxdepth: 2 purity + transformations services Third party integrations diff --git a/docs/guide/services.rst b/docs/guide/services.rst index 66d2671..0b12d96 100644 --- a/docs/guide/services.rst +++ b/docs/guide/services.rst @@ -1,20 +1,18 @@ -Services and dependencies (draft implementation) -================================================ +Services and dependencies +========================= -:Status: Draft implementation -:Stability: Alpha -:Last-Modified: 28 apr 2017 +:Last-Modified: 20 may 2017 -Most probably, you'll want to use external systems within your transformations. Those systems may include databases, -apis (using http, for example), filesystems, etc. +You'll probably want to use external systems within your transformations. Those systems may include databases, apis +(using http, for example), filesystems, etc. You can start by hardcoding those services. That does the job, at first. If you're going a little further than that, you'll feel limited, for a few reasons: * Hardcoded and tightly linked dependencies make your transformations hard to test, and hard to reuse. -* Processing data on your laptop is great, but being able to do it on different systems (or stages), in different - environments, is more realistic? You probably want to contigure a different database on a staging environment, +* Processing data on your laptop is great, but being able to do it on different target systems (or stages), in different + environments, is more realistic. You'll want to contigure a different database on a staging environment, preprod environment or production system. Maybe you have silimar systems for different clients and want to select the system at runtime. Etc. @@ -52,10 +50,11 @@ injected to your calls under the parameter name "database". Function-based transformations ------------------------------ -No implementation yet, but expect something similar to CBT API, maybe using a `@Service(...)` decorator. +No implementation yet, but expect something similar to CBT API, maybe using a `@Service(...)` decorator. See +`issue #70 `_. -Execution ---------- +Provide implementation at run time +---------------------------------- Let's see how to execute it: diff --git a/docs/guide/transformations.rst b/docs/guide/transformations.rst new file mode 100644 index 0000000..da1ea55 --- /dev/null +++ b/docs/guide/transformations.rst @@ -0,0 +1,89 @@ +Transformations +=============== + +Here is some guidelines on how to write transformations, to avoid the convention-jungle that could happen without +a few rules. + + +Naming conventions +:::::::::::::::::: + +The naming convention used is the following. + +If you're naming something which is an actual transformation, that can be used directly as a graph node, then use +underscores and lowercase names: + +.. code-block:: python + + # instance of a class based transformation + filter = Filter(...) + + # function based transformation + def uppercase(s: str) -> str: + return s.upper() + +If you're naming something which is configurable, that will need to be instanciated or called to obtain something that +can be used as a graph node, then use camelcase names: + +.. code-block:: python + + # configurable + class ChangeCase(Configurable): + modifier = Option(default='upper') + def call(self, s: str) -> str: + return getattr(s, self.modifier)() + + # transformation factory + def Apply(method): + @functools.wraps(method) + def apply(s: str) -> str: + return method(s) + return apply + + # result is a graph node candidate + upper = Apply(str.upper) + + +Function based transformations +:::::::::::::::::::::::::::::: + +The most basic transformations are function-based. Which means that you define a function, and it will be used directly +in a graph. + +.. code-block:: python + + def get_representation(row): + return repr(row) + + graph = bonobo.Graph( + [...], + get_representation, + ) + + +It does not allow any configuration, but if it's an option, prefer it as it's simpler to write. + + +Class based transformations +::::::::::::::::::::::::::: + +A lot of logic is a bit more complex, and you'll want to use classes to define some of your transformations. + +The :class:`bonobo.config.Configurable` class gives you a few toys to write configurable transformations. + +Options +------- + +.. autoclass:: bonobo.config.Option + +Services +-------- + +.. autoclass:: bonobo.config.Service + +Method +------ + +.. autoclass:: bonobo.config.Method + + diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index e57e154..4627357 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -1,23 +1,29 @@ First steps =========== -We tried hard to make **Bonobo** simple. We use simple python, and we believe it should be simple to learn. +Bonobo uses simple python and should be quick and easy to learn. + +What is Bonobo? +::::::::::::::: + +Bonobo is an ETL (Extract-Transform-Load) framework for python 3.5. The goal is to define data-transformations, with +python code in charge of handling similar shaped independant lines of data. + +Bonobo *is not* a statistical or data-science tool. If you're looking for a data-analysis tool in python, use Pandas. + +Bonobo is a lean manufacturing assembly line for data that let you focus on the actual work instead of the plumbery. + Tutorial :::::::: -We strongly advice that even if you're an advanced python developper, you go through the whole tutorial for two -reasons: that should be sufficient to do anything possible with **Bonobo** and that's a good moment to learn the few -concepts you'll see everywhere in the software. - -If you're not familiar with python, you should first read :doc:`python`. - .. toctree:: :maxdepth: 2 tut01 tut02 + What's next? :::::::::::: @@ -39,3 +45,4 @@ Read about integrating external tools with bonobo * :doc:`../guide/ext/jupyter`: run transformations within jupyter notebooks. * :doc:`../guide/ext/selenium`: run * :doc:`../guide/ext/sqlalchemy`: everything you need to interract with SQL databases. + From 4342035f1266b28453aaccf4ed6ff3d3589eba8b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 13:11:37 +0200 Subject: [PATCH 016/143] Refactoring: dispatch "core" package modules into structs and utils, as it does not have a lot of sense anymore. --- bonobo/core/__init__.py | 1 - bonobo/execution/node.py | 4 ++-- bonobo/{core => structs}/inputs.py | 0 bonobo/{core => util}/statistics.py | 0 tests/{core => structs}/test_inputs.py | 2 +- tests/{core => util}/test_statistics.py | 2 +- 6 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 bonobo/core/__init__.py rename bonobo/{core => structs}/inputs.py (100%) rename bonobo/{core => util}/statistics.py (100%) rename tests/{core => structs}/test_inputs.py (97%) rename tests/{core => util}/test_statistics.py (83%) diff --git a/bonobo/core/__init__.py b/bonobo/core/__init__.py deleted file mode 100644 index 086efcc..0000000 --- a/bonobo/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" Core required libraries. """ diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index ade3472..635068e 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -3,15 +3,15 @@ from queue import Empty from time import sleep from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED -from bonobo.core.inputs import Input -from bonobo.core.statistics import WithStatistics from bonobo.errors import InactiveReadableError from bonobo.execution.base import LoopingExecutionContext from bonobo.structs.bags import Bag +from bonobo.structs.inputs import Input from bonobo.util.compat import deprecated_alias from bonobo.util.errors import is_error from bonobo.util.iterators import iter_if_not_sequence from bonobo.util.objects import get_name +from bonobo.util.statistics import WithStatistics class NodeExecutionContext(WithStatistics, LoopingExecutionContext): diff --git a/bonobo/core/inputs.py b/bonobo/structs/inputs.py similarity index 100% rename from bonobo/core/inputs.py rename to bonobo/structs/inputs.py diff --git a/bonobo/core/statistics.py b/bonobo/util/statistics.py similarity index 100% rename from bonobo/core/statistics.py rename to bonobo/util/statistics.py diff --git a/tests/core/test_inputs.py b/tests/structs/test_inputs.py similarity index 97% rename from tests/core/test_inputs.py rename to tests/structs/test_inputs.py index 5739dc3..d2ce827 100644 --- a/tests/core/test_inputs.py +++ b/tests/structs/test_inputs.py @@ -19,8 +19,8 @@ from queue import Empty import pytest from bonobo.constants import BEGIN, END -from bonobo.core.inputs import Input from bonobo.errors import InactiveWritableError, InactiveReadableError +from bonobo.structs.inputs import Input def test_input_runlevels(): diff --git a/tests/core/test_statistics.py b/tests/util/test_statistics.py similarity index 83% rename from tests/core/test_statistics.py rename to tests/util/test_statistics.py index 9b05175..bb787eb 100644 --- a/tests/core/test_statistics.py +++ b/tests/util/test_statistics.py @@ -1,4 +1,4 @@ -from bonobo.core.statistics import WithStatistics +from bonobo.util.statistics import WithStatistics class MyThingWithStats(WithStatistics): From 2b3ef05fac1683fc15c43defa2e2a25a17710435 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 20 May 2017 14:47:30 +0200 Subject: [PATCH 017/143] Removes unused functions and test bag application to iterables. --- bonobo/config/configurables.py | 2 +- bonobo/structs/bags.py | 6 +----- bonobo/util/compat.py | 24 ------------------------ tests/structs/test_bags.py | 24 ++++++++++++++++-------- tests/util/test_compat.py | 24 ++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 38 deletions(-) create mode 100644 tests/util/test_compat.py diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 895c4ee..c0dccf9 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -108,4 +108,4 @@ class Configurable(metaclass=ConfigurableMeta): return self.call(*args, **kwargs) def call(self, *args, **kwargs): - raise NotImplementedError('Not implemented.') + raise AbstractError('Not implemented.') diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 3414d00..9800a0a 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -65,12 +65,8 @@ class Bag: if len(args) == 0 and len(kwargs) == 0: try: iter(func_or_iter) - def generator(): - nonlocal func_or_iter - for x in func_or_iter: - yield x - + yield from func_or_iter return generator() except TypeError as exc: raise TypeError('Could not apply bag to {}.'.format(func_or_iter)) from exc diff --git a/bonobo/util/compat.py b/bonobo/util/compat.py index 68c4c9c..4a62742 100644 --- a/bonobo/util/compat.py +++ b/bonobo/util/compat.py @@ -1,31 +1,7 @@ import functools -import struct - -import sys import warnings -def is_platform_little_endian(): - """ am I little endian """ - return sys.byteorder == 'little' - - -def is_platform_windows(): - return sys.platform == 'win32' or sys.platform == 'cygwin' - - -def is_platform_linux(): - return sys.platform == 'linux2' - - -def is_platform_mac(): - return sys.platform == 'darwin' - - -def is_platform_32bit(): - return struct.calcsize("P") * 8 < 64 - - def deprecated_alias(alias, func): @functools.wraps(func) def new_func(*args, **kwargs): diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index fbb4f5b..c8783e2 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -1,11 +1,11 @@ -from unittest.mock import Mock import pickle +from unittest.mock import Mock from bonobo import Bag from bonobo.constants import INHERIT_INPUT from bonobo.structs import Token -args = ('foo', 'bar', ) +args = ('foo', 'bar',) kwargs = dict(acme='corp') @@ -34,29 +34,29 @@ def test_inherit(): bag3 = bag.extend('c', c=3) bag4 = Bag('d', d=4) - assert bag.args == ('a', ) + assert bag.args == ('a',) assert bag.kwargs == {'a': 1} assert bag.flags is () - assert bag2.args == ('a', 'b', ) + assert bag2.args == ('a', 'b',) assert bag2.kwargs == {'a': 1, 'b': 2} assert INHERIT_INPUT in bag2.flags - assert bag3.args == ('a', 'c', ) + assert bag3.args == ('a', 'c',) assert bag3.kwargs == {'a': 1, 'c': 3} assert bag3.flags is () - assert bag4.args == ('d', ) + assert bag4.args == ('d',) assert bag4.kwargs == {'d': 4} assert bag4.flags is () bag4.set_parent(bag) - assert bag4.args == ('a', 'd', ) + assert bag4.args == ('a', 'd',) assert bag4.kwargs == {'a': 1, 'd': 4} assert bag4.flags is () bag4.set_parent(bag3) - assert bag4.args == ('a', 'c', 'd', ) + assert bag4.args == ('a', 'c', 'd',) assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4} assert bag4.flags is () @@ -87,3 +87,11 @@ def test_eq_operator(): def test_repr(): bag = Bag('a', a=1) assert repr(bag) == "" + + +def test_iterator(): + bag = Bag() + assert list(bag.apply([1, 2, 3])) == [1, 2, 3] + assert list(bag.apply((1, 2, 3))) == [1, 2, 3] + assert list(bag.apply(range(5))) == [0, 1, 2, 3, 4] + assert list(bag.apply('azerty')) == ['a', 'z', 'e', 'r', 't', 'y'] diff --git a/tests/util/test_compat.py b/tests/util/test_compat.py new file mode 100644 index 0000000..83bcfcc --- /dev/null +++ b/tests/util/test_compat.py @@ -0,0 +1,24 @@ +import pytest + +from bonobo.util.compat import deprecated, deprecated_alias + + +def test_deprecated(): + @deprecated + def foo(): + pass + + foo = deprecated(foo) + with pytest.warns(DeprecationWarning): + foo() + + +def test_deprecated_alias(): + def foo(): + pass + + foo = deprecated_alias('bar', foo) + + with pytest.warns(DeprecationWarning): + foo() + From 4d9b579a60e9041f32951dbc036a121fee74f006 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 21 May 2017 19:22:45 +0200 Subject: [PATCH 018/143] Tuning ValueHolder as I could not find better option to generate the double-underscore methods. --- bonobo/config/configurables.py | 6 +- bonobo/config/processors.py | 39 +++++++--- bonobo/ext/opendatasoft.py | 2 +- bonobo/nodes/basics.py | 31 +++++--- bonobo/nodes/io/csv.py | 17 +++-- bonobo/nodes/io/file.py | 4 +- bonobo/structs/bags.py | 2 + bonobo/util/objects.py | 135 +++++++++++++++++++-------------- tests/structs/test_bags.py | 14 ++-- tests/test_basics.py | 42 ++++++---- tests/util/test_compat.py | 1 - tests/util/test_objects.py | 1 + 12 files changed, 177 insertions(+), 117 deletions(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index c0dccf9..1d4fdb2 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -74,9 +74,11 @@ class Configurable(metaclass=ConfigurableMeta): # transform positional arguments in keyword arguments if possible. position = 0 for positional_option in self.__positional_options__: + if len(args) <= position: + break + kwargs[positional_option] = args[position] + position += 1 if positional_option in missing: - kwargs[positional_option] = args[position] - position += 1 missing.remove(positional_option) # complain if there are still missing options. diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 61bd5dd..7475e68 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -1,11 +1,9 @@ -import functools - import types from collections import Iterable - -from bonobo.util.compat import deprecated_alias, deprecated +from contextlib import contextmanager from bonobo.config.options import Option +from bonobo.util.compat import deprecated_alias from bonobo.util.iterators import ensure_tuple _CONTEXT_PROCESSORS_ATTR = '__processors__' @@ -52,6 +50,14 @@ class ContextCurrifier: self._stack = [] self._stack_values = [] + def __iter__(self): + yield from self.wrapped + + def __call__(self, *args, **kwargs): + if not callable(self.wrapped) and isinstance(self.wrapped, Iterable): + return self.__iter__() + return self.wrapped(*self.context, *args, **kwargs) + def setup(self, *context): if len(self._stack): raise RuntimeError('Cannot setup context currification twice.') @@ -63,14 +69,6 @@ class ContextCurrifier: self.context += ensure_tuple(_append_to_context) self._stack.append(_processed) - def __iter__(self): - yield from self.wrapped - - def __call__(self, *args, **kwargs): - if not callable(self.wrapped) and isinstance(self.wrapped, Iterable): - return self.__iter__() - return self.wrapped(*self.context, *args, **kwargs) - def teardown(self): while len(self._stack): processor = self._stack.pop() @@ -84,6 +82,23 @@ class ContextCurrifier: # No error ? We should have had StopIteration ... raise RuntimeError('Context processors should not yield more than once.') + @contextmanager + def as_contextmanager(self, *context): + """ + Convenience method to use it as a contextmanager, mostly for test purposes. + + Example: + + >>> with ContextCurrifier(node).as_contextmanager(context) as stack: + ... stack() + + :param context: + :return: + """ + self.setup(*context) + yield self + self.teardown() + def resolve_processors(mixed): try: diff --git a/bonobo/ext/opendatasoft.py b/bonobo/ext/opendatasoft.py index ad6dcea..4be3134 100644 --- a/bonobo/ext/opendatasoft.py +++ b/bonobo/ext/opendatasoft.py @@ -46,7 +46,7 @@ class OpenDataSoftAPI(Configurable): for row in records: yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})} - start.value += self.rows + start += self.rows __all__ = [ diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 03a1232..195cd8e 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -3,10 +3,12 @@ from pprint import pprint as _pprint from colorama import Fore, Style +from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor from bonobo.structs.bags import Bag from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL +from bonobo.constants import NOT_MODIFIED __all__ = [ 'identity', @@ -23,19 +25,26 @@ def identity(x): return x -def Limit(n=10): - from bonobo.constants import NOT_MODIFIED - i = 0 +class Limit(Configurable): + """ + Creates a Limit() node, that will only let go through the first n rows (defined by the `limit` option), unmodified. - def _limit(*args, **kwargs): - nonlocal i, n - i += 1 - if i <= n: + .. attribute:: limit + + Number of rows to let go through. + + """ + limit = Option(positional=True, default=10) + + @ContextProcessor + def counter(self, context): + yield ValueHolder(0) + + def call(self, counter, *args, **kwargs): + counter += 1 + if counter <= self.limit: yield NOT_MODIFIED - _limit.__name__ = 'Limit({})'.format(n) - return _limit - def Tee(f): from bonobo.constants import NOT_MODIFIED @@ -57,7 +66,7 @@ def count(counter, *args, **kwargs): def _count_counter(self, context): counter = ValueHolder(0) yield counter - context.send(Bag(counter.value)) + context.send(Bag(counter._value)) pprint = Tee(_pprint) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 6242947..45b40de 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -46,8 +46,11 @@ class CsvReader(CsvHandler, FileReader): def read(self, fs, file, headers): reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar) - headers.value = headers.value or next(reader) - field_count = len(headers.value) + + if not headers.get(): + headers.set(next(reader)) + + field_count = len(headers) if self.skip and self.skip > 0: for _ in range(0, self.skip): @@ -68,9 +71,9 @@ class CsvWriter(CsvHandler, FileWriter): yield writer, headers def write(self, fs, file, lineno, writer, headers, row): - if not lineno.value: - headers.value = headers.value or row.keys() - writer.writerow(headers.value) - writer.writerow(row[header] for header in headers.value) - lineno.value += 1 + if not lineno: + headers.set(headers.value or row.keys()) + writer.writerow(headers.get()) + writer.writerow(row[header] for header in headers.get()) + lineno += 1 return NOT_MODIFIED diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index 231e570..c88f601 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -86,7 +86,7 @@ class FileWriter(Writer): @ContextProcessor def lineno(self, context, fs, file): - lineno = ValueHolder(0, type=int) + lineno = ValueHolder(0) yield lineno def write(self, fs, file, lineno, row): @@ -94,7 +94,7 @@ class FileWriter(Writer): Write a row on the next line of opened file in context. """ self._write_line(file, (self.eol if lineno.value else '') + row) - lineno.value += 1 + lineno += 1 return NOT_MODIFIED def _write_line(self, file, line): diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 9800a0a..5fec1f2 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -65,8 +65,10 @@ class Bag: if len(args) == 0 and len(kwargs) == 0: try: iter(func_or_iter) + def generator(): yield from func_or_iter + return generator() except TypeError as exc: raise TypeError('Could not apply bag to {}.'.format(func_or_iter)) from exc diff --git a/bonobo/util/objects.py b/bonobo/util/objects.py index 1e0015a..34fc6e7 100644 --- a/bonobo/util/objects.py +++ b/bonobo/util/objects.py @@ -1,3 +1,7 @@ +import functools +from functools import partial + + def get_name(mixed): try: return mixed.__name__ @@ -27,181 +31,194 @@ class ValueHolder: """ - def __init__(self, value, *, type=None): - self.value = value - self.type = type + def __init__(self, value): + self._value = value - def __repr__(self): - return repr(self.value) + @property + def value(self): + # XXX deprecated + return self._value - def __lt__(self, other): - return self.value < other + def get(self): + return self._value - def __le__(self, other): - return self.value <= other + def set(self, new_value): + self._value = new_value + + def __bool__(self): + return bool(self._value) def __eq__(self, other): - return self.value == other + return self._value == other def __ne__(self, other): - return self.value != other + return self._value != other + + def __repr__(self): + return repr(self._value) + + def __lt__(self, other): + return self._value < other + + def __le__(self, other): + return self._value <= other def __gt__(self, other): - return self.value > other + return self._value > other def __ge__(self, other): - return self.value >= other + return self._value >= other def __add__(self, other): - return self.value + other + return self._value + other def __radd__(self, other): - return other + self.value + return other + self._value def __iadd__(self, other): - self.value += other + self._value += other return self def __sub__(self, other): - return self.value - other + return self._value - other def __rsub__(self, other): - return other - self.value + return other - self._value def __isub__(self, other): - self.value -= other + self._value -= other return self def __mul__(self, other): - return self.value * other + return self._value * other def __rmul__(self, other): - return other * self.value + return other * self._value def __imul__(self, other): - self.value *= other + self._value *= other return self def __matmul__(self, other): - return self.value @ other + return self._value @ other def __rmatmul__(self, other): - return other @ self.value + return other @ self._value def __imatmul__(self, other): - self.value @= other + self._value @= other return self def __truediv__(self, other): - return self.value / other + return self._value / other def __rtruediv__(self, other): - return other / self.value + return other / self._value def __itruediv__(self, other): - self.value /= other + self._value /= other return self def __floordiv__(self, other): - return self.value // other + return self._value // other def __rfloordiv__(self, other): - return other // self.value + return other // self._value def __ifloordiv__(self, other): - self.value //= other + self._value //= other return self def __mod__(self, other): - return self.value % other + return self._value % other def __rmod__(self, other): - return other % self.value + return other % self._value def __imod__(self, other): - self.value %= other + self._value %= other return self def __divmod__(self, other): - return divmod(self.value, other) + return divmod(self._value, other) def __rdivmod__(self, other): - return divmod(other, self.value) + return divmod(other, self._value) def __pow__(self, other): - return self.value**other + return self._value**other def __rpow__(self, other): - return other**self.value + return other**self._value def __ipow__(self, other): - self.value **= other + self._value **= other return self def __lshift__(self, other): - return self.value << other + return self._value << other def __rlshift__(self, other): - return other << self.value + return other << self._value def __ilshift__(self, other): - self.value <<= other + self._value <<= other return self def __rshift__(self, other): - return self.value >> other + return self._value >> other def __rrshift__(self, other): - return other >> self.value + return other >> self._value def __irshift__(self, other): - self.value >>= other + self._value >>= other return self def __and__(self, other): - return self.value & other + return self._value & other def __rand__(self, other): - return other & self.value + return other & self._value def __iand__(self, other): - self.value &= other + self._value &= other return self def __xor__(self, other): - return self.value ^ other + return self._value ^ other def __rxor__(self, other): - return other ^ self.value + return other ^ self._value def __ixor__(self, other): - self.value ^= other + self._value ^= other return self def __or__(self, other): - return self.value | other + return self._value | other def __ror__(self, other): - return other | self.value + return other | self._value def __ior__(self, other): - self.value |= other + self._value |= other return self def __neg__(self): - return -self.value + return -self._value def __pos__(self): - return +self.value + return +self._value def __abs__(self): - return abs(self.value) + return abs(self._value) def __invert__(self): - return ~self.value + return ~self._value def __len__(self): - return len(self.value) + return len(self._value) def get_attribute_or_create(obj, attr, default): diff --git a/tests/structs/test_bags.py b/tests/structs/test_bags.py index c8783e2..df9cc3c 100644 --- a/tests/structs/test_bags.py +++ b/tests/structs/test_bags.py @@ -5,7 +5,7 @@ from bonobo import Bag from bonobo.constants import INHERIT_INPUT from bonobo.structs import Token -args = ('foo', 'bar',) +args = ('foo', 'bar', ) kwargs = dict(acme='corp') @@ -34,29 +34,29 @@ def test_inherit(): bag3 = bag.extend('c', c=3) bag4 = Bag('d', d=4) - assert bag.args == ('a',) + assert bag.args == ('a', ) assert bag.kwargs == {'a': 1} assert bag.flags is () - assert bag2.args == ('a', 'b',) + assert bag2.args == ('a', 'b', ) assert bag2.kwargs == {'a': 1, 'b': 2} assert INHERIT_INPUT in bag2.flags - assert bag3.args == ('a', 'c',) + assert bag3.args == ('a', 'c', ) assert bag3.kwargs == {'a': 1, 'c': 3} assert bag3.flags is () - assert bag4.args == ('d',) + assert bag4.args == ('d', ) assert bag4.kwargs == {'d': 4} assert bag4.flags is () bag4.set_parent(bag) - assert bag4.args == ('a', 'd',) + assert bag4.args == ('a', 'd', ) assert bag4.kwargs == {'a': 1, 'd': 4} assert bag4.flags is () bag4.set_parent(bag3) - assert bag4.args == ('a', 'c', 'd',) + assert bag4.args == ('a', 'c', 'd', ) assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4} assert bag4.flags is () diff --git a/tests/test_basics.py b/tests/test_basics.py index 7a9f317..049dc90 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,7 +1,8 @@ from unittest.mock import MagicMock -import bonobo import pytest + +import bonobo from bonobo.config.processors import ContextCurrifier from bonobo.constants import NOT_MODIFIED @@ -10,14 +11,12 @@ def test_count(): with pytest.raises(TypeError): bonobo.count() + context = MagicMock() - currified = ContextCurrifier(bonobo.count) - currified.setup(context) - - for i in range(42): - currified() - currified.teardown() + with ContextCurrifier(bonobo.count).as_contextmanager(context) as stack: + for i in range(42): + stack() assert len(context.method_calls) == 1 bag = context.send.call_args[0][0] @@ -32,18 +31,31 @@ def test_identity(): def test_limit(): - limit = bonobo.Limit(2) - results = [] - for i in range(42): - results += list(limit()) + context, results = MagicMock(), [] + + with ContextCurrifier(bonobo.Limit(2)).as_contextmanager(context) as stack: + for i in range(42): + results += list(stack()) + assert results == [NOT_MODIFIED] * 2 def test_limit_not_there(): - limit = bonobo.Limit(42) - results = [] - for i in range(10): - results += list(limit()) + context, results = MagicMock(), [] + + with ContextCurrifier(bonobo.Limit(42)).as_contextmanager(context) as stack: + for i in range(10): + results += list(stack()) + + assert results == [NOT_MODIFIED] * 10 + +def test_limit_default(): + context, results = MagicMock(), [] + + with ContextCurrifier(bonobo.Limit()).as_contextmanager(context) as stack: + for i in range(20): + results += list(stack()) + assert results == [NOT_MODIFIED] * 10 diff --git a/tests/util/test_compat.py b/tests/util/test_compat.py index 83bcfcc..7be5dc3 100644 --- a/tests/util/test_compat.py +++ b/tests/util/test_compat.py @@ -21,4 +21,3 @@ def test_deprecated_alias(): with pytest.warns(DeprecationWarning): foo() - diff --git a/tests/util/test_objects.py b/tests/util/test_objects.py index 8f15f71..d8e6f7d 100644 --- a/tests/util/test_objects.py +++ b/tests/util/test_objects.py @@ -35,6 +35,7 @@ def test_wrapper_name(): def test_valueholder(): x = ValueHolder(42) + assert x == 42 x += 1 assert x == 43 From 88df694dc18cd72dc1c75191497be0fc680dc031 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 21 May 2017 19:24:24 +0200 Subject: [PATCH 019/143] Formating. --- tests/test_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 049dc90..283e3d7 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -11,7 +11,6 @@ def test_count(): with pytest.raises(TypeError): bonobo.count() - context = MagicMock() with ContextCurrifier(bonobo.count).as_contextmanager(context) as stack: @@ -49,6 +48,7 @@ def test_limit_not_there(): assert results == [NOT_MODIFIED] * 10 + def test_limit_default(): context, results = MagicMock(), [] From a45a6830c775218ef5d42f124b2f300138e82318 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 10:35:24 +0200 Subject: [PATCH 020/143] Fix Method to be non positional as there is a randomly happening bug that I cannot trace. --- bonobo/config/configurables.py | 13 +++++++------ bonobo/config/options.py | 9 ++++++--- tests/test_config_method.py | 10 +++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 1d4fdb2..77801fa 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -26,16 +26,19 @@ class ConfigurableMeta(type): if isinstance(value, ContextProcessor): cls.__processors__.append(value) else: + if not value.name: + value.name = name + if isinstance(value, Method): if cls.__wrappable__: raise ConfigurationError( 'Cannot define more than one "Method" option in a configurable. That may change in the future.' ) cls.__wrappable__ = name - if not value.name: - value.name = name + if not name in cls.__options__: cls.__options__[name] = value + if value.positional: cls.__positional_options__.append(name) @@ -53,11 +56,9 @@ class Configurable(metaclass=ConfigurableMeta): def __new__(cls, *args, **kwargs): if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'): - wrapped, args = args[0], args[1:] - return type(wrapped.__name__, (cls, ), {cls.__wrappable__: wrapped}) + return type(args[0].__name__, (cls,), {cls.__wrappable__: args[0]}) - # XXX is that correct ??? how does it pass args/kwargs to __init__ ??? - return super().__new__(cls) + return super(Configurable, cls).__new__(cls) def __init__(self, *args, **kwargs): super().__init__() diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 7fbdf54..545d1ca 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -67,11 +67,12 @@ class Option: def __set__(self, inst, value): inst.__options_values__[self.name] = self.clean(value) + def clean(self, value): + return self.type(value) if self.type else value + def get_default(self): return self.default() if callable(self.default) else self.default - def clean(self, value): - return self.type(value) if self.type else value class Method(Option): @@ -106,7 +107,7 @@ class Method(Option): """ def __init__(self): - super().__init__(None, required=False, positional=True) + super().__init__(None, required=False) def __get__(self, inst, typ): if not self.name in inst.__options_values__: @@ -114,6 +115,8 @@ class Method(Option): return inst.__options_values__[self.name] def __set__(self, inst, value): + if isinstance(value, str): + raise ValueError('should be callable') inst.__options_values__[self.name] = self.type(value) if self.type else value def clean(self, value): diff --git a/tests/test_config_method.py b/tests/test_config_method.py index 2798b31..dbca538 100644 --- a/tests/test_config_method.py +++ b/tests/test_config_method.py @@ -15,7 +15,6 @@ class MethodBasedConfigurable(Configurable): def test_one_wrapper_only(): with pytest.raises(ConfigurationError): - class TwoMethods(Configurable): h1 = Method() h2 = Method() @@ -28,7 +27,12 @@ def test_define_with_decorator(): def Concrete(self, *args, **kwargs): calls.append((args, kwargs, )) + print('handler', Concrete.handler) + + assert callable(Concrete.handler) t = Concrete('foo', bar='baz') + + assert callable(t.handler) assert len(calls) == 0 t() assert len(calls) == 1 @@ -41,6 +45,7 @@ def test_define_with_argument(): calls.append((args, kwargs, )) t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler) + assert callable(t.handler) assert len(calls) == 0 t() assert len(calls) == 1 @@ -54,6 +59,7 @@ def test_define_with_inheritance(): calls.append((args, kwargs, )) t = Inheriting('foo', bar='baz') + assert callable(t.handler) assert len(calls) == 0 t() assert len(calls) == 1 @@ -69,7 +75,9 @@ def test_inheritance_then_decorate(): def Concrete(self, *args, **kwargs): calls.append((args, kwargs, )) + assert callable(Concrete.handler) t = Concrete('foo', bar='baz') + assert callable(t.handler) assert len(calls) == 0 t() assert len(calls) == 1 From 52b06834e228799e80cd80a758ac981b18afa703 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 10:35:54 +0200 Subject: [PATCH 021/143] Formating. --- bonobo/config/configurables.py | 2 +- bonobo/config/options.py | 1 - tests/test_config_method.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 77801fa..aef371b 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -56,7 +56,7 @@ class Configurable(metaclass=ConfigurableMeta): def __new__(cls, *args, **kwargs): if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'): - return type(args[0].__name__, (cls,), {cls.__wrappable__: args[0]}) + return type(args[0].__name__, (cls, ), {cls.__wrappable__: args[0]}) return super(Configurable, cls).__new__(cls) diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 545d1ca..51f4a20 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -74,7 +74,6 @@ class Option: return self.default() if callable(self.default) else self.default - class Method(Option): """ A Method is a special callable-valued option, that can be used in three different ways (but for same purpose). diff --git a/tests/test_config_method.py b/tests/test_config_method.py index dbca538..13eb873 100644 --- a/tests/test_config_method.py +++ b/tests/test_config_method.py @@ -15,6 +15,7 @@ class MethodBasedConfigurable(Configurable): def test_one_wrapper_only(): with pytest.raises(ConfigurationError): + class TwoMethods(Configurable): h1 = Method() h2 = Method() From 39e083e594a806e3518ac3b0a47357eb6b595805 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 10:44:05 +0200 Subject: [PATCH 022/143] Some documentation around context processors. --- bonobo/config/processors.py | 27 +++++++++++++++++++++++++++ docs/guide/transformations.rst | 8 ++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 7475e68..16c7f5a 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -10,6 +10,33 @@ _CONTEXT_PROCESSORS_ATTR = '__processors__' class ContextProcessor(Option): + """ + A ContextProcessor is a kind of transformation decorator that can setup and teardown a transformation and runtime + related dependencies, at the execution level. + + It works like a yielding context manager, and is the recommended way to setup and teardown objects you'll need + in the context of one execution. It's the way to overcome the stateless nature of transformations. + + The yielded values will be passed as positional arguments to the next context processors (order do matter), and + finally to the __call__ method of the transformation. + + Warning: this may change for a similar but simpler implementation, don't relly too much on it (yet). + + Example: + + >>> from bonobo.config import Configurable + >>> from bonobo.util.objects import ValueHolder + + >>> class Counter(Configurable): + ... @ContextProcessor + ... def counter(self, context): + ... yield ValueHolder(0) + ... + ... def __call__(self, counter, *args, **kwargs): + ... counter += 1 + ... yield counter.get() + + """ @property def __name__(self): return self.func.__name__ diff --git a/docs/guide/transformations.rst b/docs/guide/transformations.rst index da1ea55..8222357 100644 --- a/docs/guide/transformations.rst +++ b/docs/guide/transformations.rst @@ -81,9 +81,13 @@ Services .. autoclass:: bonobo.config.Service -Method ------- +Methods +------- .. autoclass:: bonobo.config.Method +ContextProcessors +----------------- + +.. autoclass:: bonobo.config.ContextProcessor From 1813681baba2c03ebd11ed3732db0738e270aafa Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 11:05:48 +0200 Subject: [PATCH 023/143] release: 0.3.0 --- bonobo/_version.py | 2 +- bonobo/config/processors.py | 1 + docs/changelog.rst | 47 ++++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 15e8324..0404d81 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.3.0a1' +__version__ = '0.3.0' diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 16c7f5a..fc89fd4 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -37,6 +37,7 @@ class ContextProcessor(Option): ... yield counter.get() """ + @property def __name__(self): return self.func.__name__ diff --git a/docs/changelog.rst b/docs/changelog.rst index f9e2145..061e02c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,51 @@ Changelog ========= +v.0.3.0 - 22 may 2017 +::::::::::::::::::::: + +Features +-------- + +* ContextProcessors can now be implemented by getting the "yield" value (v = yield x), shortening the teardown-only context processors by one line. +* File related writers (file, csv, json ...) now returns NOT_MODIFIED, making it easier to chain something after. +* More consistent console output, nodes are now sorted in a topological order before display. +* Graph.add_chain(...) now takes _input and _output parameters the same way, accepting indexes, instances or names (subject to change). +* Graph.add_chain(...) now allows to "name" a chain, using _name keyword argument, to easily reference its output later (subject to change). +* New settings module (bonobo.settings) read environment for some global configuration stuff (DEBUG and PROFILE, for now). +* New Method subclass of Option allows to use Configurable objects as decorator (see bonobo.nodes.filter.Filter for a simple example). +* New Filter transformation in standard library. + +Internal features +----------------- + +* Better ContextProcessor implementation, avoiding to use a decorator on the parent class. Now works with Configurable instances like Option, Service and Method. +* ContextCurrifier replaces the logic that was in NodeExecutionContext, that setup and teardown the context stack. Maybe the name is not ideal. +* All builtin transformations are of course updated to use the improved API, and should be 100% backward compatible. +* The "core" package has been dismantled, and its rare remaining members are now in "structs" and "util" packages. +* Standard transformation library has been moved under the bonobo.nodes package. It does not change anything if you used bonobo.* (which you should). +* ValueHolder is now more restrictive, not allowing to use .value anymore. + +Miscellaneous +------------- + +* Code cleanup, dead code removal, more tests, etc. +* More documentation. + +v.0.2.4 - 2 may 2017 +:::::::::::::::::::: + +* Cosmetic release for PyPI package page formating. Same content as v.0.2.3. + +v.0.2.3 - 1 may 2017 +::::::::::::::::::::: + +* Positional options now supported, backward compatible. All FileHandler subclasses supports their path argument as positional. +* Better transformation lifecycle management (still work needed here). +* Windows continuous integration now works. +* Refactoring the "API" a lot to have a much cleaner first glance at it. +* More documentation, tutorials, and tuning project artifacts. + v.0.2.2 - 28 apr 2017 ::::::::::::::::::::: @@ -36,4 +81,4 @@ Initial release * Input/output MUX DEMUX removed, maybe no need for that in the real world. May come back, but not in 1.0 * Change dependency policy. We need to include only the very basic requirements (and very required). Everything related to transforms that we may not use (bs, sqla, ...) should be optional dependencies. -* Execution strategies, threaded by default. \ No newline at end of file +* Execution strategies, threaded by default. From 9bffce7299885655ae1526ab7611ff1c4c6d6c0d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 13:13:43 +0200 Subject: [PATCH 024/143] Update install.rst --- docs/install.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index ec617fb..943ffbe 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,7 +1,16 @@ Installation ============ -Bonobo is `available on PyPI `_, and it's the easiest solution to get started. +Create an ETL project +::::::::::::::::::::: + +If you only want to use Bonobo to code ETLs, your easiest option to get started is to use our +`cookiecutter template `_. + +Install from PyPI +::::::::::::::::: + +You can also install it directly from the `Python Package Index `_. .. code-block:: shell-session From 4e4a3581c98ec093bc4f9417d4539a31c580cf18 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 14:05:57 +0200 Subject: [PATCH 025/143] [cli] implements bonobo run -m and bonobo run . Also adds --quiet option implementation and --verbose option that sets the DEBUG setting on. --- bonobo/_api.py | 21 +++++++++------- bonobo/commands/run.py | 48 +++++++++++++++++++++---------------- bonobo/config/processors.py | 1 + bonobo/settings.py | 10 +++++++- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 85e5a62..41e9623 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -44,15 +44,20 @@ def run(graph, strategy=None, plugins=None, services=None): plugins = plugins or [] - if _is_interactive_console(): # pragma: no cover - from bonobo.ext.console import ConsoleOutputPlugin - if ConsoleOutputPlugin not in plugins: - plugins.append(ConsoleOutputPlugin) + from bonobo import settings - if _is_jupyter_notebook(): # pragma: no cover - from bonobo.ext.jupyter import JupyterOutputPlugin - if JupyterOutputPlugin not in plugins: - plugins.append(JupyterOutputPlugin) + settings.check() + + if not settings.QUIET: # pragma: no cover + if _is_interactive_console(): + from bonobo.ext.console import ConsoleOutputPlugin + if ConsoleOutputPlugin not in plugins: + plugins.append(ConsoleOutputPlugin) + + if _is_jupyter_notebook(): + from bonobo.ext.jupyter import JupyterOutputPlugin + if JupyterOutputPlugin not in plugins: + plugins.append(JupyterOutputPlugin) return strategy.execute(graph, plugins=plugins, services=services) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index b7872e2..2ff2283 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,12 +1,14 @@ -import argparse - import os +import runpy import bonobo DEFAULT_SERVICES_FILENAME = '_services.py' DEFAULT_SERVICES_ATTR = 'get_services' +DEFAULT_GRAPH_FILENAME = '__main__.py' +DEFAULT_GRAPH_ATTR = 'get_graph' + def get_default_services(filename, services=None): dirname = os.path.dirname(filename) @@ -29,24 +31,24 @@ def get_default_services(filename, services=None): return services or {} -def execute(file, quiet=False): - with file: - code = compile(file.read(), file.name, 'exec') +def execute(filename, module, quiet=False, verbose=False): + from bonobo import settings - # TODO: A few special variables should be set before running the file: - # - # See: - # - https://docs.python.org/3/reference/import.html#import-mod-attrs - # - https://docs.python.org/3/library/runpy.html#runpy.run_module - context = { - '__name__': '__bonobo__', - '__file__': file.name, - } + if quiet: + settings.QUIET = True - try: - exec(code, context) - except Exception as exc: - raise + if verbose: + settings.DEBUG = True + + if filename: + if os.path.isdir(filename): + filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME) + context = runpy.run_path(filename, run_name='__bonobo__') + elif module: + context = runpy.run_module(module, run_name='__bonobo__') + filename = context['__file__'] + else: + raise RuntimeError('UNEXPECTED: argparse should not allow this.') graphs = dict((k, v) for k, v in context.items() if isinstance(v, bonobo.Graph)) @@ -63,12 +65,16 @@ def execute(file, quiet=False): graph, plugins=[], services=get_default_services( - file.name, context.get(DEFAULT_SERVICES_ATTR)() if DEFAULT_SERVICES_ATTR in context else None + filename, context.get(DEFAULT_SERVICES_ATTR)() if DEFAULT_SERVICES_ATTR in context else None ) ) def register(parser): - parser.add_argument('file', type=argparse.FileType()) - parser.add_argument('--quiet', action='store_true') + source_group = parser.add_mutually_exclusive_group(required=True) + source_group.add_argument('filename', nargs='?', type=str) + source_group.add_argument('--module', '-m', type=str) + verbosity_group = parser.add_mutually_exclusive_group() + verbosity_group.add_argument('--quiet', '-q', action='store_true') + verbosity_group.add_argument('--verbose', '-v', action='store_true') return execute diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 16c7f5a..fc89fd4 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -37,6 +37,7 @@ class ContextProcessor(Option): ... yield counter.get() """ + @property def __name__(self): return self.func.__name__ diff --git a/bonobo/settings.py b/bonobo/settings.py index aec203d..9f0fbf6 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -9,8 +9,16 @@ def to_bool(s): return False -# Debug mode. +# Debug/verbose mode. DEBUG = to_bool(os.environ.get('BONOBO_DEBUG', 'f')) # Profile mode. PROFILE = to_bool(os.environ.get('BONOBO_PROFILE', 'f')) + +# Quiet mode. +QUIET = to_bool(os.environ.get('BONOBO_QUIET', 'f')) + + +def check(): + if DEBUG and QUIET: + raise RuntimeError('I cannot be verbose and quiet at the same time.') \ No newline at end of file From 329296ce6b35881ddfa191d759f727bfa7a2dca8 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 15:57:11 +0200 Subject: [PATCH 026/143] Better error display. --- bonobo/config/services.py | 3 ++- bonobo/errors.py | 3 +++ bonobo/examples/nodes/_services.py | 5 +++++ bonobo/execution/base.py | 7 +++---- bonobo/nodes/io/xml.py | 0 bonobo/strategies/executor.py | 8 +++----- bonobo/util/errors.py | 26 +++++++++++++++++++++----- 7 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 bonobo/examples/nodes/_services.py create mode 100644 bonobo/nodes/io/xml.py diff --git a/bonobo/config/services.py b/bonobo/config/services.py index 8d6c95e..107130e 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -2,6 +2,7 @@ import re import types from bonobo.config.options import Option +from bonobo.errors import MissingServiceImplementationError _service_name_re = re.compile(r"^[^\d\W]\w*(:?\.[^\d\W]\w*)*$", re.UNICODE) @@ -78,7 +79,7 @@ class Container(dict): if not name in self: if default: return default - raise KeyError('Cannot resolve service {!r} using provided service collection.'.format(name)) + raise MissingServiceImplementationError('Cannot resolve service {!r} using provided service collection.'.format(name)) value = super().get(name) if isinstance(value, types.LambdaType): value = value(self) diff --git a/bonobo/errors.py b/bonobo/errors.py index 4a2e9c5..cdb4db5 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -55,4 +55,7 @@ class ProhibitedOperationError(RuntimeError): class ConfigurationError(Exception): + pass + +class MissingServiceImplementationError(KeyError): pass \ No newline at end of file diff --git a/bonobo/examples/nodes/_services.py b/bonobo/examples/nodes/_services.py new file mode 100644 index 0000000..337bf6b --- /dev/null +++ b/bonobo/examples/nodes/_services.py @@ -0,0 +1,5 @@ +from bonobo import get_examples_path, open_fs + + +def get_services(): + return {'fs': open_fs(get_examples_path())} diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index 6ca22f2..d500e28 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -55,10 +55,9 @@ class LoopingExecutionContext(Wrapper): raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self))) self._started = True - self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) - with unrecoverable(self.handle_error): - self._stack.setup(self) + self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context()) + self._stack.setup(self) for enhancer in self._enhancers: with unrecoverable(self.handle_error): @@ -82,7 +81,7 @@ class LoopingExecutionContext(Wrapper): return try: - with unrecoverable(self.handle_error): + if self._stack: self._stack.teardown() finally: self._stopped = True diff --git a/bonobo/nodes/io/xml.py b/bonobo/nodes/io/xml.py new file mode 100644 index 0000000..e69de29 diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 3f34862..26b810b 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -36,7 +36,7 @@ class ExecutorStrategy(Strategy): plugin_context.loop() plugin_context.stop() except Exception as exc: - print_error(exc, traceback.format_exc(), prefix='Error in plugin context', context=plugin_context) + print_error(exc, traceback.format_exc(), context=plugin_context) futures.append(executor.submit(_runner)) @@ -46,9 +46,7 @@ class ExecutorStrategy(Strategy): try: node_context.start() except Exception as exc: - print_error( - exc, traceback.format_exc(), prefix='Could not start node context', context=node_context - ) + print_error(exc, traceback.format_exc(), context=node_context, method='start') node_context.input.on_end() else: node_context.loop() @@ -56,7 +54,7 @@ class ExecutorStrategy(Strategy): try: node_context.stop() except Exception as exc: - print_error(exc, traceback.format_exc(), prefix='Could not stop node context', context=node_context) + print_error(exc, traceback.format_exc(), context=node_context, method='stop') futures.append(executor.submit(_runner)) diff --git a/bonobo/util/errors.py b/bonobo/util/errors.py index bd1f51f..3160926 100644 --- a/bonobo/util/errors.py +++ b/bonobo/util/errors.py @@ -1,5 +1,7 @@ import sys +from textwrap import indent +from bonobo import settings from bonobo.structs.bags import ErrorBag @@ -7,7 +9,14 @@ def is_error(bag): return isinstance(bag, ErrorBag) -def print_error(exc, trace, context=None, prefix=''): +def _get_error_message(exc): + if hasattr(exc, '__str__'): + message = str(exc) + return message[0].upper() + message[1:] + return '\n'.join(exc.args), + + +def print_error(exc, trace, context=None, method=None): """ Error handler. Whatever happens in a plugin or component, if it looks like an exception, taste like an exception or somehow make me think it is an exception, I'll handle it. @@ -18,14 +27,21 @@ def print_error(exc, trace, context=None, prefix=''): """ from colorama import Fore, Style + + prefix = '{}{} | {}'.format(Fore.RED, Style.BRIGHT, Style.RESET_ALL) + print( Style.BRIGHT, Fore.RED, - '\U0001F4A3 {}{}{}'.format( - (prefix + ': ') if prefix else '', type(exc).__name__, ' in {!r}'.format(context) if context else '' - ), + type(exc).__name__, + ' (in {}{})'.format(type(context).__name__, '.{}()'.format(method) if method else '') if context else '', + Style.RESET_ALL, + '\n', + indent(_get_error_message(exc), prefix+Style.BRIGHT), Style.RESET_ALL, sep='', file=sys.stderr, ) - print(trace) + print(prefix, file=sys.stderr) + print(indent(trace, prefix, predicate=lambda line: True), file=sys.stderr) + From c56d497a8c66e58711e888a20202c2f31dcbf02c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 15:57:47 +0200 Subject: [PATCH 027/143] New simpler pretty printer (experimental) that supports all kind of bags. --- bonobo/nodes/basics.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 195cd8e..025b99f 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -1,6 +1,7 @@ import functools from pprint import pprint as _pprint +import itertools from colorama import Fore, Style from bonobo.config import Configurable, Option @@ -69,6 +70,12 @@ def _count_counter(self, context): context.send(Bag(counter._value)) +class PrettyPrinter(Configurable): + def call(self, *args, **kwargs): + for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): + print(' ' if i else '•', item, '=', str(value).strip().replace('\n', '\n'+CLEAR_EOL), CLEAR_EOL) + + pprint = Tee(_pprint) From ecfdc81db97494aec712db8715d48b3c70a1fb6a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 15:58:14 +0200 Subject: [PATCH 028/143] Removes unneeded __all__ definitions in subpackages. --- bonobo/nodes/io/file.py | 5 ----- bonobo/nodes/io/json.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index c88f601..f3cb1b0 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -4,11 +4,6 @@ from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED from bonobo.util.objects import ValueHolder -__all__ = [ - 'FileReader', - 'FileWriter', -] - class FileHandler(Configurable): """Abstract component factory for file-related components. diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index fdb49b8..b2db708 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -3,10 +3,6 @@ import json from bonobo.config.processors import ContextProcessor from .file import FileWriter, FileReader -__all__ = [ - 'JsonWriter', -] - class JsonHandler(): eol = ',\n' From f27db31b917b8336adf65d86d215b34348ba8105 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 17:18:47 +0200 Subject: [PATCH 029/143] Update index.rst --- docs/tutorial/index.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 4627357..d9b1886 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -1,8 +1,6 @@ First steps =========== -Bonobo uses simple python and should be quick and easy to learn. - What is Bonobo? ::::::::::::::: @@ -13,7 +11,6 @@ Bonobo *is not* a statistical or data-science tool. If you're looking for a data Bonobo is a lean manufacturing assembly line for data that let you focus on the actual work instead of the plumbery. - Tutorial :::::::: @@ -43,6 +40,6 @@ Read about integrating external tools with bonobo * :doc:`../guide/ext/docker`: run transformation graphs in isolated containers. * :doc:`../guide/ext/jupyter`: run transformations within jupyter notebooks. -* :doc:`../guide/ext/selenium`: run +* :doc:`../guide/ext/selenium`: crawl the web using a real browser and work with the gathered data. * :doc:`../guide/ext/sqlalchemy`: everything you need to interract with SQL databases. From 46ab71c193f2f898e7c437fae9cf2dda68a716cf Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 17:21:33 +0200 Subject: [PATCH 030/143] Update index.rst --- docs/tutorial/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index d9b1886..c00f27a 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -11,9 +11,16 @@ Bonobo *is not* a statistical or data-science tool. If you're looking for a data Bonobo is a lean manufacturing assembly line for data that let you focus on the actual work instead of the plumbery. +Bonobo uses simple python and should be quick and easy to learn. + Tutorial :::::::: +Warning: the documentation is still in progress. Although all content here should be accurate, you may feel a lack of +completeness, for which we plaid guilty and apologize. If there is something blocking, please come on our +`slack channel `_ and complain, we'll figure something out. If there is something +that did not block you but can be a no-go for others, please consider contributing to the docs. + .. toctree:: :maxdepth: 2 From 9ad1d19dfc9934b2f0a5b5c45b67b79ac42bef18 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 17:38:59 +0200 Subject: [PATCH 031/143] Update faq.rst --- docs/faq.rst | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 6938dcd..6f3cd3f 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -3,6 +3,7 @@ F.A.Q. List of questions that went up about the project, in no particuliar order. + Too long; didn't read. ---------------------- @@ -19,8 +20,22 @@ It's lean manufacturing for data. .. note:: - This is NOT a «big data» tool. We process around 5 millions database lines in around 1 hour with rdc.etl, bonobo - ancestor (algorithms are the same, we still need to run a bit of benchmarks). + This is NOT a «big data» tool. Neither a «data analysis» tool. We process around 5 millions database lines in around + 1 hour with rdc.etl, bonobo ancestor (algorithms are the same, we still need to run a bit of benchmarks). + + +What versions of python does bonobo support? Why not more? +---------------------------------------------------------- + +Bonobo is battle-tested against the latest python 3.5 and python 3.6. It may work well using other patch releases of those +versions, but we cannot guarantee it. + +The main reasons about why 3.5+: + +* Creating a tool that works well under both python 2 and 3 is a lot more work. +* Python 3 is nearly 10 years old. Consider moving on. +* Python 3.5 contains syntaxic sugar that makes working with data a lot more convenient. + Can a graph contain another graph? ---------------------------------- @@ -30,8 +45,14 @@ No, not for now. There are no tools today in bonobo to insert a graph as a subgr It would be great to allow it, but there is a few design questions behind this, like what node you use as input and output of the subgraph, etc. +On another hand, if you don't consider a graph as the container but by the nodes and edges it contains, its pretty +easy to add a set of nodes and edge to a subgraph, and thus simulate it. But there will be more threads, more copies +of the same nodes, so it's not really an acceptable answer for big graphs. If it was possible to use a Graph as a +node, then the problem would be correctly solved. + It is something to be seriously considered post 1.0 (probably way post 1.0). + How would one access contextual data from a transformation? Are there parameter injections like pytest's fixtures? ------------------------------------------------------------------------------------------------------------------ @@ -43,20 +64,26 @@ to find a better way to apply it. To understand how it works today, look at https://github.com/python-bonobo/bonobo/blob/0.3/bonobo/io/csv.py#L63 and class hierarchy. + What is a plugin? Do I need to write one? ----------------------------------------- Plugins are special classes added to an execution context, used to enhance or change the actual behavior of an execution in a generic way. You don't need to write plugins to code transformation graphs. + Is there a difference between a transformation node and a regular python function or generator? ----------------------------------------------------------------------------------------------- -No. +Short answer: no. Transformation callables are just regular callables, and there is nothing that differentiate it from regular python callables. You can even use some callables both in an imperative programming context and in a transformation graph, no problem. +Longer answer: yes, sometimes, but you should not care. The function-based transformations are plain old python callable. The +class-based transformations can be plain-old-python-objects, but can also subclass Configurable which brings a lot of +fancy features, like options, service injections, class factories as decorators... + Why did you include the word «marketing» in a commit message? Why is there a marketing-automation tag on the project? Isn't marketing evil? ------------------------------------------------------------------------------------------------------------------------------------------- @@ -83,6 +110,7 @@ See https://github.com/python-bonobo/bonobo/issues/1 Bonobo is not a replacement for pandas, nor dask, nor luigi, nor airflow... It may be a replacement for Pentaho, Talend or other data integration suites but targets people more comfortable with code as an interface. + All those references to monkeys hurt my head. Bonobos are not monkeys. ---------------------------------------------------------------------- @@ -96,6 +124,7 @@ known primate typing feature.» See https://github.com/python-bonobo/bonobo/issues/24 + Who is behind this? ------------------- @@ -104,6 +133,7 @@ Me (as an individual), and a few great people that helped me along the way. Not The code, documentation, and surrounding material is created using spare time and may lack a bit velocity. Feel free to jump in so we can go faster! + Documentation seriously lacks X, there is a problem in Y... ----------------------------------------------------------- From cf21c0e0883358a4014d13b19927656b604c1a29 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 19:17:34 +0200 Subject: [PATCH 032/143] [qa] attempt to fix #58. --- bonobo/execution/base.py | 7 ++++++- bonobo/execution/plugin.py | 19 +++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index d500e28..cefde32 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -8,6 +8,12 @@ from bonobo.plugins import get_enhancers from bonobo.util.errors import print_error from bonobo.util.objects import Wrapper, get_name +@contextmanager +def recoverable(error_handler): + try: + yield + except Exception as exc: # pylint: disable=broad-except + error_handler(exc, traceback.format_exc()) @contextmanager def unrecoverable(error_handler): @@ -17,7 +23,6 @@ def unrecoverable(error_handler): error_handler(exc, traceback.format_exc()) raise # raise unrecoverableerror from x ? - class LoopingExecutionContext(Wrapper): alive = True PERIOD = 0.25 diff --git a/bonobo/execution/plugin.py b/bonobo/execution/plugin.py index db5c0db..d928f4a 100644 --- a/bonobo/execution/plugin.py +++ b/bonobo/execution/plugin.py @@ -1,6 +1,4 @@ -import traceback - -from bonobo.execution.base import LoopingExecutionContext +from bonobo.execution.base import LoopingExecutionContext, recoverable class PluginExecutionContext(LoopingExecutionContext): @@ -14,21 +12,14 @@ class PluginExecutionContext(LoopingExecutionContext): def start(self): super().start() - try: + with recoverable(self.handle_error): self.wrapped.initialize() - except Exception as exc: # pylint: disable=broad-except - self.handle_error(exc, traceback.format_exc()) def shutdown(self): - try: + with recoverable(self.handle_error): self.wrapped.finalize() - except Exception as exc: # pylint: disable=broad-except - self.handle_error(exc, traceback.format_exc()) - finally: - self.alive = False + self.alive = False def step(self): - try: + with recoverable(self.handle_error): self.wrapped.run() - except Exception as exc: # pylint: disable=broad-except - self.handle_error(exc, traceback.format_exc()) From 56f9c334f6b628435d8cf2576c146cdbee4e32df Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 19:25:35 +0200 Subject: [PATCH 033/143] [qa] coverage of version command. --- tests/test_commands.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 55032b2..a0e28e2 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,7 +1,7 @@ import pkg_resources import pytest -from bonobo import get_examples_path +from bonobo import get_examples_path, __version__ from bonobo.commands import entrypoint @@ -33,3 +33,11 @@ def test_run(capsys): assert out[0].startswith('Foo ') assert out[1].startswith('Bar ') assert out[2].startswith('Baz ') + +def test_version(capsys): + entrypoint(['version']) + out, err = capsys.readouterr() + out = out.strip() + assert out.startswith('bonobo ') + assert out.endswith(__version__) + From a50b21e46d343a0df79cda69d65962a0ea8ce30c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 19:57:08 +0200 Subject: [PATCH 034/143] [qa] covers __main__ --- Makefile | 2 +- setup.py | 50 ++++++++++++++++++++++++------------------ tests/test_commands.py | 35 +++++++++++++++++++---------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 6db0f7c..3ea8912 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-03 18:02:59.359160 +# Updated at 2017-05-22 19:54:27.969596 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/setup.py b/setup.py index 844240a..81f2be3 100644 --- a/setup.py +++ b/setup.py @@ -18,13 +18,19 @@ except NameError: # Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() +try: + with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() +except: + long_description = '' # Get the classifiers from the classifiers file tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n')))) -with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f: - classifiers = tolines(f.read()) +try: + with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f: + classifiers = tolines(f.read()) +except: + classifiers = [] version_ns = {} try: @@ -36,41 +42,43 @@ else: setup( name='bonobo', - description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' - 'python 3.5+.'), + description= + ('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' + 'python 3.5+.'), license='Apache License, Version 2.0', install_requires=[ - 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' + 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', + 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' ], version=version, long_description=long_description, classifiers=classifiers, packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, - data_files=[ - ( - 'share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map' - ] - ) - ], + data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', + 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map' + ])], extras_require={ 'dev': [ - 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', + 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', + 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', 'sphinx_rtd_theme', 'yapf' ], 'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5'] }, entry_points={ 'bonobo.commands': [ - 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', + 'init = bonobo.commands.init:register', + 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], - 'edgy.project.features': ['bonobo = ' - 'bonobo.ext.edgy.project.feature:BonoboFeature'] + 'edgy.project.features': + ['bonobo = ' + 'bonobo.ext.edgy.project.feature:BonoboFeature'] }, url='https://www.bonobo-project.org/', - download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), -) + download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'. + format(version=version), ) diff --git a/tests/test_commands.py b/tests/test_commands.py index a0e28e2..66ca6e3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,10 +1,21 @@ import pkg_resources import pytest -from bonobo import get_examples_path, __version__ +from bonobo import __version__, get_examples_path from bonobo.commands import entrypoint +def runner_entrypoint(*args): + return entrypoint(list(args)) + + +def runner_module(*args): + return entrypoint(list(args)) + + +all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) + + def test_entrypoint(): commands = {} @@ -13,31 +24,31 @@ def test_entrypoint(): assert 'init' in commands assert 'run' in commands + assert 'version' in commands -def test_no_command(capsys): +@all_runners +def test_no_command(runner, capsys): with pytest.raises(SystemExit): - entrypoint([]) + runner() _, err = capsys.readouterr() assert 'error: the following arguments are required: command' in err -def test_init(): - pass # need ext dir - - -def test_run(capsys): - entrypoint(['run', '--quiet', get_examples_path('types/strings.py')]) +@all_runners +def test_run(runner, capsys): + runner('run', '--quiet', get_examples_path('types/strings.py')) out, err = capsys.readouterr() out = out.split('\n') assert out[0].startswith('Foo ') assert out[1].startswith('Bar ') assert out[2].startswith('Baz ') -def test_version(capsys): - entrypoint(['version']) + +@all_runners +def test_version(runner, capsys): + runner('version') out, err = capsys.readouterr() out = out.strip() assert out.startswith('bonobo ') assert out.endswith(__version__) - From 1dccad883dbc1b718e64d88838b80c3f2efedde4 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 20:06:26 +0200 Subject: [PATCH 035/143] [qa] covers __main__, and formating. --- bonobo/config/services.py | 4 +++- bonobo/errors.py | 3 ++- bonobo/execution/base.py | 3 +++ bonobo/nodes/basics.py | 2 +- bonobo/util/errors.py | 3 +-- setup.py | 36 +++++++++++++++++------------------- tests/test_commands.py | 9 +++++++-- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/bonobo/config/services.py b/bonobo/config/services.py index 107130e..a15ee12 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -79,7 +79,9 @@ class Container(dict): if not name in self: if default: return default - raise MissingServiceImplementationError('Cannot resolve service {!r} using provided service collection.'.format(name)) + raise MissingServiceImplementationError( + 'Cannot resolve service {!r} using provided service collection.'.format(name) + ) value = super().get(name) if isinstance(value, types.LambdaType): value = value(self) diff --git a/bonobo/errors.py b/bonobo/errors.py index cdb4db5..564950d 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -57,5 +57,6 @@ class ProhibitedOperationError(RuntimeError): class ConfigurationError(Exception): pass + class MissingServiceImplementationError(KeyError): - pass \ No newline at end of file + pass diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index cefde32..779f212 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -8,6 +8,7 @@ from bonobo.plugins import get_enhancers from bonobo.util.errors import print_error from bonobo.util.objects import Wrapper, get_name + @contextmanager def recoverable(error_handler): try: @@ -15,6 +16,7 @@ def recoverable(error_handler): except Exception as exc: # pylint: disable=broad-except error_handler(exc, traceback.format_exc()) + @contextmanager def unrecoverable(error_handler): try: @@ -23,6 +25,7 @@ def unrecoverable(error_handler): error_handler(exc, traceback.format_exc()) raise # raise unrecoverableerror from x ? + class LoopingExecutionContext(Wrapper): alive = True PERIOD = 0.25 diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 025b99f..5ce550c 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -73,7 +73,7 @@ def _count_counter(self, context): class PrettyPrinter(Configurable): def call(self, *args, **kwargs): for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): - print(' ' if i else '•', item, '=', str(value).strip().replace('\n', '\n'+CLEAR_EOL), CLEAR_EOL) + print(' ' if i else '•', item, '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL) pprint = Tee(_pprint) diff --git a/bonobo/util/errors.py b/bonobo/util/errors.py index 3160926..0ea4e58 100644 --- a/bonobo/util/errors.py +++ b/bonobo/util/errors.py @@ -37,11 +37,10 @@ def print_error(exc, trace, context=None, method=None): ' (in {}{})'.format(type(context).__name__, '.{}()'.format(method) if method else '') if context else '', Style.RESET_ALL, '\n', - indent(_get_error_message(exc), prefix+Style.BRIGHT), + indent(_get_error_message(exc), prefix + Style.BRIGHT), Style.RESET_ALL, sep='', file=sys.stderr, ) print(prefix, file=sys.stderr) print(indent(trace, prefix, predicate=lambda line: True), file=sys.stderr) - diff --git a/setup.py b/setup.py index 81f2be3..feabb8c 100644 --- a/setup.py +++ b/setup.py @@ -42,43 +42,41 @@ else: setup( name='bonobo', - description= - ('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' - 'python 3.5+.'), + description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' + 'python 3.5+.'), license='Apache License, Version 2.0', install_requires=[ - 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', - 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' + 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' ], version=version, long_description=long_description, classifiers=classifiers, packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, - data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map' - ])], + data_files=[ + ( + 'share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map' + ] + ) + ], extras_require={ 'dev': [ - 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', - 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', + 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', 'sphinx_rtd_theme', 'yapf' ], 'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5'] }, entry_points={ 'bonobo.commands': [ - 'init = bonobo.commands.init:register', - 'run = bonobo.commands.run:register', + 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], - 'edgy.project.features': - ['bonobo = ' - 'bonobo.ext.edgy.project.feature:BonoboFeature'] + 'edgy.project.features': ['bonobo = ' + 'bonobo.ext.edgy.project.feature:BonoboFeature'] }, url='https://www.bonobo-project.org/', - download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'. - format(version=version), ) + download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), +) diff --git a/tests/test_commands.py b/tests/test_commands.py index 66ca6e3..a8cd5b1 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,7 +1,11 @@ +import runpy +import sys +from unittest.mock import patch + import pkg_resources import pytest -from bonobo import __version__, get_examples_path +from bonobo import __main__, __version__, get_examples_path from bonobo.commands import entrypoint @@ -10,7 +14,8 @@ def runner_entrypoint(*args): def runner_module(*args): - return entrypoint(list(args)) + with patch.object(sys, 'argv', ['bonobo', *args]): + return runpy.run_path(__main__.__file__, run_name='__main__') all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module]) From 91c9322c58094c23aa68b5b665cec2a0122a64a1 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 20:29:35 +0200 Subject: [PATCH 036/143] [qa] tests bonobo run using module path. --- tests/test_commands.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index a8cd5b1..52428b6 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -50,6 +50,16 @@ def test_run(runner, capsys): assert out[2].startswith('Baz ') +@all_runners +def test_run_module(runner, capsys): + runner('run', '--quiet', '-m', 'bonobo.examples.types.strings') + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0].startswith('Foo ') + assert out[1].startswith('Bar ') + assert out[2].startswith('Baz ') + + @all_runners def test_version(runner, capsys): runner('version') From 04f2088220bafa549368f35d1277109135667e42 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 20:48:16 +0200 Subject: [PATCH 037/143] [qa] add test for bonobo run . --- bonobo/examples/types/__main__.py | 3 +++ tests/test_commands.py | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 bonobo/examples/types/__main__.py diff --git a/bonobo/examples/types/__main__.py b/bonobo/examples/types/__main__.py new file mode 100644 index 0000000..3d1549f --- /dev/null +++ b/bonobo/examples/types/__main__.py @@ -0,0 +1,3 @@ +from bonobo.util.python import require + +graph = require('strings').graph diff --git a/tests/test_commands.py b/tests/test_commands.py index 52428b6..ff358b3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -59,6 +59,15 @@ def test_run_module(runner, capsys): assert out[1].startswith('Bar ') assert out[2].startswith('Baz ') +@all_runners +def test_run_path(runner, capsys): + runner('run', '--quiet', get_examples_path('types')) + out, err = capsys.readouterr() + out = out.split('\n') + assert out[0].startswith('Foo ') + assert out[1].startswith('Bar ') + assert out[2].startswith('Baz ') + @all_runners def test_version(runner, capsys): From 1ba31191eef01c5deadb6a6190d24f7c75aa94bb Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 22:22:36 +0200 Subject: [PATCH 038/143] [qa] adds a rather stupid test to check valueholder works correctly. Still some operations missing. --- bonobo/util/testing.py | 10 +++++++ tests/test_commands.py | 1 + tests/util/test_objects.py | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index 7cf3da0..d5b6cc8 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from unittest.mock import MagicMock from bonobo.execution.node import NodeExecutionContext @@ -7,3 +8,12 @@ class CapturingNodeExecutionContext(NodeExecutionContext): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.send = MagicMock() + + +@contextmanager +def optional_contextmanager(cm, *, ignore=False): + if cm is None or ignore: + yield + else: + with cm: + yield diff --git a/tests/test_commands.py b/tests/test_commands.py index ff358b3..40a6ed5 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -59,6 +59,7 @@ def test_run_module(runner, capsys): assert out[1].startswith('Bar ') assert out[2].startswith('Baz ') + @all_runners def test_run_path(runner, capsys): runner('run', '--quiet', get_examples_path('types')) diff --git a/tests/util/test_objects.py b/tests/util/test_objects.py index d8e6f7d..c6e30b2 100644 --- a/tests/util/test_objects.py +++ b/tests/util/test_objects.py @@ -1,4 +1,9 @@ +import operator + +import pytest + from bonobo.util.objects import Wrapper, get_name, ValueHolder +from bonobo.util.testing import optional_contextmanager class foo: @@ -52,3 +57,56 @@ def test_valueholder(): assert y == x assert y is not x assert repr(x) == repr(y) == repr(43) + + +unsupported_operations = { + int: {operator.matmul}, + str: { + operator.sub, operator.mul, operator.matmul, operator.floordiv, operator.truediv, operator.mod, divmod, + operator.pow, operator.lshift, operator.rshift, operator.and_, operator.xor, operator.or_ + }, +} + + +@pytest.mark.parametrize('x,y', [(5, 3), (0, 10), (0, 0), (1, 1), ('foo', 'bar'), ('', 'baz!')]) +@pytest.mark.parametrize( + 'operation,inplace_operation', [ + (operator.add, operator.iadd), + (operator.sub, operator.isub), + (operator.mul, operator.imul), + (operator.matmul, operator.imatmul), + (operator.truediv, operator.itruediv), + (operator.floordiv, operator.ifloordiv), + (operator.mod, operator.imod), + (divmod, None), + (operator.pow, operator.ipow), + (operator.lshift, operator.ilshift), + (operator.rshift, operator.irshift), + (operator.and_, operator.iand), + (operator.xor, operator.ixor), + (operator.or_, operator.ior), + ] +) +def test_valueholder_integer_operations(x, y, operation, inplace_operation): + v = ValueHolder(x) + + is_supported = operation not in unsupported_operations.get(type(x), set()) + + isdiv = ('div' in operation.__name__) or ('mod' in operation.__name__) + + # forward... + with optional_contextmanager(pytest.raises(TypeError), ignore=is_supported): + with optional_contextmanager(pytest.raises(ZeroDivisionError), ignore=y or not isdiv): + assert operation(x, y) == operation(v, y) + + # backward... + with optional_contextmanager(pytest.raises(TypeError), ignore=is_supported): + with optional_contextmanager(pytest.raises(ZeroDivisionError), ignore=x or not isdiv): + assert operation(y, x) == operation(y, v) + + # in place... + if inplace_operation is not None: + with optional_contextmanager(pytest.raises(TypeError), ignore=is_supported): + with optional_contextmanager(pytest.raises(ZeroDivisionError), ignore=y or not isdiv): + inplace_operation(v, y) + assert v == operation(x, y) From 02e038b4b35228071410dac25b04b4b3fcee1574 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 22 May 2017 22:34:33 +0200 Subject: [PATCH 039/143] [qa] removes a bunch of unused code. --- bonobo/commands/run.py | 6 ++---- bonobo/config/processors.py | 10 +--------- bonobo/config/services.py | 1 + tests/test_config_method.py | 2 -- tests/test_publicapi.py | 4 ++-- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 2ff2283..25fe956 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -20,10 +20,8 @@ def get_default_services(filename, services=None): '__name__': '__bonobo__', '__file__': services_filename, } - try: - exec(code, context) - except Exception: - raise + exec(code, context) + return { **context[DEFAULT_SERVICES_ATTR](), **(services or {}), diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index fc89fd4..d441b6e 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -1,4 +1,3 @@ -import types from collections import Iterable from contextlib import contextmanager @@ -132,14 +131,7 @@ def resolve_processors(mixed): try: yield from mixed.__processors__ except AttributeError: - # old code, deprecated usage - if isinstance(mixed, types.FunctionType): - yield from getattr(mixed, _CONTEXT_PROCESSORS_ATTR, ()) - - for cls in reversed((mixed if isinstance(mixed, type) else type(mixed)).__mro__): - yield from cls.__dict__.get(_CONTEXT_PROCESSORS_ATTR, ()) - - return () + yield from () get_context_processors = deprecated_alias('get_context_processors', resolve_processors) diff --git a/bonobo/config/services.py b/bonobo/config/services.py index a15ee12..30aca65 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -83,6 +83,7 @@ class Container(dict): 'Cannot resolve service {!r} using provided service collection.'.format(name) ) value = super().get(name) + # XXX this is not documented and can lead to errors. if isinstance(value, types.LambdaType): value = value(self) return value diff --git a/tests/test_config_method.py b/tests/test_config_method.py index 13eb873..3a5f6a3 100644 --- a/tests/test_config_method.py +++ b/tests/test_config_method.py @@ -28,8 +28,6 @@ def test_define_with_decorator(): def Concrete(self, *args, **kwargs): calls.append((args, kwargs, )) - print('handler', Concrete.handler) - assert callable(Concrete.handler) t = Concrete('foo', bar='baz') diff --git a/tests/test_publicapi.py b/tests/test_publicapi.py index 0ce6323..6b554e1 100644 --- a/tests/test_publicapi.py +++ b/tests/test_publicapi.py @@ -1,4 +1,4 @@ -import types +import inspect def test_wildcard_import(): @@ -10,7 +10,7 @@ def test_wildcard_import(): if name.startswith('_'): continue attr = getattr(bonobo, name) - if isinstance(attr, types.ModuleType): + if inspect.ismodule(attr): continue assert name in bonobo.__all__ From f2befe9a253b377d187ab731adb64b82cc48a794 Mon Sep 17 00:00:00 2001 From: Michael Copeland Date: Wed, 24 May 2017 22:20:23 -0400 Subject: [PATCH 040/143] Add pickle io functionality --- bonobo/_api.py | 4 ++- bonobo/nodes/io/__init__.py | 3 ++ bonobo/nodes/io/pickle.py | 69 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 bonobo/nodes/io/pickle.py diff --git a/bonobo/_api.py b/bonobo/_api.py index 85e5a62..2090a28 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -2,7 +2,7 @@ import warnings from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrint, Tee, count, identity, noop, pprint + PrettyPrint, PickleWriter, PickleReader, Tee, count, identity, noop, pprint from bonobo.strategies import create_strategy from bonobo.util.objects import get_name @@ -93,6 +93,8 @@ register_api_group( JsonReader, JsonWriter, Limit, + PickleReader, + PickleWriter, PrettyPrint, Tee, count, diff --git a/bonobo/nodes/io/__init__.py b/bonobo/nodes/io/__init__.py index f814c31..f364dd9 100644 --- a/bonobo/nodes/io/__init__.py +++ b/bonobo/nodes/io/__init__.py @@ -3,6 +3,7 @@ from .file import FileReader, FileWriter from .json import JsonReader, JsonWriter from .csv import CsvReader, CsvWriter +from .pickle import PickleReader, PickleWriter __all__ = [ 'CsvReader', @@ -11,4 +12,6 @@ __all__ = [ 'FileWriter', 'JsonReader', 'JsonWriter', + 'PickleReader', + 'PickleWriter', ] diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py new file mode 100644 index 0000000..032b036 --- /dev/null +++ b/bonobo/nodes/io/pickle.py @@ -0,0 +1,69 @@ +import pickle + +from bonobo.config.processors import ContextProcessor +from bonobo.config import Option +from bonobo.constants import NOT_MODIFIED +from bonobo.util.objects import ValueHolder +from .file import FileReader, FileWriter, FileHandler + + +class PickleHandler(FileHandler): + """ + + .. attribute:: item_names + + The names of the items in the pickle, if it is not defined in the first item of the pickle. + + """ + + item_names = Option(tuple) + + +class PickleReader(PickleHandler, FileReader): + """ + Reads a Python pickle object and yields the items in dicts. + """ + + mode = Option(str, default='rb') + + @ContextProcessor + def pickle_items(self, context, fs, file): + yield ValueHolder(self.item_names) + + def read(self, fs, file, item_names): + data = pickle.load(file) + + # if the data is not iterable, then wrap the object in a list so it may be iterated + if isinstance(data, dict): + is_dict = True + iterator = iter(data.items()) + else: + is_dict = False + try: + iterator = iter(data) + except TypeError: + iterator = iter([data]) + + if not item_names.get(): + item_names.set(next(iterator)) + + item_count = len(item_names.value) + + for i in iterator: + if len(i) != item_count: + raise ValueError('Received an object with %d items, expecting %d.' % (len(i), item_count, )) + + yield dict(zip(i)) if is_dict else dict(zip(item_names.value, i)) + + +class PickleWriter(PickleHandler, FileWriter): + + mode = Option(str, default='wb') + + def write(self, fs, file, itemno, item): + """ + Write a pickled item to the opened file. + """ + file.write(pickle.dumps(item)) + itemno += 1 + return NOT_MODIFIED From 5d8f4c3dc7fbd8bc7380e272889640271b512582 Mon Sep 17 00:00:00 2001 From: Michael Copeland Date: Wed, 24 May 2017 22:20:59 -0400 Subject: [PATCH 041/143] Add tests for pickle functionality --- tests/io/test_pickle.py | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/io/test_pickle.py diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py new file mode 100644 index 0000000..a8333ef --- /dev/null +++ b/tests/io/test_pickle.py @@ -0,0 +1,60 @@ +import pickle +import pytest + +from bonobo import Bag, PickleReader, PickleWriter, open_fs +from bonobo.constants import BEGIN, END +from bonobo.execution.node import NodeExecutionContext +from bonobo.util.testing import CapturingNodeExecutionContext + + +def test_write_pickled_dict_to_file(tmpdir): + fs, filename = open_fs(tmpdir), 'output.pkl' + + writer = PickleWriter(path=filename) + context = NodeExecutionContext(writer, services={'fs': fs}) + + context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) + + context.start() + context.step() + context.step() + context.stop() + + assert pickle.loads(fs.open(filename, 'rb').read()) == {'foo': 'bar'} + + with pytest.raises(AttributeError): + getattr(context, 'file') + + +def test_read_pickled_list_from_file(tmpdir): + fs, filename = open_fs(tmpdir), 'input.pkl' + fs.open(filename, 'wb').write(pickle.dumps([ + ['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar'] + ])) + + reader = PickleReader(path=filename) + + context = CapturingNodeExecutionContext(reader, services={'fs': fs}) + + context.start() + context.write(BEGIN, Bag(), END) + context.step() + context.stop() + + assert len(context.send.mock_calls) == 2 + + args0, kwargs0 = context.send.call_args_list[0] + assert len(args0) == 1 and not len(kwargs0) + args1, kwargs1 = context.send.call_args_list[1] + assert len(args1) == 1 and not len(kwargs1) + + assert args0[0].args[0] == { + 'a': 'a foo', + 'b': 'b foo', + 'c': 'c foo', + } + assert args1[0].args[0] == { + 'a': 'a bar', + 'b': 'b bar', + 'c': 'c bar', + } From 2ffbc90bcb74606aca4bc6c75dd52a2ce4d6875b Mon Sep 17 00:00:00 2001 From: Michael Copeland Date: Wed, 24 May 2017 22:21:29 -0400 Subject: [PATCH 042/143] Add examples for pickle functionality --- bonobo/examples/datasets/spam.tgz | Bin 0 -> 6231 bytes bonobo/examples/files/pickle_handlers.py | 58 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 bonobo/examples/datasets/spam.tgz create mode 100644 bonobo/examples/files/pickle_handlers.py diff --git a/bonobo/examples/datasets/spam.tgz b/bonobo/examples/datasets/spam.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5ffddab49eecb13a51b6a108a086d443fb33fa41 GIT binary patch literal 6231 zcmV-d7^vqTiwFqQ2qjqp|8sC*Z7y_YdI0TOXOtY-RhGTZIp^?NyVgj%qgMAMg=Kd< zni*+EnxLH-?UX=^dENb{ySA(9b){*wuwgM^3^)O{0UL~sS)u_GZNMg2Cg+@!$vNlb z@4K&hs>jFY@CP`DbGnYssJiOC`|i8(yZ2Sv(_yvO4UYUTUNu~Eb93@Pu8;YDYIbhA zexyEKuh*w)Gt*PkM{4!j-0bWTRXg&3aOG*H6Q%w~HaWVv|0mZ&w>=OaIsQQWh~wVl z@Uy8i)3(VVex!FBex_j>-)0_&A9Y-Qc61m&+OxR3XjL9%exM%A6C1Tv&-zg|shWwt zIjC0E!oKzc-3W}bQ7}+`q#AkKPgSJaiP4$r+azeIKr4e6dwV9UV$z}?8I^TRVpQ8^ zN{1GAea(0AW5C2?OC}!Yy%=|!U64pP^^)!S;G*yEsT@oCobq@~dBSn;QQQWMJbt40 zcHCY_j8P`a65(n}Y1Qw7!=?>;Dzl&vsS68gG4H88KhlB9x>>5G>S?Cd^*~M4YO__f zV{T*|h4E)j*Qe^dNlerD`$N)kZqm%R(eX5EsSKkN$o0g|vBim0>0S>D*3Qh#PMxXO zYNu!IzDeq5Cervx;P}bqCZFQfad%a<@<^pvJhjT`1amu9Hhon!l@1`!)_`J@LEQLL zLFQ>CnWuaA;BG^A)T(OPXd=U4KkBLu#cWbCpQn41{2gEiU@)M< zK66Bt&+^_{$Z|bxs}(aS^wt7@RD_>lqYVR|rd8jVE`CL}uQbR;mfCNrhHiE%YIOo; z;CBr+7aFyPoepfSD1DoNih>k_U|#Jsl*MT8hNmdeW~VKDrzUDms#_0#=-u_|Y)#bA z+>f6PW}j2;|GC~e?w0q#OgK=D#FAA-V}y~(+N9Jox(n@R9TggAxWg@psfNZ4(tayd zJ*Y9ts`2x%#PiD~Uf{hGcMEtA%Gf{xa}X0y6YC~`7r4L2Hi`xi}EO~sw;^}Q+4NA?xSgY12Vvt9>2h23j&1wA58ighyybb_LF0;00@e zX=WH#TD}%NoUPSVFY#|0JnrkHRZ%-&D$sr?TTr#LwYmD~T771&4u^NoZOf?3Ytu7$ za=#Azma0#|#PcMT;8BfV0=8c|!nX3>hT9e0avKe8!-sI|b=*Z+2ebGrG4pVcE&8LM z1&-6Jy%zMxyNt$;duXU>gOW<>OrWW;Wi67XjaNj7Gzzuu7Aq(%T6JM-c^Q1dpOMMV zo|&sn*WwBoK3ZDG?Ouqx)rk=P2Hit~*j`AJ$2_`(U-)HTN11S^gRasIn~STaFnPpC zj2uWr%n;w&9R%C%JlqEwX+qO2Y?Pjgw=G10_ztk}vJsgb^InVF?NtxHqobyCdj`A3 zh3%CJc4>h9Ti0_+_hVef?Z4u-m|i_0536?JWBYkdkx)g^>D zNrxtE!00=EkKvn&6YsA%p09}IT|#8(*$k$4BNI(C-kej5YYVIEo%B8|YAt<2 zC(RC{AMJ+gCe)Lxo;st`2)lG7HvpP#FvK7nRS}ze(1D*CSOc~V#x;VD7Vo$v%oEt6oF%z^2_#8a|}z($kK{LrM;_#}2#Eq8Xx zyPZ2zD+~88s|B^Ru(PZ-H`MmkjirTaYIk*K1%KAoDwVi~iS-dZPI+&^t><~pPuA_e zZx(~RagJdiPm2vT6VvjE76QN76-n(bTtSqDbf8rJj+kMYH^E-2+lD^vMl;Rad#~zb zS?}DbQ+?g5sv9?MIPVR)TocPxk34&v^GSjdTDo664GeZrlfiK<eS&TACLRMJ@HvAbFN(GZtq@ZRze|w8JJ9V>)~5(Nu3=71i*NV z37DZi_Kgpq0^9^8$DaV2_l&6JUU9GGs2$J@_5o#@1|i4S>Lf!C#O$YzP}}MtJ(U?9 zLI-^$7y1xX7HPs6;vv4008l%9TA8+5-nh87u(4D=+errJb{E!`wMwVBjXG2(eJ{qj-}3*HGDVJb3!b0xmbQvpAH@u9;~ zpO4RD`eHeK$(!Z$k396=TW`PhK89YZmR<;WMrWEDri+f|Kf$RF3XF`GLFz(D>Y_Kt z*lE?R*S!J=0Mq>hVXuImJ49Z6{sAj3jbLqIrZ7N1A{bi%t<{p&CC?*L*vnu50GNK- zq4x%P)a*D6=YvzwNT8Q~qyiv4wE(mTe;t=S;H4_t1QTyA?W!5v<>V50+UG$nX$+GyX8npmkC?_kc= za?btUYjC&Z3#5QdGN%p{C#jl;0*njW$v|Rf1iNqJ`Eb0rwWe#7V<8WYEf7iw02~-- zomd}%A%b{Gz&g6{Xqv$q-m3X{7rc2TZ`Zub1$-c6R>ySMyBncn0t@x)UFJ3yLyM9~BsnS((RbAfJ}&~j10pac%EqmCcA3|&xf zO6;e5Np3?qt9B@dsfs!eYJEXD;Js~~k}AL_l(7p}7D{iiKJ6I6`bh8dDsEy+t#V67 zs^QBpK+uKEfD|*yWKN0@Y&G7)ly*6#4%OHwA_&?pvqJsta4>A{iY)Aink+Z}?RR3fkVM4C5J0M{PV3RSj+ zrGab#tuG&f^@4MNm;qapHX;t;-s~v}-RcUg2bcnWjff2Qpt%BPaue5<&>4GfU{U zmhi8q? zcUJ=9C#5?`rOpX}2(y&YP%fRxw$-MQ zAp55HxxRf--J^D4KvH?K?VyW2BxczEpts?T_Yrayf1Sss@`e)*1{9?X?|`-;H+dev9<$z1&U&MF(%A@e0-2$e zbWBe*lplsUaGxM?CX>jAQhZ&F-vkn0P?GpU@1>0G+E0q6z+~w$E)v;jKK>$1dUHAH zE#BfmT7(=>G6pE zT};Vx5~>&l(kyQ@Y!Zn;Db|+TvSFWYn8%qDU9+ZR)1IH{fFrx6mxn~K&PPhWuW)vCB1h@23cU)9s-Euf`^kFE+hsvVnk370tup&Avo_a1*D-)GQnNa z)?x63B@~e~5Z}jcI~FdK8f3thTZWq~3CCXnj^0^v^e*p;0ENAQ57fj)qgDw5YtdI8 zIuzSy5rykA6XG|bPueN^2K0+wfC_ThiHg4xtGv5h<*U5g9|1YqepO-^5ig4Y|0ZL_ zP`U#7*wGFf8pP=jjiPg@o2Z^_OYB_2uaiM`;F~SQ?m-IAlWvJlhj59AojpCjuuw|P z3Cz_1257d$RXe)Z8x)$o?$GnTxqj4DE-8dCC@qpiobgJmFM4pQjUY5S-bXCn!=_rJ z4_z!oMiwqdw#y>qX#6k)_thh@_iMZxtTflRs}O?P?q&Pur0ck0`sxx))(q5=+4pJC zY{TKrlF1SapWMQ}b_Iz*HDDf+SwS6Y2JF!#iD_ocfU8l{{g_wF5zw1!%Ppfo-@dYQ zfBdyz=j%#6e!aKGP-I6jhUPg4Jg=+OHZY6sYXO340I?fp^m%v*c%xuBAOtvy+P>-QK%rJMp+=dLrbW%}yM8##qHHHm)y~e$ z&2qVlT2$M4FJaiee-fM`LEV_1RNI+Oc6E~C)?`h|m^0E(tf zml-m#51S&`NJYip1irtyl+1g*qtX$8iVG+v+_0mZFdnd=mNN5OFz;K-dEe$ehftUa zrQaFWJn#)ciV>~usKdV#=Jg>ccR(p}R?GGP;cnG635KCLv^4uGfVb2| z*LP7E!{YA-2j5e2@ByzTUHYQG(zdq6{t6~v^phear+${Zbe0l1F(!WyRKB;Q@_pX( z30E^lsvaUDw+(5zvUl~q`1>*Z2g>0e^3DtuGm3@Teg$U7J{gTBnZ)oD$&IOuMJD?c z{rUJ7sC~Gk_Jdw@aLOo6`kW`4vb-EMwzxB7*%l9AOmc7-2fs`;oT8s2H9%GSs26RL zNngXgVEMr}!(O126cKwT@ehHAA0Ba$9}zS?dNwmWlbLj2q$hYuV3=<0`>x6A0_~m! z2Jcf;Jfwk1WB)#4iQRKLUbhXP=cW(;5HykNDbI&N)#uqVLMaa^uOC&r9lzO8@>0Z+AB+)u49&dS zmcs@%jJP+BNkj&6u)LS>c!C+qwc`?oGHJgajei0h{p5%Yf69B97zD-+8C=~u*r%3r z-v9|cLWJOnpD(At%Xwm#d?pyR1Kd5S7Mk|hGEa7A>T2`g%%N5-Pq)~tURvEQ&xGq| z?wY%Ecrr+yLf};d&II(r&1AbBAX)|4iF$2Pt!>xU^une1r@{NrjPU-mlCy`c zrVhj7@MwsA^1iyhyEWi_pjC9>5-0lw6?q2@7M)txJb6m)ag zVuXm|KLUY2E(!dJx9U152!l;)0fK!JLr{g?28P-0;7G_FTDu+xbk_3Qk)I9ZJ)#uy zYF3T^6y*P`B>(5$l<+q!d$4KS<5{RwSz+GnoI4?ohBf6;+8H1iSL43`oxd#U{FO*_ ziQPehaa0lf>Otu}X2sExi9j5iiT@h&|E8S(w}Lkq>m%#TJJpa4bpxl7<{{(c@Pthx zoiqsY`@$oOlN}8Umllh((yH;_VTHdhSNI3XzaKwu$Nz}2|5T3sXDQz zw>bTm-WebQzwBWRkLa2_+EIPXA*zoh)j`ovB-VG`sQjR_FQ9_`4LN3!zQ$C6q=83R z*U&)O{*#9fYCe=_#M10zN#$MS*PQ82v_bO>GC!>*k8^xI{t#bJAYX5CFbeberB~6y zk#p?gBMnI}(!&QXpq=-KZukv=>$n59A9UaOB@-e3Qh^0fccjB!;GS+~R{9c_+_r6& zA;F+LMVXkf3pSB;4HC#6MRyVXoCg^l@o1J-?iw1Aw95q%x!gE21rxO86o7yr zZ}GDzr$H(Ik*J17>stE4dsQXRbvu0Cp&dS-JA6HN7_#LB&ZNi1bB^)}ZO;7?-+`Hk zh4th^7zxc=A2TG)!@63@6EJeMc-}BgdsFh>=EibiE<9CAhaJ^i22vT7GMfedyy{5yH zXM17sF_Q~1yPF$KV7ZoW8j8j~F_rTe>p<3QZTLM=C3&%9_a%o!`%-e-l|o~mMnL=$ z6%F76Mz|YZX?z@`c>>^;Hi*nTT?>HYcBVA^rM$ zu(OMBgR3c>fF^i8@iQVUbzyxix!o~$$00GjjJ#C@sTChNWG2Ylxsp#Ns#TWpx`9YD zpB!`ZCl1X&&iT{$!GaU9cvlX+&4GA~Q!1lNN?*cFBsSO6gXDyxc;_LCcah@zsW>0x zy6$ro`1wiNQO5?UtLGWG@xbN#hVL5QY@%VaHfbMp{@wbl#)@K0zC7Ybsl@h5Y9V%_ ztxG-xxa_Mk4o0GTh_Yc=AOe?3d^g3H2TjfIR(c6aC32C^UqAW;Y~W}z>9%$9(6*}F z*4ff~iVrT0e9AKXgp2m*@~m}TSrer0D@4>r$tlt`)g$9;e2uU1HNM8z_!?j1YkZBb z@io52*Z3M=<7<44ukkg$#@F~7U*l_hjj!=FzQ))18eijUeEs*=M*#swC!_#S004rn BC^rBA literal 0 HcmV?d00001 diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py new file mode 100644 index 0000000..c00b3fa --- /dev/null +++ b/bonobo/examples/files/pickle_handlers.py @@ -0,0 +1,58 @@ +import bonobo +from fs.tarfs import TarFS +import os + + +def cleanse_sms(row): + + if row['category'] == 'spam': + row['sms_clean'] = '**MARKED AS SPAM** ' + row['sms'][0:50] + ('...' if len(row['sms']) > 50 else '') + else: + row['sms_clean'] = row['sms'] + + return row['sms_clean'] + + +graph = bonobo.Graph( + bonobo.PickleReader('spam.pkl'), # spam.pkl is within the gzipped tarball + cleanse_sms, + print +) + + +if __name__ == '__main__': + + ''' + This example shows how a different file system service can be injected + into a transformation (as compressing pickled objects often makes sense + anyways). The pickle itself contains a list of lists as follows: + + ``` + [ + ['category', 'sms'], + ['ham', 'Go until jurong point, crazy..'], + ['ham', 'Ok lar... Joking wif u oni...'], + ['spam', 'Free entry in 2 a wkly comp to win...'], + ['ham', 'U dun say so early hor... U c already then say...'], + ['ham', 'Nah I don't think he goes to usf, he lives around here though'], + ['spam', 'FreeMsg Hey there darling it's been 3 week's now...'], + ... + ] + ``` + + where the first column categorizes and sms as "ham" or "spam". The second + column contains the sms itself. + + Data set taken from: + https://www.kaggle.com/uciml/sms-spam-collection-dataset/downloads/sms-spam-collection-dataset.zip + + The transformation (1) reads the pickled data, (2) marks and shortens + messages categorized as spam, and (3) prints the output. + ''' + + services = { + 'fs': TarFS( + os.path.join(bonobo.get_examples_path(), 'datasets', 'spam.tgz') + ) + } + bonobo.run(graph, services=services) From 8abd40cd736ce8559f9269279d494c39cf1a3a20 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 10:21:21 +0200 Subject: [PATCH 043/143] Cosmetics in docs. --- README.rst | 4 ++-- docs/roadmap.rst | 31 +++++++++++-------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 65ee30f..59b1d10 100644 --- a/README.rst +++ b/README.rst @@ -51,12 +51,12 @@ so as though it may not yet be complete or fully stable (please, allow us to rea ---- +Homepage: https://www.bonobo-project.org/ (`Roadmap `_) + Documentation: http://docs.bonobo-project.org/ Issues: https://github.com/python-bonobo/bonobo/issues -Roadmap: https://www.bonobo-project.org/roadmap - Slack: https://bonobo-slack.herokuapp.com/ Release announcements: http://eepurl.com/csHFKL diff --git a/docs/roadmap.rst b/docs/roadmap.rst index 4bfcc91..182cf71 100644 --- a/docs/roadmap.rst +++ b/docs/roadmap.rst @@ -1,12 +1,12 @@ -Detailed roadmap -================ +Internal roadmap notes +====================== -initialize / finalize better than start / stop ? +Things that should be thought about and/or implemented, but that I don't know where to store. Graph and node level plugins :::::::::::::::::::::::::::: - * Enhancers or nide-level plugins + * Enhancers or node-level plugins * Graph level plugins * Documentation @@ -15,21 +15,19 @@ Command line interface and environment * How do we manage environment ? .env ? * How do we configure plugins ? -* Console run should allow console plugin as a command line argument (or silence it). Services and Processors ::::::::::::::::::::::: -* ContextProcessors not clean +* ContextProcessors not clean (a bit better, but still not in love with the api) Next... ::::::: * Release process specialised for bonobo. With changelog production, etc. * Document how to upgrade version, like, minor need change badges, etc. -* PyPI page looks like crap: https://pypi.python.org/pypi/bonobo/0.2.1 -* Windows break because of readme encoding. Fix in edgy. -* bonobo init --with sqlalchemy,docker +* Windows console looks crappy. +* bonobo init --with sqlalchemy,docker; cookiecutter? * logger, vebosity level @@ -39,22 +37,15 @@ External libs that looks good * dask.distributed * mediator (event dispatcher) -Version 0.3 +Version 0.4 ::::::::::: -* Services ! * SQLAlchemy 101 -Version 0.2 -::::::::::: +Design decisions +:::::::::::::::: -* Autodetect if within jupyter notebook context, and apply plugin if it's the case. -* New bonobo.structs package with simple data structures (bags, graphs, tokens). - -Plugins API -::::::::::: - -* Stabilize, find other things to do. +* initialize / finalize better than start / stop ? Minor stuff ::::::::::: From 71c47a762b3c3768b77b4c8806df72082980467a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 11:14:49 +0200 Subject: [PATCH 044/143] [config] Implements "Exclusive" context processor allowing to ask for an exclusive usage of a service while in a transformation. --- bonobo/_api.py | 2 - bonobo/config/__init__.py | 6 +- bonobo/config/services.py | 39 ++++++++ config/__init__.py | 0 tests/__init__.py | 0 .../test_configurables.py} | 57 ----------- .../test_methods.py} | 0 .../test_processors.py} | 0 tests/config/test_services.py | 96 +++++++++++++++++++ 9 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 config/__init__.py create mode 100644 tests/__init__.py rename tests/{test_config.py => config/test_configurables.py} (61%) rename tests/{test_config_method.py => config/test_methods.py} (100%) rename tests/{test_config_processors.py => config/test_processors.py} (100%) create mode 100644 tests/config/test_services.py diff --git a/bonobo/_api.py b/bonobo/_api.py index 41e9623..5554fef 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,5 +1,3 @@ -import warnings - from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PrettyPrint, Tee, count, identity, noop, pprint diff --git a/bonobo/config/__init__.py b/bonobo/config/__init__.py index 9fc9971..8e662c4 100644 --- a/bonobo/config/__init__.py +++ b/bonobo/config/__init__.py @@ -1,13 +1,15 @@ from bonobo.config.configurables import Configurable from bonobo.config.options import Option, Method from bonobo.config.processors import ContextProcessor -from bonobo.config.services import Container, Service +from bonobo.config.services import Container, Service, Exclusive +# bonobo.config public programming interface __all__ = [ 'Configurable', 'Container', 'ContextProcessor', - 'Option', + 'Exclusive', 'Method', + 'Option', 'Service', ] diff --git a/bonobo/config/services.py b/bonobo/config/services.py index 30aca65..4a91668 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -1,5 +1,7 @@ import re +import threading import types +from contextlib import ContextDecorator from bonobo.config.options import Option from bonobo.errors import MissingServiceImplementationError @@ -87,3 +89,40 @@ class Container(dict): if isinstance(value, types.LambdaType): value = value(self) return value + + +class Exclusive(ContextDecorator): + """ + Decorator and context manager used to require exclusive usage of an object, most probably a service. It's usefull + for example if call order matters on a service implementation (think of an http api that requires a nonce or version + parameter ...). + + Usage: + + >>> def handler(some_service): + ... with Exclusive(some_service): + ... some_service.call_1() + ... some_service.call_2() + ... some_service.call_3() + + This will ensure that nobody else is using the same service while in the "with" block, using a lock primitive to + ensure that. + + """ + _locks = {} + + def __init__(self, wrapped): + self._wrapped = wrapped + + def get_lock(self): + _id = id(self._wrapped) + if not _id in Exclusive._locks: + Exclusive._locks[_id] = threading.RLock() + return Exclusive._locks[_id] + + def __enter__(self): + self.get_lock().acquire() + return self._wrapped + + def __exit__(self, *exc): + self.get_lock().release() diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/config/test_configurables.py similarity index 61% rename from tests/test_config.py rename to tests/config/test_configurables.py index 3f17a53..178c188 100644 --- a/tests/test_config.py +++ b/tests/config/test_configurables.py @@ -2,7 +2,6 @@ import pytest from bonobo.config.configurables import Configurable from bonobo.config.options import Option -from bonobo.config.services import Container, Service, validate_service_name class MyConfigurable(Configurable): @@ -25,28 +24,6 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable): third = Option(str, required=False, positional=True) -class PrinterInterface(): - def print(self, *args): - raise NotImplementedError() - - -class ConcretePrinter(PrinterInterface): - def __init__(self, prefix): - self.prefix = prefix - - def print(self, *args): - return ';'.join((self.prefix, *args)) - - -class MyServiceDependantConfigurable(Configurable): - printer = Service( - PrinterInterface, - ) - - def __call__(self, printer: PrinterInterface, *args): - return printer.print(*args) - - def test_missing_required_option_error(): with pytest.raises(TypeError) as exc: MyConfigurable() @@ -107,39 +84,5 @@ def test_option_resolution_order(): assert o.integer == None -def test_service_name_validator(): - assert validate_service_name('foo') == 'foo' - assert validate_service_name('foo.bar') == 'foo.bar' - assert validate_service_name('Foo') == 'Foo' - assert validate_service_name('Foo.Bar') == 'Foo.Bar' - assert validate_service_name('Foo.a0') == 'Foo.a0' - - with pytest.raises(ValueError): - validate_service_name('foo.0') - - with pytest.raises(ValueError): - validate_service_name('0.foo') - - -SERVICES = Container( - printer0=ConcretePrinter(prefix='0'), - printer1=ConcretePrinter(prefix='1'), -) - - -def test_service_dependency(): - o = MyServiceDependantConfigurable(printer='printer0') - - assert o(SERVICES.get('printer0'), 'foo', 'bar') == '0;foo;bar' - assert o(SERVICES.get('printer1'), 'bar', 'baz') == '1;bar;baz' - assert o(*SERVICES.args_for(o), 'foo', 'bar') == '0;foo;bar' - - -def test_service_dependency_unavailable(): - o = MyServiceDependantConfigurable(printer='printer2') - with pytest.raises(KeyError): - SERVICES.args_for(o) - - def test_option_positional(): o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello') diff --git a/tests/test_config_method.py b/tests/config/test_methods.py similarity index 100% rename from tests/test_config_method.py rename to tests/config/test_methods.py diff --git a/tests/test_config_processors.py b/tests/config/test_processors.py similarity index 100% rename from tests/test_config_processors.py rename to tests/config/test_processors.py diff --git a/tests/config/test_services.py b/tests/config/test_services.py new file mode 100644 index 0000000..b762dbe --- /dev/null +++ b/tests/config/test_services.py @@ -0,0 +1,96 @@ +import threading +import time + +import pytest + +from bonobo.config import Configurable, Container, Exclusive, Service +from bonobo.config.services import validate_service_name + + +class PrinterInterface(): + def print(self, *args): + raise NotImplementedError() + + +class ConcretePrinter(PrinterInterface): + def __init__(self, prefix): + self.prefix = prefix + + def print(self, *args): + return ';'.join((self.prefix, *args)) + + +SERVICES = Container( + printer0=ConcretePrinter(prefix='0'), + printer1=ConcretePrinter(prefix='1'), +) + + +class MyServiceDependantConfigurable(Configurable): + printer = Service( + PrinterInterface, + ) + + def __call__(self, printer: PrinterInterface, *args): + return printer.print(*args) + + +def test_service_name_validator(): + assert validate_service_name('foo') == 'foo' + assert validate_service_name('foo.bar') == 'foo.bar' + assert validate_service_name('Foo') == 'Foo' + assert validate_service_name('Foo.Bar') == 'Foo.Bar' + assert validate_service_name('Foo.a0') == 'Foo.a0' + + with pytest.raises(ValueError): + validate_service_name('foo.0') + + with pytest.raises(ValueError): + validate_service_name('0.foo') + + +def test_service_dependency(): + o = MyServiceDependantConfigurable(printer='printer0') + + assert o(SERVICES.get('printer0'), 'foo', 'bar') == '0;foo;bar' + assert o(SERVICES.get('printer1'), 'bar', 'baz') == '1;bar;baz' + assert o(*SERVICES.args_for(o), 'foo', 'bar') == '0;foo;bar' + + +def test_service_dependency_unavailable(): + o = MyServiceDependantConfigurable(printer='printer2') + with pytest.raises(KeyError): + SERVICES.args_for(o) + + +class VCR: + def __init__(self): + self.tape = [] + + def append(self, x): + return self.tape.append(x) + + +def test_exclusive(): + vcr = VCR() + vcr.append('hello') + + def record(prefix, vcr=vcr): + with Exclusive(vcr): + for i in range(5): + vcr.append(' '.join((prefix, str(i)))) + time.sleep(0.05) + + threads = [threading.Thread(target=record, args=(str(i), )) for i in range(5)] + + for thread in threads: + thread.start() + time.sleep(0.01) # this is not good practice, how to test this without sleeping ?? XXX + + for thread in threads: + thread.join() + + assert vcr.tape == [ + 'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3', + '2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4' + ] From a377639f9459fcd50d33a1ea33cdbc14cbec8935 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 11:19:56 +0200 Subject: [PATCH 045/143] [config] adds documentation for Exclusive contextmanager --- docs/guide/services.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/guide/services.rst b/docs/guide/services.rst index 0b12d96..cf7ecc7 100644 --- a/docs/guide/services.rst +++ b/docs/guide/services.rst @@ -81,6 +81,26 @@ A dictionary, or dictionary-like, "services" named argument can be passed to the provided is pretty basic, and feature-less. But you can use much more evolved libraries instead of the provided stub, and as long as it works the same (a.k.a implements a dictionary-like interface), the system will use it. +Solving concurrency problems +---------------------------- + +If a service cannot be used by more than one thread at a time, either because it's just not threadsafe, or because +it requires to carefully order the calls made (apis that includes nonces, or work on results returned by previous +calls are usually good candidates), you can use the :class:`bonobo.config.Exclusive` context processor to lock the +use of a dependency for a time period. + +.. code-block:: python + + from bonobo.config import Exclusive + + def t1(api): + with Exclusive(api): + api.first_call() + api.second_call() + # ... etc + api.last_call() + + Service configuration (to be decided and implemented) ::::::::::::::::::::::::::::::::::::::::::::::::::::: From 33498b231116a6041209ed6446d93c36daa7c64d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 12:41:36 +0200 Subject: [PATCH 046/143] [fs] adds support for ~ in open_fs(...) implementation (using os.path.expanduser). --- bonobo/_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 5554fef..ca5f363 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -83,7 +83,9 @@ def open_fs(fs_url, *args, **kwargs): :returns: :class:`~fs.base.FS` object """ from fs import open_fs as _open_fs - return _open_fs(str(fs_url), *args, **kwargs) + from os.path import expanduser + + return _open_fs(expanduser(str(fs_url)), *args, **kwargs) # bonobo.nodes From 046b65aa2fe9fb68de3f320fd10dcc81115a3f2d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 12:42:10 +0200 Subject: [PATCH 047/143] [nodes/io] adds support for encoding kwarg to all file readers/writers (tests needed!). --- bonobo/nodes/io/file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index f3cb1b0..b06fae3 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -18,6 +18,7 @@ class FileHandler(Configurable): path = Option(str, required=True, positional=True) # type: str eol = Option(str, default='\n') # type: str mode = Option(str) # type: str + encoding = Option(str, default='utf-8') # type: str fs = Service('fs') # type: str @@ -27,7 +28,7 @@ class FileHandler(Configurable): yield file def open(self, fs): - return fs.open(self.path, self.mode) + return fs.open(self.path, self.mode, encoding=self.encoding) class Reader(FileHandler): From 1c500b31cf9ef4324840ea7eab03d5a971c74799 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 14:44:46 +0200 Subject: [PATCH 048/143] [misc] Fix missing import in bonobo.config.configurables. --- bonobo/config/configurables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index aef371b..43cb8c2 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -1,6 +1,6 @@ from bonobo.config.options import Method, Option from bonobo.config.processors import ContextProcessor -from bonobo.errors import ConfigurationError +from bonobo.errors import ConfigurationError, AbstractError __all__ = [ 'Configurable', From 9375a5d1fbf3b5bf28618742d234048bd0d58107 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 15:14:51 +0200 Subject: [PATCH 049/143] [nodes/io] Adds an output_format option to CsvReader (BC ok) for more flexibility. --- bonobo/nodes/io/csv.py | 28 +++++++++++++++++++++++++++- bonobo/structs/bags.py | 8 ++++++++ tests/io/test_csv.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index 45b40de..e0412fa 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -3,6 +3,8 @@ import csv from bonobo.config import Option from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED +from bonobo.errors import ConfigurationError, ValidationError +from bonobo.structs import Bag from bonobo.util.objects import ValueHolder from .file import FileHandler, FileReader, FileWriter @@ -28,6 +30,14 @@ class CsvHandler(FileHandler): headers = Option(tuple) +def validate_csv_output_format(v): + if callable(v): + return v + if v in {'dict', 'kwargs'}: + return v + raise ValidationError('Unsupported format {!r}.'.format(v)) + + class CsvReader(CsvHandler, FileReader): """ Reads a CSV and yield the values as dicts. @@ -39,13 +49,23 @@ class CsvReader(CsvHandler, FileReader): """ skip = Option(int, default=0) + output_format = Option(validate_csv_output_format, default='dict') @ContextProcessor def csv_headers(self, context, fs, file): yield ValueHolder(self.headers) + def get_output_formater(self): + if callable(self.output_format): + return self.output_format + elif isinstance(self.output_format, str): + return getattr(self, '_format_as_' + self.output_format) + else: + raise ConfigurationError('Unsupported format {!r} for {}.'.format(self.output_format, type(self).__name__)) + def read(self, fs, file, headers): reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar) + formater = self.get_output_formater() if not headers.get(): headers.set(next(reader)) @@ -60,7 +80,13 @@ class CsvReader(CsvHandler, FileReader): if len(row) != field_count: raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) - yield dict(zip(headers.value, row)) + yield formater(headers.get(), row) + + def _format_as_dict(self, headers, values): + return dict(zip(headers, values)) + + def _format_as_kwargs(self, headers, values): + return Bag(**dict(zip(headers, values))) class CsvWriter(CsvHandler, FileWriter): diff --git a/bonobo/structs/bags.py b/bonobo/structs/bags.py index 5fec1f2..4ef2fa7 100644 --- a/bonobo/structs/bags.py +++ b/bonobo/structs/bags.py @@ -75,6 +75,14 @@ class Bag: raise TypeError('Could not apply bag to {}.'.format(func_or_iter)) + def get(self): + """ + Get a 2 element tuple of this bag's args and kwargs. + + :return: tuple + """ + return self.args, self.kwargs + def extend(self, *args, **kwargs): return type(self)(*args, _parent=self, **kwargs) diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 59f7197..bded111 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -55,3 +55,38 @@ def test_read_csv_from_file(tmpdir): 'b': 'b bar', 'c': 'c bar', } + + +def test_read_csv_kwargs_output_formater(tmpdir): + fs, filename = open_fs(tmpdir), 'input.csv' + fs.open(filename, 'w').write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') + + reader = CsvReader(path=filename, delimiter=',', output_format='kwargs') + + context = CapturingNodeExecutionContext(reader, services={'fs': fs}) + + context.start() + context.write(BEGIN, Bag(), END) + context.step() + context.stop() + + assert len(context.send.mock_calls) == 2 + + args0, kwargs0 = context.send.call_args_list[0] + assert len(args0) == 1 and not len(kwargs0) + args1, kwargs1 = context.send.call_args_list[1] + assert len(args1) == 1 and not len(kwargs1) + + _args, _kwargs = args0[0].get() + assert not len(_args) and _kwargs == { + 'a': 'a foo', + 'b': 'b foo', + 'c': 'c foo', + } + + _args, _kwargs = args1[0].get() + assert not len(_args) and _kwargs == { + 'a': 'a bar', + 'b': 'b bar', + 'c': 'c bar', + } From 83f0b67b94311054c55204e4f23faefdc8cf1df0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 15:28:24 +0200 Subject: [PATCH 050/143] [misc] Change some names to be consistent with classtree. --- bonobo/nodes/io/pickle.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index 032b036..cf6b5eb 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -27,10 +27,10 @@ class PickleReader(PickleHandler, FileReader): mode = Option(str, default='rb') @ContextProcessor - def pickle_items(self, context, fs, file): + def pickle_headers(self, context, fs, file): yield ValueHolder(self.item_names) - def read(self, fs, file, item_names): + def read(self, fs, file, pickle_headers): data = pickle.load(file) # if the data is not iterable, then wrap the object in a list so it may be iterated @@ -44,26 +44,26 @@ class PickleReader(PickleHandler, FileReader): except TypeError: iterator = iter([data]) - if not item_names.get(): - item_names.set(next(iterator)) + if not pickle_headers.get(): + pickle_headers.set(next(iterator)) - item_count = len(item_names.value) + item_count = len(pickle_headers.value) for i in iterator: if len(i) != item_count: raise ValueError('Received an object with %d items, expecting %d.' % (len(i), item_count, )) - yield dict(zip(i)) if is_dict else dict(zip(item_names.value, i)) + yield dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i)) class PickleWriter(PickleHandler, FileWriter): mode = Option(str, default='wb') - def write(self, fs, file, itemno, item): + def write(self, fs, file, lineno, item): """ Write a pickled item to the opened file. """ file.write(pickle.dumps(item)) - itemno += 1 + lineno += 1 return NOT_MODIFIED From d489eb2c4ced23a4b0bf8f0d5d08e54edea7fb1c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 15:44:18 +0200 Subject: [PATCH 051/143] [docs] tldr on how to create a feature branch and pull request it. --- docs/contribute/index.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst index 3c3f6e9..690cab7 100644 --- a/docs/contribute/index.rst +++ b/docs/contribute/index.rst @@ -13,6 +13,26 @@ contributions have less value, all contributions are very important. * You can enhance tests. * etc. +tl;dr +::::: + +1. Fork the github repository + +.. code-block:: shell-session + + $ git clone https://github.com/python-bonobo/bonobo.git # change this to use your fork. + $ cd bonobo + $ git remote add upstream https://github.com/python-bonobo/bonobo.git + $ git fetch upstream + $ git checkout upstream/develop -b feature/my_awesome_feature + $ # code, code, code, test, doc, code, test ... + $ git commit -m '[topic] .... blaaaah ....' + $ git push origin feature/my_awesome_feature + +2. Open pull request +3. Rince, repeat + + Code-related contributions (including tests and examples) ::::::::::::::::::::::::::::::::::::::::::::::::::::::::: From 34aa357fd32fae66662d4d0b5a42186ebacf2073 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 25 May 2017 19:49:05 +0200 Subject: [PATCH 052/143] [misc] minor output tuning. --- bonobo/ext/console.py | 15 ++++++++++----- bonobo/nodes/basics.py | 11 ++++++++++- bonobo/settings.py | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index e15453b..d454bcd 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -21,16 +21,22 @@ class ConsoleOutputPlugin(Plugin): def initialize(self): self.prefix = '' + self.counter = 0 + self._append_cache = '' def _write(self, graph_context, rewind): if settings.PROFILE: - append = ( - ('Memory', '{0:.2f} Mb'.format(memory_usage())), - # ('Total time', '{0} s'.format(execution_time(harness))), - ) + if self.counter % 10 and self._append_cache: + append = self._append_cache + else: + self._append_cache = append = ( + ('Memory', '{0:.2f} Mb'.format(memory_usage())), + # ('Total time', '{0} s'.format(execution_time(harness))), + ) else: append = () self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind) + self.counter += 1 def run(self): if sys.stdout.isatty(): @@ -81,7 +87,6 @@ class ConsoleOutputPlugin(Plugin): print(MOVE_CURSOR_UP(t_cnt + 2)) -@functools.lru_cache(1) def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 5ce550c..bbf6ab5 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -4,6 +4,7 @@ from pprint import pprint as _pprint import itertools from colorama import Fore, Style +from bonobo import settings from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor from bonobo.structs.bags import Bag @@ -72,8 +73,16 @@ def _count_counter(self, context): class PrettyPrinter(Configurable): def call(self, *args, **kwargs): + formater = self._format_quiet if settings.QUIET else self._format_console + for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): - print(' ' if i else '•', item, '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL) + print(formater(i, item, value)) + + def _format_quiet(self, i, item, value): + return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip())) + + def _format_console(self, i, item, value): + return ' '.join(((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL)) pprint = Tee(_pprint) diff --git a/bonobo/settings.py b/bonobo/settings.py index 9f0fbf6..d956b2c 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -21,4 +21,4 @@ QUIET = to_bool(os.environ.get('BONOBO_QUIET', 'f')) def check(): if DEBUG and QUIET: - raise RuntimeError('I cannot be verbose and quiet at the same time.') \ No newline at end of file + raise RuntimeError('I cannot be verbose and quiet at the same time.') From eacf52aaf6d0ab8e45a7e40ed265c5afa64fc712 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 14:55:25 +0200 Subject: [PATCH 053/143] Simpler package generation using cookiecutter, stdout buffering for consoleplugin --- Projectfile | 18 +++----- bonobo/_api.py | 5 +-- bonobo/commands/init.py | 9 ++-- bonobo/commands/version.py | 26 ++++++++++- bonobo/examples/nodes/slow.py | 17 +++++++ bonobo/ext/console.py | 70 ++++++++++++++++++++--------- bonobo/ext/edgy/__init__.py | 0 bonobo/ext/edgy/project/__init__.py | 0 bonobo/ext/edgy/project/feature.py | 22 --------- bonobo/nodes/basics.py | 5 ++- bonobo/util/packages.py | 8 ++++ 11 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 bonobo/examples/nodes/slow.py delete mode 100644 bonobo/ext/edgy/__init__.py delete mode 100644 bonobo/ext/edgy/project/__init__.py delete mode 100644 bonobo/ext/edgy/project/feature.py create mode 100644 bonobo/util/packages.py diff --git a/Projectfile b/Projectfile index 47844ee..d8fcf44 100644 --- a/Projectfile +++ b/Projectfile @@ -12,12 +12,12 @@ author_email = 'romain@dorgueil.net' enable_features = { 'make', - 'sphinx', - 'pytest', + 'sphinx', # should install sphinx + 'pytest', # should install pytest/pytest-cov/coverage 'git', 'pylint', 'python', - 'yapf', + 'yapf', # should install yapf } # stricts deendencies in requirements.txt @@ -30,10 +30,6 @@ install_requires = [ ] extras_require = { - 'jupyter': [ - 'jupyter >=1.0,<1.1', - 'ipywidgets >=6.0.0.beta5' - ], 'dev': [ 'coverage >=4,<5', 'pylint >=1,<2', @@ -41,9 +37,12 @@ extras_require = { 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', - 'sphinx_rtd_theme', 'yapf', ], + 'jupyter': [ + 'jupyter >=1.0,<1.1', + 'ipywidgets >=6.0.0.beta5' + ], } data_files = [ @@ -63,9 +62,6 @@ entry_points = { 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register', ], - 'edgy.project.features': [ - 'bonobo = bonobo.ext.edgy.project.feature:BonoboFeature' - ] } @listen('edgy.project.feature.make.on_generate', priority=10) diff --git a/bonobo/_api.py b/bonobo/_api.py index ca5f363..049eba4 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,12 +1,11 @@ from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrint, Tee, count, identity, noop, pprint + PrettyPrinter, Tee, count, identity, noop, pprint from bonobo.strategies import create_strategy from bonobo.util.objects import get_name __all__ = [] - def register_api(x, __all__=__all__): __all__.append(get_name(x)) return x @@ -98,7 +97,7 @@ register_api_group( JsonReader, JsonWriter, Limit, - PrettyPrint, + PrettyPrinter, Tee, count, identity, diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index 55c4b6b..d13a21f 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,16 +1,17 @@ import os -def execute(): +def execute(name): try: - from edgy.project.__main__ import handle_init + from cookiecutter.main import cookiecutter except ImportError as exc: raise ImportError( - 'You must install "edgy.project" to use this command.\n\n $ pip install edgy.project\n' + 'You must install "cookiecutter" to use this command.\n\n $ pip install edgy.project\n' ) from exc - return handle_init(os.path.join(os.getcwd(), 'Projectfile')) + return cookiecutter('https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True) def register(parser): + parser.add_argument('name') return execute diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index 332286a..c6ac9e8 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -1,9 +1,31 @@ import bonobo +from bonobo.util.packages import bonobo_packages -def execute(): - print('{} v.{}'.format(bonobo.__name__, bonobo.__version__)) +def format_version(mod, *, name=None, quiet=False): + return ('{name} {version}' if quiet else '{name} v.{version} (in {location})').format( + name=name or mod.__name__, + version=mod.__version__, + location=bonobo_packages[name or mod.__name__].location + ) + + +def execute(all=False, quiet=False): + print(format_version(bonobo, quiet=quiet)) + if all: + for name in sorted(bonobo_packages): + if name != 'bonobo': + try: + mod = __import__(name.replace('-', '_')) + try: + print(format_version(mod, name=name, quiet=quiet)) + except Exception as exc: + print('{} ({})'.format(name, exc)) + except ImportError as exc: + print('{} is not importable ({}).'.format(name, exc)) def register(parser): + parser.add_argument('--all', '-a', action='store_true') + parser.add_argument('--quiet', '-q', action='store_true') return execute diff --git a/bonobo/examples/nodes/slow.py b/bonobo/examples/nodes/slow.py new file mode 100644 index 0000000..703ebc2 --- /dev/null +++ b/bonobo/examples/nodes/slow.py @@ -0,0 +1,17 @@ +import bonobo +import time + + +from bonobo.constants import NOT_MODIFIED + + +def pause(*args, **kwargs): + time.sleep(0.1) + return NOT_MODIFIED + + +graph = bonobo.Graph( + lambda: tuple(range(20)), + pause, + print, +) diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index d454bcd..6a3ef8e 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -1,5 +1,6 @@ -import functools +import io import sys +from contextlib import redirect_stdout from colorama import Style, Fore @@ -8,6 +9,21 @@ from bonobo.plugins import Plugin from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP +class IOBuffer(): + def __init__(self): + self.current = io.StringIO() + self.write = self.current.write + + def switch(self): + previous = self.current + self.current = io.StringIO() + self.write = self.current.write + try: + return previous.getvalue() + finally: + previous.close() + + class ConsoleOutputPlugin(Plugin): """ Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor @@ -23,34 +39,30 @@ class ConsoleOutputPlugin(Plugin): self.prefix = '' self.counter = 0 self._append_cache = '' + self.isatty = sys.stdout.isatty() - def _write(self, graph_context, rewind): - if settings.PROFILE: - if self.counter % 10 and self._append_cache: - append = self._append_cache - else: - self._append_cache = append = ( - ('Memory', '{0:.2f} Mb'.format(memory_usage())), - # ('Total time', '{0} s'.format(execution_time(harness))), - ) - else: - append = () - self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind) - self.counter += 1 + self._stdout = sys.stdout + self.stdout = IOBuffer() + self.redirect_stdout = redirect_stdout(self.stdout) + self.redirect_stdout.__enter__() def run(self): - if sys.stdout.isatty(): + if self.isatty: self._write(self.context.parent, rewind=True) else: pass # not a tty def finalize(self): self._write(self.context.parent, rewind=False) + self.redirect_stdout.__exit__(None, None, None) - @staticmethod - def write(context, prefix='', rewind=True, append=None): + def write(self, context, prefix='', rewind=True, append=None): t_cnt = len(context) + buffered = self.stdout.switch() + for line in buffered.split('\n')[:-1]: + print(line + CLEAR_EOL, file=sys.stderr) + for i in context.graph.topologically_sorted_indexes: node = context[i] name_suffix = '({})'.format(i) if settings.DEBUG else '' @@ -68,7 +80,7 @@ class ConsoleOutputPlugin(Plugin): Style.RESET_ALL, ' ', ) ) - print(prefix + _line + '\033[0K') + print(prefix + _line + '\033[0K', file=sys.stderr) if append: # todo handle multiline @@ -78,16 +90,30 @@ class ConsoleOutputPlugin(Plugin): ' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append), CLEAR_EOL ) - ) + ), file=sys.stderr ) t_cnt += 1 if rewind: - print(CLEAR_EOL) - print(MOVE_CURSOR_UP(t_cnt + 2)) + print(CLEAR_EOL, file=sys.stderr) + print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr) + + def _write(self, graph_context, rewind): + if settings.PROFILE: + if self.counter % 10 and self._append_cache: + append = self._append_cache + else: + self._append_cache = append = ( + ('Memory', '{0:.2f} Mb'.format(memory_usage())), + # ('Total time', '{0} s'.format(execution_time(harness))), + ) + else: + append = () + self.write(graph_context, prefix=self.prefix, append=append, rewind=rewind) + self.counter += 1 def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2**20) \ No newline at end of file + return process.memory_info()[0] / float(2 ** 20) diff --git a/bonobo/ext/edgy/__init__.py b/bonobo/ext/edgy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/ext/edgy/project/__init__.py b/bonobo/ext/edgy/project/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bonobo/ext/edgy/project/feature.py b/bonobo/ext/edgy/project/feature.py deleted file mode 100644 index 5a01f97..0000000 --- a/bonobo/ext/edgy/project/feature.py +++ /dev/null @@ -1,22 +0,0 @@ -try: - import edgy.project -except ImportError as e: - import logging - - logging.exception('You must install edgy.project to use this.') - -import os - -from edgy.project.events import subscribe -from edgy.project.feature import Feature, SUPPORT_PRIORITY - - -class BonoboFeature(Feature): - requires = {'python'} - - @subscribe('edgy.project.on_start', priority=SUPPORT_PRIORITY) - def on_start(self, event): - package_path = event.setup['name'].replace('.', os.sep) - - for file in ('example_graph'): - self.render_file(os.path.join(package_path, file + '.py'), os.path.join('tornado', file + '.py.j2')) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index bbf6ab5..1242cb2 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -18,7 +18,7 @@ __all__ = [ 'Tee', 'count', 'pprint', - 'PrettyPrint', + 'PrettyPrinter', 'noop', ] @@ -85,7 +85,8 @@ class PrettyPrinter(Configurable): return ' '.join(((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL)) -pprint = Tee(_pprint) +pprint = PrettyPrinter() +pprint.__name__ = 'pprint' def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True): diff --git a/bonobo/util/packages.py b/bonobo/util/packages.py new file mode 100644 index 0000000..e4de1b6 --- /dev/null +++ b/bonobo/util/packages.py @@ -0,0 +1,8 @@ +import pkg_resources +from packaging.utils import canonicalize_name + +bonobo_packages = {} +for p in pkg_resources.working_set: + name = canonicalize_name(p.project_name) + if name.startswith('bonobo'): + bonobo_packages[name] = p From aea5ecf17e45a2008d9168426ad37ab020af2e5e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 15:17:55 +0200 Subject: [PATCH 054/143] [cli] Adds --install/-I flag to install requirements.txt prior to run when targetting a directory, needed for docker runs. --- bonobo/commands/run.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 25fe956..0f85eb8 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,6 +1,8 @@ import os import runpy +import pip + import bonobo DEFAULT_SERVICES_FILENAME = '_services.py' @@ -29,7 +31,7 @@ def get_default_services(filename, services=None): return services or {} -def execute(filename, module, quiet=False, verbose=False): +def execute(filename, module, install=False, quiet=False, verbose=False): from bonobo import settings if quiet: @@ -40,7 +42,12 @@ def execute(filename, module, quiet=False, verbose=False): if filename: if os.path.isdir(filename): + if install: + requirements = os.path.join(filename, 'requirements.txt') + pip.main(['install', '-qr', requirements]) filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME) + elif install: + raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).') context = runpy.run_path(filename, run_name='__bonobo__') elif module: context = runpy.run_module(module, run_name='__bonobo__') @@ -68,11 +75,17 @@ def execute(filename, module, quiet=False, verbose=False): ) -def register(parser): +def register_generic_run_arguments(parser): source_group = parser.add_mutually_exclusive_group(required=True) source_group.add_argument('filename', nargs='?', type=str) source_group.add_argument('--module', '-m', type=str) + return parser + + +def register(parser): + parser = register_generic_run_arguments(parser) verbosity_group = parser.add_mutually_exclusive_group() verbosity_group.add_argument('--quiet', '-q', action='store_true') verbosity_group.add_argument('--verbose', '-v', action='store_true') + parser.add_argument('--install', '-I', action='store_true') return execute From 7e28eeddd6009e4450f0daa668b5e385a8c2d25b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 15:25:22 +0200 Subject: [PATCH 055/143] Rename packages package as it looks like it cause trouble when uploading to pypi. --- bonobo/commands/version.py | 2 +- bonobo/util/{packages.py => pkgs.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bonobo/util/{packages.py => pkgs.py} (100%) diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index c6ac9e8..ceeec3b 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -1,5 +1,5 @@ import bonobo -from bonobo.util.packages import bonobo_packages +from bonobo.util.pkgs import bonobo_packages def format_version(mod, *, name=None, quiet=False): diff --git a/bonobo/util/packages.py b/bonobo/util/pkgs.py similarity index 100% rename from bonobo/util/packages.py rename to bonobo/util/pkgs.py From 1afd8746eb0d4c3912e9c59f98bc44a94cf2e3b9 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 16:08:10 +0200 Subject: [PATCH 056/143] [pm] Moving project artifact management to next edgy.project version. --- Makefile | 8 +- Projectfile | 93 ++++++++++-------------- bonobo/_api.py | 1 + bonobo/commands/init.py | 4 +- bonobo/commands/version.py | 4 +- bonobo/examples/files/pickle_handlers.py | 15 ++-- bonobo/examples/nodes/slow.py | 1 - bonobo/ext/console.py | 5 +- bonobo/nodes/basics.py | 4 +- requirements-dev.txt | 25 +++---- requirements-jupyter.txt | 5 +- requirements.txt | 9 ++- setup.py | 27 +++---- tests/io/test_pickle.py | 5 +- 14 files changed, 92 insertions(+), 114 deletions(-) diff --git a/Makefile b/Makefile index 3ea8912..022a1ca 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-22 19:54:27.969596 +# Updated at 2017-05-27 16:06:39.394642 PACKAGE ?= bonobo PYTHON ?= $(shell which python) @@ -20,8 +20,9 @@ SPHINX_SOURCEDIR ?= docs SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build YAPF ?= $(PYTHON_DIRNAME)/yapf YAPF_OPTIONS ?= -rip +VERSION ?= $(shell git describe 2>/dev/null || echo dev) -.PHONY: $(SPHINX_SOURCEDIR) clean format install install-dev lint test +.PHONY: $(SPHINX_SOURCEDIR) clean format install install-dev test # Installs the local project dependencies. install: @@ -39,9 +40,6 @@ install-dev: clean: rm -rf build dist *.egg-info -lint: install-dev - $(PYTHON_DIRNAME)/pylint --py3k $(PACKAGE) -f html > pylint.html - test: install-dev $(PYTEST) $(PYTEST_OPTIONS) tests diff --git a/Projectfile b/Projectfile index d8fcf44..a04e297 100644 --- a/Projectfile +++ b/Projectfile @@ -1,69 +1,52 @@ # bonobo (see github.com/python-edgy/project) -name = 'bonobo' -description = 'Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.' -license = 'Apache License, Version 2.0' +from edgy.project import require -url = 'https://www.bonobo-project.org/' -download_url = 'https://github.com/python-bonobo/bonobo/tarball/{version}' +pytest = require('pytest') +python = require('python') +sphinx = require('sphinx') +yapf = require('yapf') -author = 'Romain Dorgueil' -author_email = 'romain@dorgueil.net' +python.setup( + name='bonobo', + description='Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.', + license='Apache License, Version 2.0', + url='https://www.bonobo-project.org/', + download_url='https://github.com/python-bonobo/bonobo/tarball/{version}', + author='Romain Dorgueil', + author_email='romain@dorgueil.net', + data_files=[ + ('share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', + 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map', + ]), + ], -enable_features = { - 'make', - 'sphinx', # should install sphinx - 'pytest', # should install pytest/pytest-cov/coverage - 'git', - 'pylint', - 'python', - 'yapf', # should install yapf -} + entry_points={ + 'console_scripts': [ + 'bonobo = bonobo.commands:entrypoint', + ], + 'bonobo.commands': [ + 'init = bonobo.commands.init:register', + 'run = bonobo.commands.run:register', + 'version = bonobo.commands.version:register', + ], + } -# stricts deendencies in requirements.txt -install_requires = [ +) + +python.add_requirements( 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0', -] - -extras_require = { - 'dev': [ - 'coverage >=4,<5', - 'pylint >=1,<2', - 'pytest >=3,<4', - 'pytest-cov >=2,<3', + dev=[ 'pytest-timeout >=1,<2', - 'sphinx', - 'yapf', ], - 'jupyter': [ + jupyter=[ 'jupyter >=1.0,<1.1', - 'ipywidgets >=6.0.0.beta5' - ], -} - -data_files = [ - ('share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map', - ]), -] - -entry_points = { - 'console_scripts': [ - 'bonobo = bonobo.commands:entrypoint', - ], - 'bonobo.commands': [ - 'init = bonobo.commands.init:register', - 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register', - ], -} - -@listen('edgy.project.feature.make.on_generate', priority=10) -def on_make_generate_docker_targets(event): - event.makefile['SPHINX_SOURCEDIR'] = 'docs' + 'ipywidgets >=6.0.0.beta5', + ] +) diff --git a/bonobo/_api.py b/bonobo/_api.py index 1eed59f..9317e31 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -6,6 +6,7 @@ from bonobo.util.objects import get_name __all__ = [] + def register_api(x, __all__=__all__): __all__.append(get_name(x)) return x diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index d13a21f..ad3c52a 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -9,7 +9,9 @@ def execute(name): 'You must install "cookiecutter" to use this command.\n\n $ pip install edgy.project\n' ) from exc - return cookiecutter('https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True) + return cookiecutter( + 'https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True + ) def register(parser): diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index ceeec3b..fda6c45 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -4,9 +4,7 @@ from bonobo.util.pkgs import bonobo_packages def format_version(mod, *, name=None, quiet=False): return ('{name} {version}' if quiet else '{name} v.{version} (in {location})').format( - name=name or mod.__name__, - version=mod.__version__, - location=bonobo_packages[name or mod.__name__].location + name=name or mod.__name__, version=mod.__version__, location=bonobo_packages[name or mod.__name__].location ) diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py index c00b3fa..e6f3dcc 100644 --- a/bonobo/examples/files/pickle_handlers.py +++ b/bonobo/examples/files/pickle_handlers.py @@ -6,7 +6,9 @@ import os def cleanse_sms(row): if row['category'] == 'spam': - row['sms_clean'] = '**MARKED AS SPAM** ' + row['sms'][0:50] + ('...' if len(row['sms']) > 50 else '') + row['sms_clean'] = '**MARKED AS SPAM** ' + row['sms'][0:50] + ( + '...' if len(row['sms']) > 50 else '' + ) else: row['sms_clean'] = row['sms'] @@ -14,14 +16,13 @@ def cleanse_sms(row): graph = bonobo.Graph( - bonobo.PickleReader('spam.pkl'), # spam.pkl is within the gzipped tarball + bonobo.PickleReader('spam.pkl' + ), # spam.pkl is within the gzipped tarball cleanse_sms, print ) - if __name__ == '__main__': - ''' This example shows how a different file system service can be injected into a transformation (as compressing pickled objects often makes sense @@ -51,8 +52,10 @@ if __name__ == '__main__': ''' services = { - 'fs': TarFS( - os.path.join(bonobo.get_examples_path(), 'datasets', 'spam.tgz') + 'fs': + TarFS( + os.path. + join(bonobo.get_examples_path(), 'datasets', 'spam.tgz') ) } bonobo.run(graph, services=services) diff --git a/bonobo/examples/nodes/slow.py b/bonobo/examples/nodes/slow.py index 703ebc2..b9623af 100644 --- a/bonobo/examples/nodes/slow.py +++ b/bonobo/examples/nodes/slow.py @@ -1,7 +1,6 @@ import bonobo import time - from bonobo.constants import NOT_MODIFIED diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index 6a3ef8e..f30fae0 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -90,7 +90,8 @@ class ConsoleOutputPlugin(Plugin): ' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append), CLEAR_EOL ) - ), file=sys.stderr + ), + file=sys.stderr ) t_cnt += 1 @@ -116,4 +117,4 @@ class ConsoleOutputPlugin(Plugin): def memory_usage(): import os, psutil process = psutil.Process(os.getpid()) - return process.memory_info()[0] / float(2 ** 20) + return process.memory_info()[0] / float(2**20) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 1242cb2..c21757a 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -82,7 +82,9 @@ class PrettyPrinter(Configurable): return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip())) def _format_console(self, i, item, value): - return ' '.join(((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL)) + return ' '.join( + ((' ' if i else '•'), str(item), '=', str(value).strip().replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL) + ) pprint = PrettyPrinter() diff --git a/requirements-dev.txt b/requirements-dev.txt index 1cdb4ad..3e50f5d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,27 +1,24 @@ -e .[dev] - alabaster==0.7.10 -astroid==1.5.2 babel==2.4.0 -coverage==4.3.4 +certifi==2017.4.17 +chardet==3.0.3 +coverage==4.4.1 docutils==0.13.1 +idna==2.5 imagesize==0.7.1 -isort==4.2.5 jinja2==2.9.6 -lazy-object-proxy==1.2.2 markupsafe==1.0 -mccabe==0.6.1 py==1.4.33 pygments==2.2.0 -pylint==1.7.1 -pytest-cov==2.4.0 +pytest-cov==2.5.1 pytest-timeout==1.2.0 -pytest==3.0.7 +pytest==3.1.0 pytz==2017.2 -requests==2.13.0 +requests==2.16.1 six==1.10.0 snowballstemmer==1.2.1 -sphinx-rtd-theme==0.2.4 -sphinx==1.5.5 -wrapt==1.10.10 -yapf==0.16.1 +sphinx==1.6.1 +sphinxcontrib-websupport==1.0.1 +typing==3.6.1 +urllib3==1.21.1 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 1cf03f6..1e98481 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,5 +1,4 @@ -e .[jupyter] - appnope==0.1.0 bleach==2.0.0 decorator==4.0.11 @@ -18,7 +17,7 @@ jupyter-core==4.3.0 jupyter==1.0.0 markupsafe==1.0 mistune==0.7.4 -nbconvert==5.1.1 +nbconvert==5.2.1 nbformat==4.3.0 notebook==5.0.0 pandocfilters==1.4.1 @@ -33,7 +32,7 @@ qtconsole==4.3.0 simplegeneric==0.8.1 six==1.10.0 terminado==0.6 -testpath==0.3 +testpath==0.3.1 tornado==4.5.1 traitlets==4.3.2 wcwidth==0.1.7 diff --git a/requirements.txt b/requirements.txt index b11cfa3..86d900d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,15 @@ -e . - appdirs==1.4.3 +certifi==2017.4.17 +chardet==3.0.3 colorama==0.3.9 enum34==1.1.6 fs==2.0.3 -pbr==3.0.0 +idna==2.5 +pbr==3.0.1 psutil==5.2.2 pytz==2017.2 -requests==2.13.0 +requests==2.16.1 six==1.10.0 stevedore==1.21.0 +urllib3==1.21.1 diff --git a/setup.py b/setup.py index feabb8c..62064d1 100644 --- a/setup.py +++ b/setup.py @@ -41,41 +41,34 @@ else: version = version_ns.get('__version__', 'dev') setup( - name='bonobo', + author='Romain Dorgueil', + author_email='romain@dorgueil.net', description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' 'python 3.5+.'), license='Apache License, Version 2.0', - install_requires=[ - 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' - ], + name='bonobo', version=version, long_description=long_description, classifiers=classifiers, packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, - data_files=[ - ( - 'share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map' - ] - ) + install_requires=[ + 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'psutil (>= 5.2, < 6.0)', 'requests (>= 2.0, < 3.0)', + 'stevedore (>= 1.21, < 2.0)' ], extras_require={ 'dev': [ - 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', - 'sphinx_rtd_theme', 'yapf' + 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', + 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' ], - 'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5'] + 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] }, entry_points={ 'bonobo.commands': [ 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], - 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], - 'edgy.project.features': ['bonobo = ' - 'bonobo.ext.edgy.project.feature:BonoboFeature'] + 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, url='https://www.bonobo-project.org/', download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index a8333ef..662fc4a 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -28,9 +28,8 @@ def test_write_pickled_dict_to_file(tmpdir): def test_read_pickled_list_from_file(tmpdir): fs, filename = open_fs(tmpdir), 'input.pkl' - fs.open(filename, 'wb').write(pickle.dumps([ - ['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar'] - ])) + fs.open(filename, + 'wb').write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) reader = PickleReader(path=filename) From 665ddb560e10e3dddc6ef9aa8933103c1c55207b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 16:10:14 +0200 Subject: [PATCH 057/143] Fix version test --- tests/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 40a6ed5..280308d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -76,4 +76,4 @@ def test_version(runner, capsys): out, err = capsys.readouterr() out = out.strip() assert out.startswith('bonobo ') - assert out.endswith(__version__) + assert __version__ in out From d241fdd708d7974a29683afbd9ac8447c27aba69 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 16:26:14 +0200 Subject: [PATCH 058/143] [cli] bonobo version -qq now returns only version number --- bonobo/commands/version.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index fda6c45..bfa03a7 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -3,9 +3,20 @@ from bonobo.util.pkgs import bonobo_packages def format_version(mod, *, name=None, quiet=False): - return ('{name} {version}' if quiet else '{name} v.{version} (in {location})').format( - name=name or mod.__name__, version=mod.__version__, location=bonobo_packages[name or mod.__name__].location - ) + args = { + 'name': name or mod.__name__, + 'version': mod.__version__, + 'location': bonobo_packages[name or mod.__name__].location + } + + if not quiet: + return '{name} v.{version} (in {location})'.format(**args) + if quiet < 2: + return '{name} {version}'.format(**args) + if quiet < 3: + return '{version}'.format(**args) + + raise RuntimeError('Hard to be so quiet...') def execute(all=False, quiet=False): @@ -25,5 +36,5 @@ def execute(all=False, quiet=False): def register(parser): parser.add_argument('--all', '-a', action='store_true') - parser.add_argument('--quiet', '-q', action='store_true') + parser.add_argument('--quiet', '-q', action='count') return execute From 3a5ebff43532983aa801b377556719745e325872 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 17:06:18 +0200 Subject: [PATCH 059/143] Adds packaging requirement. --- Makefile | 2 +- Projectfile | 1 + requirements-dev.txt | 2 +- requirements.txt | 4 +++- setup.py | 4 ++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 022a1ca..e0ab813 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-27 16:06:39.394642 +# Updated at 2017-05-27 17:05:44.723397 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/Projectfile b/Projectfile index a04e297..94c8a4f 100644 --- a/Projectfile +++ b/Projectfile @@ -39,6 +39,7 @@ python.setup( python.add_requirements( 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', + 'packaging >=16,<17', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0', diff --git a/requirements-dev.txt b/requirements-dev.txt index 3e50f5d..83f50fa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-timeout==1.2.0 pytest==3.1.0 pytz==2017.2 -requests==2.16.1 +requests==2.16.2 six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.1 diff --git a/requirements.txt b/requirements.txt index 86d900d..184e757 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,12 @@ colorama==0.3.9 enum34==1.1.6 fs==2.0.3 idna==2.5 +packaging==16.8 pbr==3.0.1 psutil==5.2.2 +pyparsing==2.2.0 pytz==2017.2 -requests==2.16.1 +requests==2.16.2 six==1.10.0 stevedore==1.21.0 urllib3==1.21.1 diff --git a/setup.py b/setup.py index 62064d1..cc20417 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,8 @@ setup( packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, install_requires=[ - 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'psutil (>= 5.2, < 6.0)', 'requests (>= 2.0, < 3.0)', - 'stevedore (>= 1.21, < 2.0)' + 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'packaging (>= 16, < 17)', 'psutil (>= 5.2, < 6.0)', + 'requests (>= 2.0, < 3.0)', 'stevedore (>= 1.21, < 2.0)' ], extras_require={ 'dev': [ From 9bf30df9755bc7c6f1edd7a81ff3a095f0334d40 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 27 May 2017 19:55:43 +0200 Subject: [PATCH 060/143] Empty commit for CI trigger. From 5013708c85f4dca864d57631356cd05611ba9e47 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 12:04:49 +0200 Subject: [PATCH 061/143] [misc] updating dependencies. --- Makefile | 8 ++-- Projectfile | 97 ++++++++++++++++------------------------ requirements-dev.txt | 25 +++++------ requirements-jupyter.txt | 5 +-- requirements.txt | 9 ++-- setup.py | 41 +++++++++-------- 6 files changed, 80 insertions(+), 105 deletions(-) diff --git a/Makefile b/Makefile index 6db0f7c..75e5700 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-03 18:02:59.359160 +# Updated at 2017-05-28 12:03:49.427061 PACKAGE ?= bonobo PYTHON ?= $(shell which python) @@ -20,8 +20,9 @@ SPHINX_SOURCEDIR ?= docs SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build YAPF ?= $(PYTHON_DIRNAME)/yapf YAPF_OPTIONS ?= -rip +VERSION ?= $(shell git describe 2>/dev/null || echo dev) -.PHONY: $(SPHINX_SOURCEDIR) clean format install install-dev lint test +.PHONY: $(SPHINX_SOURCEDIR) clean format install install-dev test # Installs the local project dependencies. install: @@ -39,9 +40,6 @@ install-dev: clean: rm -rf build dist *.egg-info -lint: install-dev - $(PYTHON_DIRNAME)/pylint --py3k $(PACKAGE) -f html > pylint.html - test: install-dev $(PYTEST) $(PYTEST_OPTIONS) tests diff --git a/Projectfile b/Projectfile index 47844ee..a04e297 100644 --- a/Projectfile +++ b/Projectfile @@ -1,73 +1,52 @@ # bonobo (see github.com/python-edgy/project) -name = 'bonobo' -description = 'Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.' -license = 'Apache License, Version 2.0' +from edgy.project import require -url = 'https://www.bonobo-project.org/' -download_url = 'https://github.com/python-bonobo/bonobo/tarball/{version}' +pytest = require('pytest') +python = require('python') +sphinx = require('sphinx') +yapf = require('yapf') -author = 'Romain Dorgueil' -author_email = 'romain@dorgueil.net' +python.setup( + name='bonobo', + description='Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.', + license='Apache License, Version 2.0', + url='https://www.bonobo-project.org/', + download_url='https://github.com/python-bonobo/bonobo/tarball/{version}', + author='Romain Dorgueil', + author_email='romain@dorgueil.net', + data_files=[ + ('share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', + 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map', + ]), + ], -enable_features = { - 'make', - 'sphinx', - 'pytest', - 'git', - 'pylint', - 'python', - 'yapf', -} + entry_points={ + 'console_scripts': [ + 'bonobo = bonobo.commands:entrypoint', + ], + 'bonobo.commands': [ + 'init = bonobo.commands.init:register', + 'run = bonobo.commands.run:register', + 'version = bonobo.commands.version:register', + ], + } -# stricts deendencies in requirements.txt -install_requires = [ +) + +python.add_requirements( 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0', -] - -extras_require = { - 'jupyter': [ - 'jupyter >=1.0,<1.1', - 'ipywidgets >=6.0.0.beta5' - ], - 'dev': [ - 'coverage >=4,<5', - 'pylint >=1,<2', - 'pytest >=3,<4', - 'pytest-cov >=2,<3', + dev=[ 'pytest-timeout >=1,<2', - 'sphinx', - 'sphinx_rtd_theme', - 'yapf', ], -} - -data_files = [ - ('share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map', - ]), -] - -entry_points = { - 'console_scripts': [ - 'bonobo = bonobo.commands:entrypoint', - ], - 'bonobo.commands': [ - 'init = bonobo.commands.init:register', - 'run = bonobo.commands.run:register', - 'version = bonobo.commands.version:register', - ], - 'edgy.project.features': [ - 'bonobo = bonobo.ext.edgy.project.feature:BonoboFeature' + jupyter=[ + 'jupyter >=1.0,<1.1', + 'ipywidgets >=6.0.0.beta5', ] -} - -@listen('edgy.project.feature.make.on_generate', priority=10) -def on_make_generate_docker_targets(event): - event.makefile['SPHINX_SOURCEDIR'] = 'docs' +) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1cdb4ad..18f7807 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,27 +1,24 @@ -e .[dev] - alabaster==0.7.10 -astroid==1.5.2 babel==2.4.0 -coverage==4.3.4 +certifi==2017.4.17 +chardet==3.0.3 +coverage==4.4.1 docutils==0.13.1 +idna==2.5 imagesize==0.7.1 -isort==4.2.5 jinja2==2.9.6 -lazy-object-proxy==1.2.2 markupsafe==1.0 -mccabe==0.6.1 py==1.4.33 pygments==2.2.0 -pylint==1.7.1 -pytest-cov==2.4.0 +pytest-cov==2.5.1 pytest-timeout==1.2.0 -pytest==3.0.7 +pytest==3.1.0 pytz==2017.2 -requests==2.13.0 +requests==2.16.5 six==1.10.0 snowballstemmer==1.2.1 -sphinx-rtd-theme==0.2.4 -sphinx==1.5.5 -wrapt==1.10.10 -yapf==0.16.1 +sphinx==1.6.1 +sphinxcontrib-websupport==1.0.1 +typing==3.6.1 +urllib3==1.21.1 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 1cf03f6..1e98481 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,5 +1,4 @@ -e .[jupyter] - appnope==0.1.0 bleach==2.0.0 decorator==4.0.11 @@ -18,7 +17,7 @@ jupyter-core==4.3.0 jupyter==1.0.0 markupsafe==1.0 mistune==0.7.4 -nbconvert==5.1.1 +nbconvert==5.2.1 nbformat==4.3.0 notebook==5.0.0 pandocfilters==1.4.1 @@ -33,7 +32,7 @@ qtconsole==4.3.0 simplegeneric==0.8.1 six==1.10.0 terminado==0.6 -testpath==0.3 +testpath==0.3.1 tornado==4.5.1 traitlets==4.3.2 wcwidth==0.1.7 diff --git a/requirements.txt b/requirements.txt index b11cfa3..e9bdffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,15 @@ -e . - appdirs==1.4.3 +certifi==2017.4.17 +chardet==3.0.3 colorama==0.3.9 enum34==1.1.6 fs==2.0.3 -pbr==3.0.0 +idna==2.5 +pbr==3.0.1 psutil==5.2.2 pytz==2017.2 -requests==2.13.0 +requests==2.16.5 six==1.10.0 stevedore==1.21.0 +urllib3==1.21.1 diff --git a/setup.py b/setup.py index 844240a..62064d1 100644 --- a/setup.py +++ b/setup.py @@ -18,13 +18,19 @@ except NameError: # Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() +try: + with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() +except: + long_description = '' # Get the classifiers from the classifiers file tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n')))) -with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f: - classifiers = tolines(f.read()) +try: + with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f: + classifiers = tolines(f.read()) +except: + classifiers = [] version_ns = {} try: @@ -35,41 +41,34 @@ else: version = version_ns.get('__version__', 'dev') setup( - name='bonobo', + author='Romain Dorgueil', + author_email='romain@dorgueil.net', description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for ' 'python 3.5+.'), license='Apache License, Version 2.0', - install_requires=[ - 'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0' - ], + name='bonobo', version=version, long_description=long_description, classifiers=classifiers, packages=find_packages(exclude=['ez_setup', 'example', 'test']), include_package_data=True, - data_files=[ - ( - 'share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map' - ] - ) + install_requires=[ + 'colorama (>= 0.3, < 1.0)', 'fs (>= 2.0, < 3.0)', 'psutil (>= 5.2, < 6.0)', 'requests (>= 2.0, < 3.0)', + 'stevedore (>= 1.21, < 2.0)' ], extras_require={ 'dev': [ - 'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx', - 'sphinx_rtd_theme', 'yapf' + 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', + 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' ], - 'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5'] + 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] }, entry_points={ 'bonobo.commands': [ 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], - 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], - 'edgy.project.features': ['bonobo = ' - 'bonobo.ext.edgy.project.feature:BonoboFeature'] + 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] }, url='https://www.bonobo-project.org/', download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), From 44bc3f909d0270852ad72dd6bc211b8dfc2626d5 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 12:08:41 +0200 Subject: [PATCH 062/143] [misc] updating dependency management. --- Makefile | 3 ++- Projectfile | 15 ++++++++------- setup.py | 4 +++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 75e5700..a1d49da 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-28 12:03:49.427061 +# Updated at 2017-05-28 12:24:17.298429 PACKAGE ?= bonobo PYTHON ?= $(shell which python) @@ -48,3 +48,4 @@ $(SPHINX_SOURCEDIR): install-dev format: install-dev $(YAPF) $(YAPF_OPTIONS) . + $(YAPF) $(YAPF_OPTIONS) Projectfile diff --git a/Projectfile b/Projectfile index a04e297..90f8c65 100644 --- a/Projectfile +++ b/Projectfile @@ -16,13 +16,14 @@ python.setup( author='Romain Dorgueil', author_email='romain@dorgueil.net', data_files=[ - ('share/jupyter/nbextensions/bonobo-jupyter', [ - 'bonobo/ext/jupyter/static/extension.js', - 'bonobo/ext/jupyter/static/index.js', - 'bonobo/ext/jupyter/static/index.js.map', - ]), + ( + 'share/jupyter/nbextensions/bonobo-jupyter', [ + 'bonobo/ext/jupyter/static/extension.js', + 'bonobo/ext/jupyter/static/index.js', + 'bonobo/ext/jupyter/static/index.js.map', + ] + ), ], - entry_points={ 'console_scripts': [ 'bonobo = bonobo.commands:entrypoint', @@ -32,8 +33,8 @@ python.setup( 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register', ], + 'edgy.project.features': ['bonobo = bonobo.ext.edgy.project.feature:BonoboFeature'] } - ) python.add_requirements( diff --git a/setup.py b/setup.py index 62064d1..3b39127 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,9 @@ setup( 'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register', 'version = bonobo.commands.version:register' ], - 'console_scripts': ['bonobo = bonobo.commands:entrypoint'] + 'console_scripts': ['bonobo = bonobo.commands:entrypoint'], + 'edgy.project.features': ['bonobo = ' + 'bonobo.ext.edgy.project.feature:BonoboFeature'] }, url='https://www.bonobo-project.org/', download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version), From d8232abfc2000ab10cc4d15d87a50a7ead4c41a2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 13:03:13 +0200 Subject: [PATCH 063/143] release: 0.3.1 --- bonobo/_version.py | 2 +- docs/changelog.rst | 59 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index 0404d81..e1424ed 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.3.0' +__version__ = '0.3.1' diff --git a/docs/changelog.rst b/docs/changelog.rst index 061e02c..c386ff7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,29 +1,63 @@ Changelog ========= +v.0.3.1 - 28 may 2017 +::::::::::::::::::::: + +Weekly maintenance release. + +* Updated project management model to edgy.project 0.3 format. +* Updated frozen version numbers in requirements. + + * certifi==2017.4.17 + * chardet==3.0.3 + * coverage==4.4.1 + * idna==2.5 + * nbconvert==5.2.1 + * pbr==3.0.1 + * pytest-cov==2.5.1 + * pytest==3.1.0 + * requests==2.16.5 + * sphinx==1.6.1 + * sphinxcontrib-websupport==1.0.1 + * testpath==0.3.1 + * typing==3.6.1 + * urllib3==1.21.1 + + Note: this does not change anything when used as a dependency if you freeze your requirements, as the setup.py + requirement specifiers did not change. + v.0.3.0 - 22 may 2017 ::::::::::::::::::::: Features -------- -* ContextProcessors can now be implemented by getting the "yield" value (v = yield x), shortening the teardown-only context processors by one line. +* ContextProcessors can now be implemented by getting the "yield" value (v = yield x), shortening the teardown-only + context processors by one line. * File related writers (file, csv, json ...) now returns NOT_MODIFIED, making it easier to chain something after. * More consistent console output, nodes are now sorted in a topological order before display. -* Graph.add_chain(...) now takes _input and _output parameters the same way, accepting indexes, instances or names (subject to change). -* Graph.add_chain(...) now allows to "name" a chain, using _name keyword argument, to easily reference its output later (subject to change). -* New settings module (bonobo.settings) read environment for some global configuration stuff (DEBUG and PROFILE, for now). -* New Method subclass of Option allows to use Configurable objects as decorator (see bonobo.nodes.filter.Filter for a simple example). +* Graph.add_chain(...) now takes _input and _output parameters the same way, accepting indexes, instances or names + (subject to change). +* Graph.add_chain(...) now allows to "name" a chain, using _name keyword argument, to easily reference its output later + (subject to change). +* New settings module (bonobo.settings) read environment for some global configuration stuff (DEBUG and PROFILE, for + now). +* New Method subclass of Option allows to use Configurable objects as decorator (see bonobo.nodes.filter.Filter for a + simple example). * New Filter transformation in standard library. Internal features ----------------- -* Better ContextProcessor implementation, avoiding to use a decorator on the parent class. Now works with Configurable instances like Option, Service and Method. -* ContextCurrifier replaces the logic that was in NodeExecutionContext, that setup and teardown the context stack. Maybe the name is not ideal. +* Better ContextProcessor implementation, avoiding to use a decorator on the parent class. Now works with Configurable + instances like Option, Service and Method. +* ContextCurrifier replaces the logic that was in NodeExecutionContext, that setup and teardown the context stack. Maybe + the name is not ideal. * All builtin transformations are of course updated to use the improved API, and should be 100% backward compatible. * The "core" package has been dismantled, and its rare remaining members are now in "structs" and "util" packages. -* Standard transformation library has been moved under the bonobo.nodes package. It does not change anything if you used bonobo.* (which you should). +* Standard transformation library has been moved under the bonobo.nodes package. It does not change anything if you used + bonobo.* (which you should). * ValueHolder is now more restrictive, not allowing to use .value anymore. Miscellaneous @@ -40,7 +74,8 @@ v.0.2.4 - 2 may 2017 v.0.2.3 - 1 may 2017 ::::::::::::::::::::: -* Positional options now supported, backward compatible. All FileHandler subclasses supports their path argument as positional. +* Positional options now supported, backward compatible. All FileHandler subclasses supports their path argument as + positional. * Better transformation lifecycle management (still work needed here). * Windows continuous integration now works. * Refactoring the "API" a lot to have a much cleaner first glance at it. @@ -74,10 +109,12 @@ Initial release * Migration from rdc.etl. * New cool name (ok, that's debatable). -* Only supports python 3.5+, aggressively (which means, we can use async, and we remove all things from python 2/six compat) +* Only supports python 3.5+, aggressively (which means, we can use async, and we remove all things from python 2/six + compat) * Removes all thing deprecated and/or not really convincing from rdc.etl. * We want transforms to be simple callables, so refactoring of the harness mess. -* We want to use plain python data structures, so hashes are removed. If you use python 3.6, you may even get sorted dicts. +* We want to use plain python data structures, so hashes are removed. If you use python 3.6, you may even get sorted + dicts. * Input/output MUX DEMUX removed, maybe no need for that in the real world. May come back, but not in 1.0 * Change dependency policy. We need to include only the very basic requirements (and very required). Everything related to transforms that we may not use (bs, sqla, ...) should be optional dependencies. From 9370f6504e0c3228afabda76947c2ef015bbb5a1 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 16:51:02 +0200 Subject: [PATCH 064/143] [ext] Adds docker extra to setup.py --- Makefile | 2 +- Projectfile | 3 +++ requirements-docker.txt | 22 ++++++++++++++++++++++ setup.py | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 requirements-docker.txt diff --git a/Makefile b/Makefile index fac0470..7dcf058 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-28 13:14:45.778931 +# Updated at 2017-05-28 16:50:32.109035 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/Projectfile b/Projectfile index dd0aaaf..bc5443e 100644 --- a/Projectfile +++ b/Projectfile @@ -46,6 +46,9 @@ python.add_requirements( dev=[ 'pytest-timeout >=1,<2', ], + docker=[ + 'bonobo-docker', + ], jupyter=[ 'jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5', diff --git a/requirements-docker.txt b/requirements-docker.txt new file mode 100644 index 0000000..844d43a --- /dev/null +++ b/requirements-docker.txt @@ -0,0 +1,22 @@ +-e .[docker] +appdirs==1.4.3 +bonobo-docker==0.2.3 +bonobo==0.3.1 +certifi==2017.4.17 +chardet==3.0.3 +colorama==0.3.9 +docker-pycreds==0.2.1 +docker==2.3.0 +enum34==1.1.6 +fs==2.0.3 +idna==2.5 +packaging==16.8 +pbr==3.0.1 +psutil==5.2.2 +pyparsing==2.2.0 +pytz==2017.2 +requests==2.16.5 +six==1.10.0 +stevedore==1.21.0 +urllib3==1.21.1 +websocket-client==0.40.0 diff --git a/setup.py b/setup.py index cc20417..3aa6575 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ setup( 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' ], + 'docker': ['bonobo-docker'], 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] }, entry_points={ From c94a7ffbb2fc2b7c5b8c9c472ff2254df20ba525 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 17:36:27 +0200 Subject: [PATCH 065/143] Update init.py --- bonobo/commands/init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index ad3c52a..d7ad066 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -6,7 +6,7 @@ def execute(name): from cookiecutter.main import cookiecutter except ImportError as exc: raise ImportError( - 'You must install "cookiecutter" to use this command.\n\n $ pip install edgy.project\n' + 'You must install "cookiecutter" to use this command.\n\n $ pip install cookiecutter\n' ) from exc return cookiecutter( From 0146fb0d552c5bfd35e4d0d0f4a41880e096f7e2 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 19:21:12 +0200 Subject: [PATCH 066/143] [doc] Documentation work for the 0.4 release (not finished). --- Makefile | 2 +- Projectfile | 1 + bonobo/examples/tutorials/tut01e01.py | 23 +++ bonobo/examples/tutorials/tut01e02.py | 10 ++ .../{tut02_01_read.py => tut02e01_read.py} | 0 .../{tut02_02_write.py => tut02e02_write.py} | 0 ...3_writeasmap.py => tut02e03_writeasmap.py} | 0 docs/reference/examples.rst | 12 +- docs/reference/examples/tutorials.rst | 50 ++++++ docs/tutorial/tut01.rst | 164 +++++++++++++----- docs/tutorial/tut02.rst | 85 +++++---- docs/tutorial/tut03.rst | 9 + docs/tutorial/tut04.rst | 4 + requirements-dev.txt | 12 +- setup.py | 4 +- 15 files changed, 282 insertions(+), 94 deletions(-) create mode 100644 bonobo/examples/tutorials/tut01e01.py create mode 100644 bonobo/examples/tutorials/tut01e02.py rename bonobo/examples/tutorials/{tut02_01_read.py => tut02e01_read.py} (100%) rename bonobo/examples/tutorials/{tut02_02_write.py => tut02e02_write.py} (100%) rename bonobo/examples/tutorials/{tut02_03_writeasmap.py => tut02e03_writeasmap.py} (100%) create mode 100644 docs/reference/examples/tutorials.rst create mode 100644 docs/tutorial/tut03.rst create mode 100644 docs/tutorial/tut04.rst diff --git a/Makefile b/Makefile index 7dcf058..a6fb6f8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-28 16:50:32.109035 +# Updated at 2017-05-28 18:02:16.552433 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/Projectfile b/Projectfile index bc5443e..46f262b 100644 --- a/Projectfile +++ b/Projectfile @@ -45,6 +45,7 @@ python.add_requirements( 'stevedore >=1.21,<2.0', dev=[ 'pytest-timeout >=1,<2', + 'cookiecutter >=1.5,<1.6', ], docker=[ 'bonobo-docker', diff --git a/bonobo/examples/tutorials/tut01e01.py b/bonobo/examples/tutorials/tut01e01.py new file mode 100644 index 0000000..c524039 --- /dev/null +++ b/bonobo/examples/tutorials/tut01e01.py @@ -0,0 +1,23 @@ +import bonobo + + +def extract(): + yield 'foo' + yield 'bar' + yield 'baz' + + +def transform(x): + return x.upper() + + +def load(x): + print(x) + + +graph = bonobo.Graph(extract, transform, load) + +graph.__doc__ = 'hello' + +if __name__ == '__main__': + bonobo.run(graph) diff --git a/bonobo/examples/tutorials/tut01e02.py b/bonobo/examples/tutorials/tut01e02.py new file mode 100644 index 0000000..3784235 --- /dev/null +++ b/bonobo/examples/tutorials/tut01e02.py @@ -0,0 +1,10 @@ +import bonobo + +graph = bonobo.Graph( + ['foo', 'bar', 'baz', ], + str.upper, + print, +) + +if __name__ == '__main__': + bonobo.run(graph) diff --git a/bonobo/examples/tutorials/tut02_01_read.py b/bonobo/examples/tutorials/tut02e01_read.py similarity index 100% rename from bonobo/examples/tutorials/tut02_01_read.py rename to bonobo/examples/tutorials/tut02e01_read.py diff --git a/bonobo/examples/tutorials/tut02_02_write.py b/bonobo/examples/tutorials/tut02e02_write.py similarity index 100% rename from bonobo/examples/tutorials/tut02_02_write.py rename to bonobo/examples/tutorials/tut02e02_write.py diff --git a/bonobo/examples/tutorials/tut02_03_writeasmap.py b/bonobo/examples/tutorials/tut02e03_writeasmap.py similarity index 100% rename from bonobo/examples/tutorials/tut02_03_writeasmap.py rename to bonobo/examples/tutorials/tut02e03_writeasmap.py diff --git a/docs/reference/examples.rst b/docs/reference/examples.rst index e685f79..b36c414 100644 --- a/docs/reference/examples.rst +++ b/docs/reference/examples.rst @@ -1,10 +1,16 @@ Examples ======== -There are a few examples bundled with **bonobo**. You'll find them under the :mod:`bonobo.examples` package, and -you can try them in a clone of bonobo by typing:: +There are a few examples bundled with **bonobo**. - $ bonobo run bonobo/examples/.../file.py +You'll find them under the :mod:`bonobo.examples` package, and you can run them directly as modules: + + $ bonobo run -m bonobo.examples...module + +.. toctree:: + :maxdepth: 4 + + examples/tutorials Datasets diff --git a/docs/reference/examples/tutorials.rst b/docs/reference/examples/tutorials.rst new file mode 100644 index 0000000..2a5ca4f --- /dev/null +++ b/docs/reference/examples/tutorials.rst @@ -0,0 +1,50 @@ +Examples from the tutorial +========================== + +Examples from :doc:`/tutorial/tut01` +:::::::::::::::::::::::::::::::::::: + +Example 1 +--------- + +.. automodule:: bonobo.examples.tutorials.tut01e01 + :members: + :undoc-members: + :show-inheritance: + +Example 2 +--------- + +.. automodule:: bonobo.examples.tutorials.tut01e02 + :members: + :undoc-members: + :show-inheritance: + +Examples from :doc:`/tutorial/tut02` +:::::::::::::::::::::::::::::::::::: + +Example 1: Read +--------------- + +.. automodule:: bonobo.examples.tutorials.tut02e01_read + :members: + :undoc-members: + :show-inheritance: + +Example 2: Write +---------------- + +.. automodule:: bonobo.examples.tutorials.tut02e02_write + :members: + :undoc-members: + :show-inheritance: + +Example 3: Write as map +----------------------- + +.. automodule:: bonobo.examples.tutorials.tut02e02_writeasmap + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/tutorial/tut01.rst b/docs/tutorial/tut01.rst index ddff6e8..0357926 100644 --- a/docs/tutorial/tut01.rst +++ b/docs/tutorial/tut01.rst @@ -1,58 +1,91 @@ -Basic concepts -============== +Let's get started! +================== -To begin with Bonobo, you need to install it in a working python 3.5+ environment: +To begin with Bonobo, you need to install it in a working python 3.5+ environment, and you'll also need cookiecutter +to bootstrap your project. .. code-block:: shell-session - $ pip install bonobo + $ pip install bonobo cookiecutter See :doc:`/install` for more options. -Let's write a first data transformation -::::::::::::::::::::::::::::::::::::::: -We'll start with the simplest transformation possible. +Create an empty project +::::::::::::::::::::::: -In **Bonobo**, a transformation is a plain old python callable, not more, not less. Let's write one that takes a string -and uppercases it. +Your ETL code will live in ETL projects, which are basically a bunch of files, including python code, that bonobo +can run. + +.. code-block:: shell-session + + bonobo init tutorial + +This will create a `tutorial` directory (`content description here `_). + +To run this project, use: + +.. code-block:: shell-session + + bonobo run tutorial + + +Write a first transformation +:::::::::::::::::::::::::::: + +Open `tutorial/__main__.py`, and delete all the code here. + +A transformation can be whatever python can call, having inputs and outputs. Simplest transformations are functions. + +Let's write one: .. code-block:: python - def uppercase(x: str): + def transform(x): return x.upper() -Pretty straightforward. +Easy. -You could even use :func:`str.upper` directly instead of writing a wrapper, as a type's method (unbound) will take an -instance of this type as its first parameter (what you'd call `self` in your method). +.. note:: -The type annotations written here are not used, but can make your code much more readable, and may very well be used as -validators in the future. + This is about the same as :func:`str.upper`, and in the real world, you'd use it directly. -Let's write two more transformations: a generator to produce the data to be transformed, and something that outputs it, -because, yeah, feedback is cool. +Let's write two more transformations for the "extract" and "load" steps. In this example, we'll generate the data from +scratch, and we'll use stdout to simulate data-persistence. .. code-block:: python - def generate_data(): + def extract(): yield 'foo' yield 'bar' yield 'baz' - def output(x: str): + def load(x): print(x) -Once again, you could have skipped the pain of writing this and simply use an iterable to generate the data and the -builtin :func:`print` for the output, but we'll stick to writing our own transformations for now. +Bonobo makes no difference between generators (yielding functions) and regular functions. It will, in all cases, iterate +on things returned, and a normal function will just be seen as a generator that yields only once. -Let's chain the three transformations together and run the transformation graph: +.. note:: + + Once again, :func:`print` would be used directly in a real-world transformation. + + +Create a transformation graph +::::::::::::::::::::::::::::: + +Bonobo main roles are two things: + +* Execute the transformations in independant threads +* Pass the outputs of one thread to other(s) thread(s). + +To do this, it needs to know what data-flow you want to achieve, and you'll use a :class:`bonobo.Graph` to describe it. .. code-block:: python import bonobo - graph = bonobo.Graph(generate_data, uppercase, output) + graph = bonobo.Graph(extract, transform, load) if __name__ == '__main__': bonobo.run(graph) @@ -64,14 +97,60 @@ Let's chain the three transformations together and run the transformation graph: stylesheet = "../_static/graphs.css"; BEGIN [shape="point"]; - BEGIN -> "generate_data" -> "uppercase" -> "output"; + BEGIN -> "extract" -> "transform" -> "load"; } -We use the :func:`bonobo.run` helper that hides the underlying object composition necessary to actually run the -transformations in parallel, because it's simpler. +.. note:: -Depending on what you're doing, you may use the shorthand helper method, or the verbose one. Always favor the shorter, -if you don't need to tune the graph or the execution strategy (see below). + The `if __name__ == '__main__':` section is not required, unless you want to run it directly using the python + interpreter. + + +Execute the job +::::::::::::::: + +Save `tutorial/__main__.py` and execute your transformation: + +.. code-block:: shell-session + + bonobo run tutorial + +This example is available in :mod:`bonobo.examples.tutorials.tut01e01`, and you can also run it as a module: + +.. code-block:: shell-session + + bonobo run -m bonobo.examples.tutorials.tut01e01 + + +Rewrite it using builtins +::::::::::::::::::::::::: + +There is a much simpler way to describe an equivalent graph: + +.. code-block:: python + + import bonobo + + graph = bonobo.Graph( + ['foo', 'bar', 'baz',], + str.upper, + print, + ) + + if __name__ == '__main__': + bonobo.run(graph) + +We use a shortcut notation for the generator, with a list. Bonobo will wrap an iterable as a generator by itself if it +is added in a graph. + +This example is available in :mod:`bonobo.examples.tutorials.tut01e02`, and you can also run it as a module: + +.. code-block:: shell-session + + bonobo run -m bonobo.examples.tutorials.tut01e02 + +You can now jump to the next part (:doc:`tut02`), or read a small summary of concepts and definitions introduced here +below. Takeaways ::::::::: @@ -79,7 +158,7 @@ Takeaways ① The :class:`bonobo.Graph` class is used to represent a data-processing pipeline. It can represent simple list-like linear graphs, like here, but it can also represent much more complex graphs, with -branches and cycles. +forks and joins. This is what the graph we defined looks like: @@ -97,10 +176,10 @@ either `return` or `yield` data to send it to the next step. Regular functions ( each call is guaranteed to return exactly one result, while generators (using `yield`) should be prefered if the number of output lines for a given input varies. -③ The `Graph` instance, or `transformation graph` is then executed using an `ExecutionStrategy`. You did not use it -directly in this tutorial, but :func:`bonobo.run` created an instance of :class:`bonobo.ThreadPoolExecutorStrategy` -under the hood (which is the default strategy). Actual behavior of an execution will depend on the strategy chosen, but -the default should be fine in most of the basic cases. +③ The `Graph` instance, or `transformation graph` is executed using an `ExecutionStrategy`. You won't use it directly, +but :func:`bonobo.run` created an instance of :class:`bonobo.ThreadPoolExecutorStrategy` under the hood (the default +strategy). Actual behavior of an execution will depend on the strategy chosen, but the default should be fine for most +cases. ④ Before actually executing the `transformations`, the `ExecutorStrategy` instance will wrap each component in an `execution context`, whose responsibility is to hold the state of the transformation. It enables to keep the @@ -111,21 +190,22 @@ Concepts and definitions * Transformation: a callable that takes input (as call parameters) and returns output(s), either as its return value or by yielding values (a.k.a returning a generator). -* Transformation graph (or Graph): a set of transformations tied together in a :class:`bonobo.Graph` instance, which is a simple - directed acyclic graph (also refered as a DAG, sometimes). -* Node: a transformation within the context of a transformation graph. The node defines what to do with a - transformation's output, and especially what other nodes to feed with the output. + +* Transformation graph (or Graph): a set of transformations tied together in a :class:`bonobo.Graph` instance, which is + a directed acyclic graph (or DAG). + +* Node: a graph element, most probably a transformation in a graph. + * Execution strategy (or strategy): a way to run a transformation graph. It's responsibility is mainly to parallelize (or not) the transformations, on one or more process and/or computer, and to setup the right queuing mechanism for transformations' inputs and outputs. + * Execution context (or context): a wrapper around a node that holds the state for it. If the node needs state, there - are tools available in bonobo to feed it to the transformation using additional call parameters, and so every - transformation will be atomic. + are tools available in bonobo to feed it to the transformation using additional call parameters, keeping + transformations stateless. Next :::: -You now know all the basic concepts necessary to build (batch-like) data processors. - -Time to jump to the second part: :doc:`tut02` +Time to jump to the second part: :doc:`tut02`. diff --git a/docs/tutorial/tut02.rst b/docs/tutorial/tut02.rst index 0c96a40..685e455 100644 --- a/docs/tutorial/tut02.rst +++ b/docs/tutorial/tut02.rst @@ -1,11 +1,14 @@ Working with files ================== -Bonobo would be a bit useless if the aim was just to uppercase small lists of strings. +Bonobo would be pointless if the aim was just to uppercase small lists of strings. In fact, Bonobo should not be used if you don't expect any gain from parallelization/distribution of tasks. -Let's take the following graph as an example: +Some background... +:::::::::::::::::: + +Let's take the following graph: .. graphviz:: @@ -16,8 +19,8 @@ Let's take the following graph as an example: "B" -> "D"; } -The execution strategy does a bit of under the scene work, wrapping every component in a thread (assuming you're using -the :class:`bonobo.strategies.ThreadPoolExecutorStrategy`). +When run, the execution strategy wraps every component in a thread (assuming you're using the default +:class:`bonobo.strategies.ThreadPoolExecutorStrategy`). Bonobo will send each line of data in the input node's thread (here, `A`). Now, each time `A` *yields* or *returns* something, it will be pushed on `B` input :class:`queue.Queue`, and will be consumed by `B`'s thread. @@ -25,9 +28,11 @@ something, it will be pushed on `B` input :class:`queue.Queue`, and will be cons When there is more than one node linked as the output of a node (for example, with `B`, `C`, and `D`) , the same thing happens except that each result coming out of `B` will be sent to both on `C` and `D` input :class:`queue.Queue`. -The great thing is that you generally don't have to think about it. Just be aware that your components will be run in -parallel (with the default strategy), and don't worry too much about blocking components, as they won't block their -siblings when run in bonobo. +One thing to keep in mind here is that as the objects are passed from thread to thread, you need to write "pure" +transformations (see :doc:`/guide/purity`). + +You generally don't have to think about it. Just be aware that your nodes will run in parallel, and don't worry +too much about blocking nodes, as they won't block other nodes. That being said, let's manipulate some files. @@ -38,9 +43,10 @@ There are a few component builders available in **Bonobo** that let you read fro All readers work the same way. They need a filesystem to work with, and open a "path" they will read from. -* :class:`bonobo.io.FileReader` -* :class:`bonobo.io.JsonReader` -* :class:`bonobo.io.CsvReader` +* :class:`bonobo.CsvReader` +* :class:`bonobo.FileReader` +* :class:`bonobo.JsonReader` +* :class:`bonobo.PickleReader` We'll use a text file that was generated using Bonobo from the "liste-des-cafes-a-un-euro" dataset made available by Mairie de Paris under the Open Database License (ODbL). You can `explore the original dataset @@ -49,35 +55,14 @@ Mairie de Paris under the Open Database License (ODbL). You can `explore the ori You'll need the `example dataset `_, available in **Bonobo**'s repository. -.. literalinclude:: ../../bonobo/examples/tutorials/tut02_01_read.py +.. literalinclude:: ../../bonobo/examples/tutorials/tut02e01_read.py :language: python -You can run this script directly using the python interpreter: +You can run this example as a module: .. code-block:: shell-session - $ python bonobo/examples/tutorials/tut02_01_read.py - -Another option is to use the bonobo cli, which allows more flexibility: - -.. code-block:: shell-session - - $ bonobo run bonobo/examples/tutorials/tut02_01_read.py - -Using bonobo command line has a few advantages. - -It will look for one and only one :class:`bonobo.Graph` instance in the file given as argument, configure an execution -strategy, eventually plugins, and execute it. It has the benefit of allowing to tune the "artifacts" surrounding the -transformation graph on command line (verbosity, plugins ...), and it will also ease the transition to run -transformation graphs in containers, as the syntax will be the same. Of course, it is not required, and the -containerization capabilities are provided by an optional and separate python package. - -It also change a bit the way you can configure service dependencies. The CLI won't run the `if __name__ == '__main__'` -block, and thus it won't get the configured services passed to :func:`bonobo.run`. Instead, one option to configure -services is to define a `get_services()` function in a -`_services.py `_ file. - -There will be more options using the CLI or environment to override things soon. + $ bonobo run -m bonobo.examples.tutorials.tut02e01_read Writing to files :::::::::::::::: @@ -86,22 +71,34 @@ Let's split this file's each lines on the first comma and store a json file mapp Here are, like the readers, the classes available to write files -* :class:`bonobo.io.FileWriter` -* :class:`bonobo.io.JsonWriter` -* :class:`bonobo.io.CsvWriter` +* :class:`bonobo.CsvWriter` +* :class:`bonobo.FileWriter` +* :class:`bonobo.JsonWriter` +* :class:`bonobo.PickleWriter` Let's write a first implementation: -.. literalinclude:: ../../bonobo/examples/tutorials/tut02_02_write.py +.. literalinclude:: ../../bonobo/examples/tutorials/tut02e02_write.py :language: python -You can run it and read the output file, you'll see it misses the "map" part of the question. Let's extend -:class:`bonobo.io.JsonWriter` to finish the job: +(run it with :code:`bonobo run -m bonobo.examples.tutorials.tut02e02_write` or :code:`bonobo run myfile.py`) -.. literalinclude:: ../../bonobo/examples/tutorials/tut02_03_writeasmap.py +If you read the output file, you'll see it misses the "map" part of the problem. + +Let's extend :class:`bonobo.io.JsonWriter` to finish the job: + +.. literalinclude:: ../../bonobo/examples/tutorials/tut02e03_writeasmap.py :language: python -You can now run it again, it should produce a nice map. We favored a bit hackish solution here instead of constructing a -map in python then passing the whole to :func:`json.dumps` because we want to work with streams, if you have to -construct the whole data structure in python, you'll loose a lot of bonobo's benefits. +(run it with :code:`bonobo run -m bonobo.examples.tutorials.tut02e03_writeasmap` or :code:`bonobo run myfile.py`) +It should produce a nice map. + +We favored a bit hackish solution here instead of constructing a map in python then passing the whole to +:func:`json.dumps` because we want to work with streams, if you have to construct the whole data structure in python, +you'll loose a lot of bonobo's benefits. + +Next +:::: + +Time to write some more advanced transformations, with service dependencies: :doc:`tut03`. diff --git a/docs/tutorial/tut03.rst b/docs/tutorial/tut03.rst new file mode 100644 index 0000000..dc1e4c2 --- /dev/null +++ b/docs/tutorial/tut03.rst @@ -0,0 +1,9 @@ +Configurables and Services +========================== + +TODO + +Next +:::: + +:doc:`tut04`. diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst new file mode 100644 index 0000000..18bea48 --- /dev/null +++ b/docs/tutorial/tut04.rst @@ -0,0 +1,4 @@ +Working with databases +====================== + +TODO diff --git a/requirements-dev.txt b/requirements-dev.txt index 18f7807..c346203 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,24 +1,32 @@ -e .[dev] alabaster==0.7.10 +arrow==0.10.0 babel==2.4.0 +binaryornot==0.4.3 certifi==2017.4.17 chardet==3.0.3 +click==6.7 +cookiecutter==1.5.1 coverage==4.4.1 docutils==0.13.1 +future==0.16.0 idna==2.5 imagesize==0.7.1 +jinja2-time==0.2.0 jinja2==2.9.6 markupsafe==1.0 +poyo==0.4.1 py==1.4.33 pygments==2.2.0 pytest-cov==2.5.1 pytest-timeout==1.2.0 pytest==3.1.0 +python-dateutil==2.6.0 pytz==2017.2 requests==2.16.5 six==1.10.0 snowballstemmer==1.2.1 -sphinx==1.6.1 +sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 -typing==3.6.1 urllib3==1.21.1 +whichcraft==0.4.1 diff --git a/setup.py b/setup.py index 3aa6575..618ec41 100644 --- a/setup.py +++ b/setup.py @@ -58,8 +58,8 @@ setup( ], extras_require={ 'dev': [ - 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', - 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' + 'cookiecutter (>= 1.5, < 1.6)', 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', + 'pytest-cov (>= 2.5, < 3.0)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' ], 'docker': ['bonobo-docker'], 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] From ac800d2e148fb3a53cd1d3d9baeb83d8f3f23d81 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 23:02:34 +0200 Subject: [PATCH 067/143] [cli] Setup logging to unsilence stevedore errors, like failing to load an entrypoint. --- Makefile | 2 +- bonobo/commands/__init__.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a6fb6f8..783e61b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-28 18:02:16.552433 +# Updated at 2017-05-28 22:04:19.262686 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 37d55b1..feae672 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -5,6 +5,7 @@ from stevedore import ExtensionManager def entrypoint(args=None): + logging.basicConfig() parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='command') @@ -19,9 +20,7 @@ def entrypoint(args=None): except Exception: logging.exception('Error while loading command {}.'.format(ext.name)) - mgr = ExtensionManager( - namespace='bonobo.commands', - ) + mgr = ExtensionManager(namespace='bonobo.commands') mgr.map(register_extension) args = parser.parse_args(args).__dict__ From 5b7e3c832475105764f4a327dad5ec9c0a7e55f0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 23:03:22 +0200 Subject: [PATCH 068/143] [cli] fixes --install which did not work with --editable packages, aka things added to sys.path by some *.pth file in a very not explicit way. --- bonobo/commands/run.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 0f85eb8..8bb7b10 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,3 +1,4 @@ +import importlib import os import runpy @@ -44,7 +45,13 @@ def execute(filename, module, install=False, quiet=False, verbose=False): if os.path.isdir(filename): if install: requirements = os.path.join(filename, 'requirements.txt') - pip.main(['install', '-qr', requirements]) + pip.main(['install', '-r', requirements]) + # Some shenanigans to be sure everything is importable after this, especially .egg-link files which + # are referenced in *.pth files and apparently loaded by site.py at some magic bootstrap moment of the + # python interpreter. + pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources) + import site + importlib.reload(site) filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME) elif install: raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).') From 3ca5f962d268aad9f8ad7a977a1c50b4a922d7dd Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 23:06:20 +0200 Subject: [PATCH 069/143] [qa] removes examples and docs from travis as it does not fail on the first and duplicate rtd work on the second, at the cost of a much longer build time. --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e82faaf..3eb10b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,6 @@ install: - make install-dev - pip install coveralls script: - - make clean docs test - - pip install pycountry - - bin/run_all_examples.sh + - make clean test after_success: - coveralls From 2c2bc4fca93989a2c9635b33a1e80bd3af110ff1 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 28 May 2017 23:15:40 +0200 Subject: [PATCH 070/143] [qa] fix unclosed files problems in tests. --- tests/io/test_csv.py | 9 ++++++--- tests/io/test_file.py | 9 ++++++--- tests/io/test_json.py | 6 ++++-- tests/io/test_pickle.py | 7 ++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index bded111..3a6fd37 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -19,7 +19,8 @@ def test_write_csv_to_file(tmpdir): context.step() context.stop() - assert fs.open(filename).read() == 'foo\nbar\nbaz\n' + with fs.open(filename)as fp: + assert fp.read() == 'foo\nbar\nbaz\n' with pytest.raises(AttributeError): getattr(context, 'file') @@ -27,7 +28,8 @@ def test_write_csv_to_file(tmpdir): def test_read_csv_from_file(tmpdir): fs, filename = open_fs(tmpdir), 'input.csv' - fs.open(filename, 'w').write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') + with fs.open(filename, 'w') as fp: + fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') reader = CsvReader(path=filename, delimiter=',') @@ -59,7 +61,8 @@ def test_read_csv_from_file(tmpdir): def test_read_csv_kwargs_output_formater(tmpdir): fs, filename = open_fs(tmpdir), 'input.csv' - fs.open(filename, 'w').write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') + with fs.open(filename, 'w') as fp: + fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') reader = CsvReader(path=filename, delimiter=',', output_format='kwargs') diff --git a/tests/io/test_file.py b/tests/io/test_file.py index a6ac8c4..1566b39 100644 --- a/tests/io/test_file.py +++ b/tests/io/test_file.py @@ -25,7 +25,8 @@ def test_file_writer_in_context(tmpdir, lines, output): context.step() context.stop() - assert fs.open(filename).read() == output + with fs.open(filename) as fp: + assert fp.read() == output def test_file_writer_out_of_context(tmpdir): @@ -36,13 +37,15 @@ def test_file_writer_out_of_context(tmpdir): with writer.open(fs) as fp: fp.write('Yosh!') - assert fs.open(filename).read() == 'Yosh!' + with fs.open(filename) as fp: + assert fp.read() == 'Yosh!' def test_file_reader_in_context(tmpdir): fs, filename = open_fs(tmpdir), 'input.txt' - fs.open(filename, 'w').write('Hello\nWorld\n') + with fs.open(filename, 'w') as fp: + fp.write('Hello\nWorld\n') reader = FileReader(path=filename) context = CapturingNodeExecutionContext(reader, services={'fs': fs}) diff --git a/tests/io/test_json.py b/tests/io/test_json.py index 442397d..56f679f 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -17,7 +17,8 @@ def test_write_json_to_file(tmpdir): context.step() context.stop() - assert fs.open(filename).read() == '[{"foo": "bar"}]' + with fs.open(filename) as fp: + assert fp.read() == '[{"foo": "bar"}]' with pytest.raises(AttributeError): getattr(context, 'file') @@ -28,7 +29,8 @@ def test_write_json_to_file(tmpdir): def test_read_json_from_file(tmpdir): fs, filename = open_fs(tmpdir), 'input.json' - fs.open(filename, 'w').write('[{"x": "foo"},{"x": "bar"}]') + with fs.open(filename, 'w') as fp: + fp.write('[{"x": "foo"},{"x": "bar"}]') reader = JsonReader(path=filename) context = CapturingNodeExecutionContext(reader, services={'fs': fs}) diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index 662fc4a..368e526 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -20,7 +20,8 @@ def test_write_pickled_dict_to_file(tmpdir): context.step() context.stop() - assert pickle.loads(fs.open(filename, 'rb').read()) == {'foo': 'bar'} + with fs.open(filename, 'rb') as fp: + assert pickle.loads(fp.read()) == {'foo': 'bar'} with pytest.raises(AttributeError): getattr(context, 'file') @@ -28,8 +29,8 @@ def test_write_pickled_dict_to_file(tmpdir): def test_read_pickled_list_from_file(tmpdir): fs, filename = open_fs(tmpdir), 'input.pkl' - fs.open(filename, - 'wb').write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) + with fs.open(filename, 'wb') as fp: + fp.write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) reader = PickleReader(path=filename) From 0ac9e05956aa9b65f223922f2f5362fbc357ddfe Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 May 2017 05:11:07 -0700 Subject: [PATCH 071/143] Update sphinx from 1.6.1 to 1.6.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 18f7807..1f03666 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ pytz==2017.2 requests==2.16.5 six==1.10.0 snowballstemmer==1.2.1 -sphinx==1.6.1 +sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 typing==3.6.1 urllib3==1.21.1 From 7157e890afa0652e39801443fc6b458e2de6790f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 May 2017 16:15:55 -0700 Subject: [PATCH 072/143] Update requests from 2.16.5 to 2.17.3 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 18f7807..8b01e64 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-timeout==1.2.0 pytest==3.1.0 pytz==2017.2 -requests==2.16.5 +requests==2.17.3 six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.1 From b2e3daf8c305c6e373065347bff36dbcb28270b3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 May 2017 16:15:57 -0700 Subject: [PATCH 073/143] Update requests from 2.16.5 to 2.17.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9bdffb..6845a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ idna==2.5 pbr==3.0.1 psutil==5.2.2 pytz==2017.2 -requests==2.16.5 +requests==2.17.3 six==1.10.0 stevedore==1.21.0 urllib3==1.21.1 From 0a429ab079bc9f65eb4befb3a3bc60b129cbb23d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 30 May 2017 06:33:59 -0700 Subject: [PATCH 074/143] Update stevedore from 1.21.0 to 1.22.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9bdffb..689f472 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ psutil==5.2.2 pytz==2017.2 requests==2.16.5 six==1.10.0 -stevedore==1.21.0 +stevedore==1.22.0 urllib3==1.21.1 From 8963ded2d1202f33785ce40a85c44ed68f2a9647 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 15:16:55 +0200 Subject: [PATCH 075/143] Update pytest from 3.1.0 to 3.1.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 18f7807..479a21a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ py==1.4.33 pygments==2.2.0 pytest-cov==2.5.1 pytest-timeout==1.2.0 -pytest==3.1.0 +pytest==3.1.1 pytz==2017.2 requests==2.16.5 six==1.10.0 From 1d070636e9dc1df8cd142ff58938f12e8c4dec05 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 15:16:56 +0200 Subject: [PATCH 076/143] Update requests from 2.16.5 to 2.17.3 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 479a21a..f67bee1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-timeout==1.2.0 pytest==3.1.1 pytz==2017.2 -requests==2.16.5 +requests==2.17.3 six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.1 From 99f9952c7cbf3fea1204eaea69521ecc124cd59c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 15:16:58 +0200 Subject: [PATCH 077/143] Update requests from 2.16.5 to 2.17.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9bdffb..6845a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ idna==2.5 pbr==3.0.1 psutil==5.2.2 pytz==2017.2 -requests==2.16.5 +requests==2.17.3 six==1.10.0 stevedore==1.21.0 urllib3==1.21.1 From bd6eddc48e1c8e5a4d1bf4529628c326b0d6700d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 15:17:49 +0200 Subject: [PATCH 078/143] Update sphinx from 1.6.1 to 1.6.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f67bee1..c7a14c6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ pytz==2017.2 requests==2.17.3 six==1.10.0 snowballstemmer==1.2.1 -sphinx==1.6.1 +sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 typing==3.6.1 urllib3==1.21.1 From 203843f9943b25e72804f720fb7fa2df8148fcdd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 15:17:51 +0200 Subject: [PATCH 079/143] Update stevedore from 1.21.0 to 1.22.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6845a63..8e999c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ psutil==5.2.2 pytz==2017.2 requests==2.17.3 six==1.10.0 -stevedore==1.21.0 +stevedore==1.22.0 urllib3==1.21.1 From 4b0015706e1f73735e8362ffa94bdb307eee0946 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 May 2017 19:32:59 +0200 Subject: [PATCH 080/143] Update pytest from 3.1.0 to 3.1.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d2deed2..c7a14c6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ py==1.4.33 pygments==2.2.0 pytest-cov==2.5.1 pytest-timeout==1.2.0 -pytest==3.1.0 +pytest==3.1.1 pytz==2017.2 requests==2.17.3 six==1.10.0 From 88d46a724dda39532fb8a73f80d01da9c047f237 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 31 May 2017 22:06:08 +0200 Subject: [PATCH 081/143] [cli] adds python logging facility configuration. --- bonobo/commands/__init__.py | 16 ++++++-- bonobo/commands/init.py | 3 -- bonobo/commands/run.py | 20 ++++------ bonobo/commands/version.py | 8 ++-- bonobo/logging.py | 75 +++++++++++++++++++++++++++++++++++++ bonobo/settings.py | 11 ++++-- bonobo/util/iterators.py | 16 +++++++- 7 files changed, 122 insertions(+), 27 deletions(-) create mode 100644 bonobo/logging.py diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index feae672..59e6dfb 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -1,12 +1,13 @@ import argparse -import logging -from stevedore import ExtensionManager +from bonobo import logging, settings + +logger = logging.get_logger() def entrypoint(args=None): - logging.basicConfig() parser = argparse.ArgumentParser() + parser.add_argument('--debug', '-D', action='store_true') subparsers = parser.add_subparsers(dest='command') subparsers.required = True @@ -18,10 +19,17 @@ def entrypoint(args=None): parser = subparsers.add_parser(ext.name) commands[ext.name] = ext.plugin(parser) except Exception: - logging.exception('Error while loading command {}.'.format(ext.name)) + logger.exception('Error while loading command {}.'.format(ext.name)) + from stevedore import ExtensionManager mgr = ExtensionManager(namespace='bonobo.commands') mgr.map(register_extension) args = parser.parse_args(args).__dict__ + if args.pop('debug', False): + settings.DEBUG = True + settings.LOGGING_LEVEL = logging.DEBUG + logging.set_level(settings.LOGGING_LEVEL) + + logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) commands[args.pop('command')](**args) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index ad3c52a..81af38c 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,6 +1,3 @@ -import os - - def execute(name): try: from cookiecutter.main import cookiecutter diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 8bb7b10..b2e1bcc 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -1,10 +1,4 @@ -import importlib import os -import runpy - -import pip - -import bonobo DEFAULT_SERVICES_FILENAME = '_services.py' DEFAULT_SERVICES_ATTR = 'get_services' @@ -12,7 +6,6 @@ DEFAULT_SERVICES_ATTR = 'get_services' DEFAULT_GRAPH_FILENAME = '__main__.py' DEFAULT_GRAPH_ATTR = 'get_graph' - def get_default_services(filename, services=None): dirname = os.path.dirname(filename) services_filename = os.path.join(dirname, DEFAULT_SERVICES_FILENAME) @@ -33,7 +26,8 @@ def get_default_services(filename, services=None): def execute(filename, module, install=False, quiet=False, verbose=False): - from bonobo import settings + import runpy + from bonobo import Graph, run, settings if quiet: settings.QUIET = True @@ -44,6 +38,8 @@ def execute(filename, module, install=False, quiet=False, verbose=False): if filename: if os.path.isdir(filename): if install: + import importlib + import pip requirements = os.path.join(filename, 'requirements.txt') pip.main(['install', '-r', requirements]) # Some shenanigans to be sure everything is importable after this, especially .egg-link files which @@ -62,7 +58,7 @@ def execute(filename, module, install=False, quiet=False, verbose=False): else: raise RuntimeError('UNEXPECTED: argparse should not allow this.') - graphs = dict((k, v) for k, v in context.items() if isinstance(v, bonobo.Graph)) + graphs = dict((k, v) for k, v in context.items() if isinstance(v, Graph)) assert len(graphs) == 1, ( 'Having zero or more than one graph definition in one file is unsupported for now, ' @@ -73,7 +69,7 @@ def execute(filename, module, install=False, quiet=False, verbose=False): # todo if console and not quiet, then add the console plugin # todo when better console plugin, add it if console and just disable display - return bonobo.run( + return run( graph, plugins=[], services=get_default_services( @@ -82,8 +78,8 @@ def execute(filename, module, install=False, quiet=False, verbose=False): ) -def register_generic_run_arguments(parser): - source_group = parser.add_mutually_exclusive_group(required=True) +def register_generic_run_arguments(parser, required=True): + source_group = parser.add_mutually_exclusive_group(required=required) source_group.add_argument('filename', nargs='?', type=str) source_group.add_argument('--module', '-m', type=str) return parser diff --git a/bonobo/commands/version.py b/bonobo/commands/version.py index bfa03a7..6d4f3e7 100644 --- a/bonobo/commands/version.py +++ b/bonobo/commands/version.py @@ -1,8 +1,5 @@ -import bonobo -from bonobo.util.pkgs import bonobo_packages - - def format_version(mod, *, name=None, quiet=False): + from bonobo.util.pkgs import bonobo_packages args = { 'name': name or mod.__name__, 'version': mod.__version__, @@ -20,6 +17,9 @@ def format_version(mod, *, name=None, quiet=False): def execute(all=False, quiet=False): + import bonobo + from bonobo.util.pkgs import bonobo_packages + print(format_version(bonobo, quiet=quiet)) if all: for name in sorted(bonobo_packages): diff --git a/bonobo/logging.py b/bonobo/logging.py new file mode 100644 index 0000000..1561089 --- /dev/null +++ b/bonobo/logging.py @@ -0,0 +1,75 @@ +import logging +import sys +import textwrap +from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING + +from colorama import Fore, Style + +from bonobo import settings +from bonobo.util.term import CLEAR_EOL + + +def get_format(): + yield '{b}[%(fg)s%(levelname)s{b}][{w}' + yield '{b}][{w}'.join(('%(spent)04d', '%(name)s')) + yield '{b}]' + yield ' %(fg)s%(message)s{r}' + yield CLEAR_EOL + + +colors = { + 'b': Fore.BLACK, + 'w': Fore.LIGHTBLACK_EX, + 'r': Style.RESET_ALL, +} +format = (''.join(get_format())).format(**colors) + + +class Filter(logging.Filter): + def filter(self, record): + record.spent = record.relativeCreated // 1000 + if record.levelname == 'DEBG': + record.fg = Fore.LIGHTBLACK_EX + elif record.levelname == 'INFO': + record.fg = Fore.LIGHTWHITE_EX + elif record.levelname == 'WARN': + record.fg = Fore.LIGHTYELLOW_EX + elif record.levelname == 'ERR ': + record.fg = Fore.LIGHTRED_EX + elif record.levelname == 'CRIT': + record.fg = Fore.RED + else: + record.fg = Fore.LIGHTWHITE_EX + return True + + +class Formatter(logging.Formatter): + def formatException(self, ei): + tb = super().formatException(ei) + return textwrap.indent(tb, Fore.BLACK + ' | ' + Fore.WHITE) + + +def setup(level): + logging.addLevelName(DEBUG, 'DEBG') + logging.addLevelName(INFO, 'INFO') + logging.addLevelName(WARNING, 'WARN') + logging.addLevelName(ERROR, 'ERR ') + logging.addLevelName(CRITICAL, 'CRIT') + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(Formatter(format)) + handler.addFilter(Filter()) + root = logging.getLogger() + root.addHandler(handler) + root.setLevel(level) + + +def set_level(level): + logging.getLogger().setLevel(level) + + +def get_logger(name='bonobo'): + return logging.getLogger(name) + + +# Setup formating and level. +setup(level=settings.LOGGING_LEVEL) diff --git a/bonobo/settings.py b/bonobo/settings.py index d956b2c..dda7ba7 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -1,5 +1,7 @@ import os +import logging + def to_bool(s): if len(s): @@ -10,13 +12,16 @@ def to_bool(s): # Debug/verbose mode. -DEBUG = to_bool(os.environ.get('BONOBO_DEBUG', 'f')) +DEBUG = to_bool(os.environ.get('DEBUG', 'f')) # Profile mode. -PROFILE = to_bool(os.environ.get('BONOBO_PROFILE', 'f')) +PROFILE = to_bool(os.environ.get('PROFILE', 'f')) # Quiet mode. -QUIET = to_bool(os.environ.get('BONOBO_QUIET', 'f')) +QUIET = to_bool(os.environ.get('QUIET', 'f')) + +# Logging level. +LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO def check(): diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py index 142b35a..ae39a49 100644 --- a/bonobo/util/iterators.py +++ b/bonobo/util/iterators.py @@ -1,4 +1,5 @@ """ Iterator utilities. """ +import functools def force_iterator(mixed): @@ -20,7 +21,20 @@ def force_iterator(mixed): def ensure_tuple(tuple_or_mixed): if isinstance(tuple_or_mixed, tuple): return tuple_or_mixed - return (tuple_or_mixed, ) + return (tuple_or_mixed,) + + +def tuplize(generator): + """ Takes a generator and make it a tuple-returning function. As a side + effect, it can also decorate any iterator-returning function to force + return value to be a tuple. + """ + + @functools.wraps(generator) + def tuplized(*args, **kwargs): + return tuple(generator(*args, **kwargs)) + + return tuplized def iter_if_not_sequence(mixed): From 84573cc8fa6a3f710482159ff20ae99719da3034 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 09:16:36 +0200 Subject: [PATCH 082/143] [deps] Update. --- Makefile | 2 +- requirements-dev.txt | 2 +- requirements-jupyter.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3544016..1cc54d4 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-31 22:08:42.384770 +# Updated at 2017-06-05 09:15:27.073880 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/requirements-dev.txt b/requirements-dev.txt index eb962d5..c499557 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,7 +16,7 @@ jinja2-time==0.2.0 jinja2==2.9.6 markupsafe==1.0 poyo==0.4.1 -py==1.4.33 +py==1.4.34 pygments==2.2.0 pytest-cov==2.5.1 pytest-timeout==1.2.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 1e98481..921057c 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -6,7 +6,7 @@ entrypoints==0.2.2 html5lib==0.999999999 ipykernel==4.6.1 ipython-genutils==0.2.0 -ipython==6.0.0 +ipython==6.1.0 ipywidgets==6.0.0 jedi==0.10.2 jinja2==2.9.6 From 471e38e67b09e09a04f200546f95550a5ab78d60 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 09:43:38 +0200 Subject: [PATCH 083/143] [jupyter] update widget so it uses str(...) instread of repr(...) and topological order of nodes. --- Makefile | 2 +- Projectfile | 2 +- bonobo/ext/jupyter/js/dist/index.js | 21 +- bonobo/ext/jupyter/js/dist/index.js.map | 2 +- bonobo/ext/jupyter/js/src/bonobo.js | 1 - bonobo/ext/jupyter/js/yarn.lock | 1441 +++++++++++++++++++++++ bonobo/ext/jupyter/plugin.py | 4 +- bonobo/ext/jupyter/static/extension.js | 4 +- bonobo/ext/jupyter/static/index.js | 21 +- bonobo/ext/jupyter/static/index.js.map | 2 +- bonobo/ext/jupyter/widget.py | 2 +- setup.py | 2 +- 12 files changed, 1472 insertions(+), 32 deletions(-) create mode 100644 bonobo/ext/jupyter/js/yarn.lock diff --git a/Makefile b/Makefile index 1cc54d4..472b625 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-05 09:15:27.073880 +# Updated at 2017-06-05 09:21:02.910936 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/Projectfile b/Projectfile index 46f262b..70eee1c 100644 --- a/Projectfile +++ b/Projectfile @@ -52,6 +52,6 @@ python.add_requirements( ], jupyter=[ 'jupyter >=1.0,<1.1', - 'ipywidgets >=6.0.0.beta5', + 'ipywidgets >=6.0.0,<7', ] ) diff --git a/bonobo/ext/jupyter/js/dist/index.js b/bonobo/ext/jupyter/js/dist/index.js index 9a10ba0..075db9f 100644 --- a/bonobo/ext/jupyter/js/dist/index.js +++ b/bonobo/ext/jupyter/js/dist/index.js @@ -42,7 +42,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return /************************************************************************/ /******/ ([ /* 0 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { // Entry point for the unpkg bundle containing custom model definitions. // @@ -55,14 +55,13 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return module.exports['version'] = __webpack_require__(4).version; -/***/ }, +/***/ }), /* 1 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { var widgets = __webpack_require__(2); var _ = __webpack_require__(3); - // Custom Model. Custom widgets models must at least provide default values // for model attributes, including `_model_name`, `_view_name`, `_model_module` // and `_view_module` when different from the base class. @@ -102,15 +101,15 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return }; -/***/ }, +/***/ }), /* 2 */ -/***/ function(module, exports) { +/***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_2__; -/***/ }, +/***/ }), /* 3 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Underscore.js 1.8.3 // http://underscorejs.org @@ -1662,9 +1661,9 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return }.call(this)); -/***/ }, +/***/ }), /* 4 */ -/***/ function(module, exports) { +/***/ (function(module, exports) { module.exports = { "name": "bonobo-jupyter", @@ -1696,6 +1695,6 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return } }; -/***/ } +/***/ }) /******/ ])});; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/bonobo/ext/jupyter/js/dist/index.js.map b/bonobo/ext/jupyter/js/dist/index.js.map index 3753533..664c7e0 100644 --- a/bonobo/ext/jupyter/js/dist/index.js.map +++ b/bonobo/ext/jupyter/js/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///webpack/bootstrap 53cc38ead473d1e34d74","webpack:///./src/embed.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACRA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACxCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"https://unpkg.com/jupyter-widget-example@0.0.1/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 53cc38ead473d1e34d74","// Entry point for the unpkg bundle containing custom model definitions.\n//\n// It differs from the notebook bundle in that it does not need to define a\n// dynamic baseURL for the static assets and may load some css that would\n// already be loaded by the notebook otherwise.\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/embed.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
    ')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///webpack/bootstrap 0804ef1bbb84581f3e1d","webpack:///./src/embed.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACRA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACvCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"https://unpkg.com/jupyter-widget-example@0.0.1/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 0804ef1bbb84581f3e1d","// Entry point for the unpkg bundle containing custom model definitions.\n//\n// It differs from the notebook bundle in that it does not need to define a\n// dynamic baseURL for the static assets and may load some css that would\n// already be loaded by the notebook otherwise.\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/embed.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
    ')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/ext/jupyter/js/src/bonobo.js b/bonobo/ext/jupyter/js/src/bonobo.js index 01822d7..7e75be2 100644 --- a/bonobo/ext/jupyter/js/src/bonobo.js +++ b/bonobo/ext/jupyter/js/src/bonobo.js @@ -1,7 +1,6 @@ var widgets = require('jupyter-js-widgets'); var _ = require('underscore'); - // Custom Model. Custom widgets models must at least provide default values // for model attributes, including `_model_name`, `_view_name`, `_model_module` // and `_view_module` when different from the base class. diff --git a/bonobo/ext/jupyter/js/yarn.lock b/bonobo/ext/jupyter/js/yarn.lock new file mode 100644 index 0000000..77c958a --- /dev/null +++ b/bonobo/ext/jupyter/js/yarn.lock @@ -0,0 +1,1441 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@jupyterlab/services@^0.35.0": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@jupyterlab/services/-/services-0.35.1.tgz#9bc0e940b381231cfd3033d8adbe90ca7cafdbf1" + dependencies: + "@types/minimist" "^1.1.29" + minimist "^1.2.0" + path-posix "^1.0.0" + phosphor "^0.7.0" + url "^0.11.0" + url-join "^1.1.0" + +"@types/backbone@^1.3.33": + version "1.3.33" + resolved "https://registry.yarnpkg.com/@types/backbone/-/backbone-1.3.33.tgz#33ab2e71619dd1d5adc477b689292fee7a34d4a3" + dependencies: + "@types/jquery" "*" + "@types/underscore" "*" + +"@types/jquery@*": + version "2.0.46" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.46.tgz#c245426299b43c4bb75f44b813090bd5918d00f2" + +"@types/minimist@^1.1.29": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + +"@types/semver@^5.3.30": + version "5.3.31" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.3.31.tgz#b999d7d935f43f5207b01b00d3de20852f4ca75f" + +"@types/underscore@*": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.8.1.tgz#bf5d680f24277284d4e42fed86bd86e5a59a44a4" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +acorn@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +ajv@^4.9.0, ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +aproba@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@^0.9.0: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + +async@^1.3.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +backbone@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.2.0.tgz#aba52f2a3caae117d871fa074eadfc9ed3193ee7" + dependencies: + underscore ">=1.7.0" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +base64-js@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" + +binary-extensions@^1.0.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +brace-expansion@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +browserify-aes@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-0.4.0.tgz#067149b668df31c4b58533e02d01e806d8608e2c" + dependencies: + inherits "^2.0.1" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +buffer@^4.9.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chokidar@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.3.0.tgz#b9fc75bb4a0ed61dcf1cd5dae96eb30c9c3e506c" + dependencies: + browserify-aes "0.4.0" + pbkdf2-compat "2.0.1" + ripemd160 "0.2.0" + sha.js "2.2.6" + +d3-format@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-0.5.1.tgz#9447d7c95c84b15d34c138975dbf0489a412e405" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@^2.2.0: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +enhanced-resolve@~0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.2.0" + tapable "^0.1.8" + +errno@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +font-awesome@^4.5.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.1.tgz#f19fd28f43eeaf761680e519a203c4d0b3d31aff" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.29" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +interpret@^0.6.4: + version "0.6.6" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.0.2, is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +jquery-ui@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51" + +jquery@^3.1.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-loader@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + dependencies: + assert-plus "1.0.0" + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +jupyter-js-widgets@^2.0.9: + version "2.1.4" + resolved "https://registry.yarnpkg.com/jupyter-js-widgets/-/jupyter-js-widgets-2.1.4.tgz#a2c8c19d706d79feb202e6dc3221bffc9d1d8edf" + dependencies: + "@jupyterlab/services" "^0.35.0" + "@types/backbone" "^1.3.33" + "@types/semver" "^5.3.30" + ajv "^4.9.0" + backbone "1.2.0" + d3-format "^0.5.1" + font-awesome "^4.5.0" + jquery "^3.1.1" + jquery-ui "^1.12.1" + jupyter-widgets-schema "^0.1.1" + lolex "^1.4.0" + phosphor "^0.7.0" + scriptjs "^2.5.8" + semver "^5.1.0" + underscore "^1.8.3" + +jupyter-widgets-schema@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jupyter-widgets-schema/-/jupyter-widgets-schema-0.1.1.tgz#72a81328191439383ec45496a4f8184d98c81b48" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +loader-utils@^0.2.11: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +lolex@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +memory-fs@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" + +memory-fs@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.3.0.tgz#7bcc6b629e3a43e871d7e29aca6ae8a7f15cbb20" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nan@^2.3.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +node-libs-browser@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-0.7.0.tgz#3e272c0819e308935e26674408d7af0e1491b83b" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.9.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "3.3.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.29: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npmlog@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-posix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" + +pbkdf2-compat@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +phosphor@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/phosphor/-/phosphor-0.7.1.tgz#fad12fe9568bc85e89c6d57326ba58d27a1d2b21" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +randomatic@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" + dependencies: + is-number "^2.0.2" + kind-of "^3.0.2" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.6: + version "2.2.10" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.10.tgz#effe72bb7c884c0dd335e2379d526196d9d011ee" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "^5.0.1" + string_decoder "~1.0.0" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +remove-trailing-separator@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +request@^2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +ripemd160@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce" + +safe-buffer@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" + +scriptjs@^2.5.8: + version "2.5.8" + resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.8.tgz#d0c43955c2e6bad33b6e4edf7b53b8965aa7ca5f" + +semver@^5.1.0, semver@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-list-map@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" + +source-map@~0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +sshpk@^1.7.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-http@^2.3.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.1.tgz#546a51741ad5a6b07e9e31b0b10441a917df528a" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string_decoder@^0.10.25: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.1.tgz#62e200f039955a6810d8df0a33ffc0f013662d98" + dependencies: + safe-buffer "^5.0.1" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^3.1.0: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +tapable@^0.1.8, tapable@~0.1.8: + version "0.1.10" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +timers-browserify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +uglify-js@~2.7.3: + version "2.7.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" + dependencies: + async "~0.2.6" + source-map "~0.5.1" + uglify-to-browserify "~1.0.0" + yargs "~3.10.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +underscore@>=1.7.0, underscore@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +url-join@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +watchpack@^0.2.1: + version "0.2.9" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-0.2.9.tgz#62eaa4ab5e5ba35fdfc018275626e3c0f5e3fb0b" + dependencies: + async "^0.9.0" + chokidar "^1.0.0" + graceful-fs "^4.1.2" + +webpack-core@~0.6.9: + version "0.6.9" + resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2" + dependencies: + source-list-map "~0.1.7" + source-map "~0.4.1" + +webpack@^1.12.14: + version "1.15.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-1.15.0.tgz#4ff31f53db03339e55164a9d468ee0324968fe98" + dependencies: + acorn "^3.0.0" + async "^1.3.0" + clone "^1.0.2" + enhanced-resolve "~0.9.0" + interpret "^0.6.4" + loader-utils "^0.2.11" + memory-fs "~0.3.0" + mkdirp "~0.5.0" + node-libs-browser "^0.7.0" + optimist "~0.6.0" + supports-color "^3.1.0" + tapable "~0.1.8" + uglify-js "~2.7.3" + watchpack "^0.2.1" + webpack-core "~0.6.9" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/bonobo/ext/jupyter/plugin.py b/bonobo/ext/jupyter/plugin.py index a418c83..a72141c 100644 --- a/bonobo/ext/jupyter/plugin.py +++ b/bonobo/ext/jupyter/plugin.py @@ -19,6 +19,8 @@ class JupyterOutputPlugin(Plugin): IPython.core.display.display(self.widget) def run(self): - self.widget.value = [repr(node) for node in self.context.parent.nodes] + self.widget.value = [ + str(self.context.parent[i]) for i in self.context.parent.graph.topologically_sorted_indexes + ] finalize = run diff --git a/bonobo/ext/jupyter/static/extension.js b/bonobo/ext/jupyter/static/extension.js index 12a268d..fee5e53 100644 --- a/bonobo/ext/jupyter/static/extension.js +++ b/bonobo/ext/jupyter/static/extension.js @@ -42,7 +42,7 @@ define(function() { return /******/ (function(modules) { // webpackBootstrap /************************************************************************/ /******/ ([ /* 0 */ -/***/ function(module, exports) { +/***/ (function(module, exports) { // This file contains the javascript that is run when the notebook is loaded. // It contains some requirejs configuration and the `load_ipython_extension` @@ -66,5 +66,5 @@ define(function() { return /******/ (function(modules) { // webpackBootstrap }; -/***/ } +/***/ }) /******/ ])});; \ No newline at end of file diff --git a/bonobo/ext/jupyter/static/index.js b/bonobo/ext/jupyter/static/index.js index a448a06..75e4b99 100644 --- a/bonobo/ext/jupyter/static/index.js +++ b/bonobo/ext/jupyter/static/index.js @@ -42,7 +42,7 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return /************************************************************************/ /******/ ([ /* 0 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { // Entry point for the notebook bundle containing custom model definitions. // @@ -58,14 +58,13 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return module.exports['version'] = __webpack_require__(4).version; -/***/ }, +/***/ }), /* 1 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { var widgets = __webpack_require__(2); var _ = __webpack_require__(3); - // Custom Model. Custom widgets models must at least provide default values // for model attributes, including `_model_name`, `_view_name`, `_model_module` // and `_view_module` when different from the base class. @@ -105,15 +104,15 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return }; -/***/ }, +/***/ }), /* 2 */ -/***/ function(module, exports) { +/***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_2__; -/***/ }, +/***/ }), /* 3 */ -/***/ function(module, exports, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Underscore.js 1.8.3 // http://underscorejs.org @@ -1665,9 +1664,9 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return }.call(this)); -/***/ }, +/***/ }), /* 4 */ -/***/ function(module, exports) { +/***/ (function(module, exports) { module.exports = { "name": "bonobo-jupyter", @@ -1699,6 +1698,6 @@ define(["jupyter-js-widgets"], function(__WEBPACK_EXTERNAL_MODULE_2__) { return } }; -/***/ } +/***/ }) /******/ ])});; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/bonobo/ext/jupyter/static/index.js.map b/bonobo/ext/jupyter/static/index.js.map index fa3bcd4..eec5f06 100644 --- a/bonobo/ext/jupyter/static/index.js.map +++ b/bonobo/ext/jupyter/static/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///webpack/bootstrap 6e8b27d93b0c1b519993","webpack:///./src/index.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACXA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACxCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 6e8b27d93b0c1b519993","// Entry point for the notebook bundle containing custom model definitions.\n//\n// Setup notebook base URL\n//\n// Some static assets may be required by the custom widget javascript. The base\n// url for the notebook is not known at build time and is therefore computed\n// dynamically.\n__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/bonobo/';\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
    ')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///webpack/bootstrap f7d5605306ad4cac6219","webpack:///./src/index.js","webpack:///./src/bonobo.js","webpack:///external \"jupyter-js-widgets\"","webpack:///./~/underscore/underscore.js","webpack:///./package.json"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACXA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;AACL,EAAC;;;AAGD;AACA;AACA;AACA;;;;;;;ACvCA,gD;;;;;;ACAA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA,wBAAuB,OAAO;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAsC,YAAY;AAClD;AACA;AACA,MAAK;AACL;AACA,wCAAuC,YAAY;AACnD;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,YAAY;AACtD;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA6B,gBAAgB;AAC7C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA,qDAAoD;AACpD,IAAG;;AAEH;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA,2CAA0C;AAC1C,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,6DAA4D,YAAY;AACxE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,+CAA8C,YAAY;AAC1D;AACA;AACA,sBAAqB,gBAAgB;AACrC;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA,wBAAuB,gBAAgB;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,8CAA6C,YAAY;AACzD;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,aAAY,8BAA8B;AAC1C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAsD;AACtD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAS;AACT;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA0C,0BAA0B;AACpE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,sBAAqB,cAAc;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,YAAY;AAC3B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAO,eAAe;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAqB,eAAe;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAsB;AACtB;AACA,0BAAyB,gBAAgB;AACzC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;;AAEA;AACA;AACA,oBAAmB;AACnB;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,6CAA4C,mBAAmB;AAC/D;AACA;AACA,0CAAyC,YAAY;AACrD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAqD;AACrD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,oBAAmB,YAAY;AAC/B;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8EAA6E;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA,sCAAqC;AACrC;AACA;AACA;;AAEA;AACA;AACA;AACA,2BAA0B;AAC1B;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,oBAAmB,OAAO;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAe;AACf,eAAc;AACd,eAAc;AACd,iBAAgB;AAChB,iBAAgB;AAChB,iBAAgB;AAChB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,0BAAyB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA4B;;AAE5B;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,QAAO;AACP;AACA,QAAO;AACP,sBAAqB;AACrB;;AAEA;AACA;AACA,MAAK;AACL,kBAAiB;;AAEjB;AACA,mDAAkD,EAAE,iBAAiB;;AAErE;AACA,yBAAwB,8BAA8B;AACtD,4BAA2B;;AAE3B;AACA;AACA,MAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,mDAAkD,iBAAiB;;AAEnE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,IAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAK;AACL;AACA,EAAC;;;;;;;AC3gDD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA,GAAE;AACF;AACA;AACA;AACA;AACA,G","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap f7d5605306ad4cac6219","// Entry point for the notebook bundle containing custom model definitions.\n//\n// Setup notebook base URL\n//\n// Some static assets may be required by the custom widget javascript. The base\n// url for the notebook is not known at build time and is therefore computed\n// dynamically.\n__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/bonobo/';\n\n// Export widget models and views, and the npm package version number.\nmodule.exports = require('./bonobo.js');\nmodule.exports['version'] = require('../package.json').version;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0","var widgets = require('jupyter-js-widgets');\nvar _ = require('underscore');\n\n// Custom Model. Custom widgets models must at least provide default values\n// for model attributes, including `_model_name`, `_view_name`, `_model_module`\n// and `_view_module` when different from the base class.\n//\n// When serialiazing entire widget state for embedding, only values different from the\n// defaults will be specified.\n\nvar BonoboModel = widgets.DOMWidgetModel.extend({\n defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, {\n _model_name: 'BonoboModel',\n _view_name: 'BonoboView',\n _model_module: 'bonobo',\n _view_module: 'bonobo',\n value: []\n })\n});\n\n\n// Custom View. Renders the widget model.\nvar BonoboView = widgets.DOMWidgetView.extend({\n render: function () {\n this.value_changed();\n this.model.on('change:value', this.value_changed, this);\n },\n\n value_changed: function () {\n this.$el.html(\n this.model.get('value').join('
    ')\n );\n },\n});\n\n\nmodule.exports = {\n BonoboModel: BonoboModel,\n BonoboView: BonoboView\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/bonobo.js\n// module id = 1\n// module chunks = 0","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jupyter-js-widgets\"\n// module id = 2\n// module chunks = 0","// Underscore.js 1.8.3\n// http://underscorejs.org\n// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `exports` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var\n push = ArrayProto.push,\n slice = ArrayProto.slice,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind,\n nativeCreate = Object.create;\n\n // Naked function reference for surrogate-prototype-swapping.\n var Ctor = function(){};\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.8.3';\n\n // Internal function that returns an efficient (for current engines) version\n // of the passed-in callback, to be repeatedly applied in other Underscore\n // functions.\n var optimizeCb = function(func, context, argCount) {\n if (context === void 0) return func;\n switch (argCount == null ? 3 : argCount) {\n case 1: return function(value) {\n return func.call(context, value);\n };\n case 2: return function(value, other) {\n return func.call(context, value, other);\n };\n case 3: return function(value, index, collection) {\n return func.call(context, value, index, collection);\n };\n case 4: return function(accumulator, value, index, collection) {\n return func.call(context, accumulator, value, index, collection);\n };\n }\n return function() {\n return func.apply(context, arguments);\n };\n };\n\n // A mostly-internal function to generate callbacks that can be applied\n // to each element in a collection, returning the desired result — either\n // identity, an arbitrary callback, a property matcher, or a property accessor.\n var cb = function(value, context, argCount) {\n if (value == null) return _.identity;\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\n if (_.isObject(value)) return _.matcher(value);\n return _.property(value);\n };\n _.iteratee = function(value, context) {\n return cb(value, context, Infinity);\n };\n\n // An internal function for creating assigner functions.\n var createAssigner = function(keysFunc, undefinedOnly) {\n return function(obj) {\n var length = arguments.length;\n if (length < 2 || obj == null) return obj;\n for (var index = 1; index < length; index++) {\n var source = arguments[index],\n keys = keysFunc(source),\n l = keys.length;\n for (var i = 0; i < l; i++) {\n var key = keys[i];\n if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];\n }\n }\n return obj;\n };\n };\n\n // An internal function for creating a new object that inherits from another.\n var baseCreate = function(prototype) {\n if (!_.isObject(prototype)) return {};\n if (nativeCreate) return nativeCreate(prototype);\n Ctor.prototype = prototype;\n var result = new Ctor;\n Ctor.prototype = null;\n return result;\n };\n\n var property = function(key) {\n return function(obj) {\n return obj == null ? void 0 : obj[key];\n };\n };\n\n // Helper for collection methods to determine whether a collection\n // should be iterated as an array or as an object\n // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength\n // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094\n var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;\n var getLength = property('length');\n var isArrayLike = function(collection) {\n var length = getLength(collection);\n return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;\n };\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles raw objects in addition to array-likes. Treats all\n // sparse array-likes as if they were dense.\n _.each = _.forEach = function(obj, iteratee, context) {\n iteratee = optimizeCb(iteratee, context);\n var i, length;\n if (isArrayLike(obj)) {\n for (i = 0, length = obj.length; i < length; i++) {\n iteratee(obj[i], i, obj);\n }\n } else {\n var keys = _.keys(obj);\n for (i = 0, length = keys.length; i < length; i++) {\n iteratee(obj[keys[i]], keys[i], obj);\n }\n }\n return obj;\n };\n\n // Return the results of applying the iteratee to each element.\n _.map = _.collect = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n results = Array(length);\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n results[index] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Create a reducing function iterating left or right.\n function createReduce(dir) {\n // Optimized iterator function as using arguments.length\n // in the main function will deoptimize the, see #1991.\n function iterator(obj, iteratee, memo, keys, index, length) {\n for (; index >= 0 && index < length; index += dir) {\n var currentKey = keys ? keys[index] : index;\n memo = iteratee(memo, obj[currentKey], currentKey, obj);\n }\n return memo;\n }\n\n return function(obj, iteratee, memo, context) {\n iteratee = optimizeCb(iteratee, context, 4);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length,\n index = dir > 0 ? 0 : length - 1;\n // Determine the initial value if none is provided.\n if (arguments.length < 3) {\n memo = obj[keys ? keys[index] : index];\n index += dir;\n }\n return iterator(obj, iteratee, memo, keys, index, length);\n };\n }\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`.\n _.reduce = _.foldl = _.inject = createReduce(1);\n\n // The right-associative version of reduce, also known as `foldr`.\n _.reduceRight = _.foldr = createReduce(-1);\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, predicate, context) {\n var key;\n if (isArrayLike(obj)) {\n key = _.findIndex(obj, predicate, context);\n } else {\n key = _.findKey(obj, predicate, context);\n }\n if (key !== void 0 && key !== -1) return obj[key];\n };\n\n // Return all the elements that pass a truth test.\n // Aliased as `select`.\n _.filter = _.select = function(obj, predicate, context) {\n var results = [];\n predicate = cb(predicate, context);\n _.each(obj, function(value, index, list) {\n if (predicate(value, index, list)) results.push(value);\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, predicate, context) {\n return _.filter(obj, _.negate(cb(predicate)), context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Aliased as `all`.\n _.every = _.all = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (!predicate(obj[currentKey], currentKey, obj)) return false;\n }\n return true;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Aliased as `any`.\n _.some = _.any = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = !isArrayLike(obj) && _.keys(obj),\n length = (keys || obj).length;\n for (var index = 0; index < length; index++) {\n var currentKey = keys ? keys[index] : index;\n if (predicate(obj[currentKey], currentKey, obj)) return true;\n }\n return false;\n };\n\n // Determine if the array or object contains a given item (using `===`).\n // Aliased as `includes` and `include`.\n _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n if (typeof fromIndex != 'number' || guard) fromIndex = 0;\n return _.indexOf(obj, item, fromIndex) >= 0;\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n var func = isFunc ? method : value[method];\n return func == null ? func : func.apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, _.property(key));\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs) {\n return _.filter(obj, _.matcher(attrs));\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.find(obj, _.matcher(attrs));\n };\n\n // Return the maximum element (or element-based computation).\n _.max = function(obj, iteratee, context) {\n var result = -Infinity, lastComputed = -Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value > result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed > lastComputed || computed === -Infinity && result === -Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iteratee, context) {\n var result = Infinity, lastComputed = Infinity,\n value, computed;\n if (iteratee == null && obj != null) {\n obj = isArrayLike(obj) ? obj : _.values(obj);\n for (var i = 0, length = obj.length; i < length; i++) {\n value = obj[i];\n if (value < result) {\n result = value;\n }\n }\n } else {\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index, list) {\n computed = iteratee(value, index, list);\n if (computed < lastComputed || computed === Infinity && result === Infinity) {\n result = value;\n lastComputed = computed;\n }\n });\n }\n return result;\n };\n\n // Shuffle a collection, using the modern version of the\n // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).\n _.shuffle = function(obj) {\n var set = isArrayLike(obj) ? obj : _.values(obj);\n var length = set.length;\n var shuffled = Array(length);\n for (var index = 0, rand; index < length; index++) {\n rand = _.random(0, index);\n if (rand !== index) shuffled[index] = shuffled[rand];\n shuffled[rand] = set[index];\n }\n return shuffled;\n };\n\n // Sample **n** random values from a collection.\n // If **n** is not specified, returns a single random element.\n // The internal `guard` argument allows it to work with `map`.\n _.sample = function(obj, n, guard) {\n if (n == null || guard) {\n if (!isArrayLike(obj)) obj = _.values(obj);\n return obj[_.random(obj.length - 1)];\n }\n return _.shuffle(obj).slice(0, Math.max(0, n));\n };\n\n // Sort the object's values by a criterion produced by an iteratee.\n _.sortBy = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value: value,\n index: index,\n criteria: iteratee(value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index - right.index;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(behavior) {\n return function(obj, iteratee, context) {\n var result = {};\n iteratee = cb(iteratee, context);\n _.each(obj, function(value, index) {\n var key = iteratee(value, index, obj);\n behavior(result, value, key);\n });\n return result;\n };\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key].push(value); else result[key] = [value];\n });\n\n // Indexes the object's values by a criterion, similar to `groupBy`, but for\n // when you know that your index values will be unique.\n _.indexBy = group(function(result, value, key) {\n result[key] = value;\n });\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = group(function(result, value, key) {\n if (_.has(result, key)) result[key]++; else result[key] = 1;\n });\n\n // Safely create a real, live array from anything iterable.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (isArrayLike(obj)) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return isArrayLike(obj) ? obj.length : _.keys(obj).length;\n };\n\n // Split a collection into two arrays: one whose elements all satisfy the given\n // predicate, and one whose elements all do not satisfy the predicate.\n _.partition = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var pass = [], fail = [];\n _.each(obj, function(value, key, obj) {\n (predicate(value, key, obj) ? pass : fail).push(value);\n });\n return [pass, fail];\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[0];\n return _.initial(array, array.length - n);\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if (n == null || guard) return array[array.length - 1];\n return _.rest(array, Math.max(0, array.length - n));\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, n == null || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, strict, startIndex) {\n var output = [], idx = 0;\n for (var i = startIndex || 0, length = getLength(input); i < length; i++) {\n var value = input[i];\n if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {\n //flatten current level of array or arguments object\n if (!shallow) value = flatten(value, shallow, strict);\n var j = 0, len = value.length;\n output.length += len;\n while (j < len) {\n output[idx++] = value[j++];\n }\n } else if (!strict) {\n output[idx++] = value;\n }\n }\n return output;\n };\n\n // Flatten out an array, either recursively (by default), or just one level.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, false);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iteratee, context) {\n if (!_.isBoolean(isSorted)) {\n context = iteratee;\n iteratee = isSorted;\n isSorted = false;\n }\n if (iteratee != null) iteratee = cb(iteratee, context);\n var result = [];\n var seen = [];\n for (var i = 0, length = getLength(array); i < length; i++) {\n var value = array[i],\n computed = iteratee ? iteratee(value, i, array) : value;\n if (isSorted) {\n if (!i || seen !== computed) result.push(value);\n seen = computed;\n } else if (iteratee) {\n if (!_.contains(seen, computed)) {\n seen.push(computed);\n result.push(value);\n }\n } else if (!_.contains(result, value)) {\n result.push(value);\n }\n }\n return result;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(flatten(arguments, true, true));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var result = [];\n var argsLength = arguments.length;\n for (var i = 0, length = getLength(array); i < length; i++) {\n var item = array[i];\n if (_.contains(result, item)) continue;\n for (var j = 1; j < argsLength; j++) {\n if (!_.contains(arguments[j], item)) break;\n }\n if (j === argsLength) result.push(item);\n }\n return result;\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = flatten(arguments, true, true, 1);\n return _.filter(array, function(value){\n return !_.contains(rest, value);\n });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n return _.unzip(arguments);\n };\n\n // Complement of _.zip. Unzip accepts an array of arrays and groups\n // each array's elements on shared indices\n _.unzip = function(array) {\n var length = array && _.max(array, getLength).length || 0;\n var result = Array(length);\n\n for (var index = 0; index < length; index++) {\n result[index] = _.pluck(array, index);\n }\n return result;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n var result = {};\n for (var i = 0, length = getLength(list); i < length; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // Generator function to create the findIndex and findLastIndex functions\n function createPredicateIndexFinder(dir) {\n return function(array, predicate, context) {\n predicate = cb(predicate, context);\n var length = getLength(array);\n var index = dir > 0 ? 0 : length - 1;\n for (; index >= 0 && index < length; index += dir) {\n if (predicate(array[index], index, array)) return index;\n }\n return -1;\n };\n }\n\n // Returns the first index on an array-like that passes a predicate test\n _.findIndex = createPredicateIndexFinder(1);\n _.findLastIndex = createPredicateIndexFinder(-1);\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iteratee, context) {\n iteratee = cb(iteratee, context, 1);\n var value = iteratee(obj);\n var low = 0, high = getLength(array);\n while (low < high) {\n var mid = Math.floor((low + high) / 2);\n if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;\n }\n return low;\n };\n\n // Generator function to create the indexOf and lastIndexOf functions\n function createIndexFinder(dir, predicateFind, sortedIndex) {\n return function(array, item, idx) {\n var i = 0, length = getLength(array);\n if (typeof idx == 'number') {\n if (dir > 0) {\n i = idx >= 0 ? idx : Math.max(idx + length, i);\n } else {\n length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;\n }\n } else if (sortedIndex && idx && length) {\n idx = sortedIndex(array, item);\n return array[idx] === item ? idx : -1;\n }\n if (item !== item) {\n idx = predicateFind(slice.call(array, i, length), _.isNaN);\n return idx >= 0 ? idx + i : -1;\n }\n for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {\n if (array[idx] === item) return idx;\n }\n return -1;\n };\n }\n\n // Return the position of the first occurrence of an item in an array,\n // or -1 if the item is not included in the array.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);\n _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (stop == null) {\n stop = start || 0;\n start = 0;\n }\n step = step || 1;\n\n var length = Math.max(Math.ceil((stop - start) / step), 0);\n var range = Array(length);\n\n for (var idx = 0; idx < length; idx++, start += step) {\n range[idx] = start;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Determines whether to execute a function as a constructor\n // or a normal function with the provided arguments\n var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {\n if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);\n var self = baseCreate(sourceFunc.prototype);\n var result = sourceFunc.apply(self, args);\n if (_.isObject(result)) return result;\n return self;\n };\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');\n var args = slice.call(arguments, 2);\n var bound = function() {\n return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));\n };\n return bound;\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context. _ acts\n // as a placeholder, allowing any combination of arguments to be pre-filled.\n _.partial = function(func) {\n var boundArgs = slice.call(arguments, 1);\n var bound = function() {\n var position = 0, length = boundArgs.length;\n var args = Array(length);\n for (var i = 0; i < length; i++) {\n args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];\n }\n while (position < arguments.length) args.push(arguments[position++]);\n return executeBound(func, bound, this, this, args);\n };\n return bound;\n };\n\n // Bind a number of an object's methods to that object. Remaining arguments\n // are the method names to be bound. Useful for ensuring that all callbacks\n // defined on an object belong to it.\n _.bindAll = function(obj) {\n var i, length = arguments.length, key;\n if (length <= 1) throw new Error('bindAll must be passed function names');\n for (i = 1; i < length; i++) {\n key = arguments[i];\n obj[key] = _.bind(obj[key], obj);\n }\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memoize = function(key) {\n var cache = memoize.cache;\n var address = '' + (hasher ? hasher.apply(this, arguments) : key);\n if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);\n return cache[address];\n };\n memoize.cache = {};\n return memoize;\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){\n return func.apply(null, args);\n }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = _.partial(_.delay, _, 1);\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time. Normally, the throttled function will run\n // as much as it can, without ever going more than once per `wait` duration;\n // but if you'd like to disable the execution on the leading edge, pass\n // `{leading: false}`. To disable execution on the trailing edge, ditto.\n _.throttle = function(func, wait, options) {\n var context, args, result;\n var timeout = null;\n var previous = 0;\n if (!options) options = {};\n var later = function() {\n previous = options.leading === false ? 0 : _.now();\n timeout = null;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n };\n return function() {\n var now = _.now();\n if (!previous && options.leading === false) previous = now;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, args, context, timestamp, result;\n\n var later = function() {\n var last = _.now() - timestamp;\n\n if (last < wait && last >= 0) {\n timeout = setTimeout(later, wait - last);\n } else {\n timeout = null;\n if (!immediate) {\n result = func.apply(context, args);\n if (!timeout) context = args = null;\n }\n }\n };\n\n return function() {\n context = this;\n args = arguments;\n timestamp = _.now();\n var callNow = immediate && !timeout;\n if (!timeout) timeout = setTimeout(later, wait);\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n }\n\n return result;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return _.partial(wrapper, func);\n };\n\n // Returns a negated version of the passed-in predicate.\n _.negate = function(predicate) {\n return function() {\n return !predicate.apply(this, arguments);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var args = arguments;\n var start = args.length - 1;\n return function() {\n var i = start;\n var result = args[start].apply(this, arguments);\n while (i--) result = args[i].call(this, result);\n return result;\n };\n };\n\n // Returns a function that will only be executed on and after the Nth call.\n _.after = function(times, func) {\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Returns a function that will only be executed up to (but not including) the Nth call.\n _.before = function(times, func) {\n var memo;\n return function() {\n if (--times > 0) {\n memo = func.apply(this, arguments);\n }\n if (times <= 1) func = null;\n return memo;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = _.partial(_.before, 2);\n\n // Object Functions\n // ----------------\n\n // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.\n var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');\n var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',\n 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];\n\n function collectNonEnumProps(obj, keys) {\n var nonEnumIdx = nonEnumerableProps.length;\n var constructor = obj.constructor;\n var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;\n\n // Constructor is a special case.\n var prop = 'constructor';\n if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);\n\n while (nonEnumIdx--) {\n prop = nonEnumerableProps[nonEnumIdx];\n if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {\n keys.push(prop);\n }\n }\n }\n\n // Retrieve the names of an object's own properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = function(obj) {\n if (!_.isObject(obj)) return [];\n if (nativeKeys) return nativeKeys(obj);\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve all the property names of an object.\n _.allKeys = function(obj) {\n if (!_.isObject(obj)) return [];\n var keys = [];\n for (var key in obj) keys.push(key);\n // Ahem, IE < 9.\n if (hasEnumBug) collectNonEnumProps(obj, keys);\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var values = Array(length);\n for (var i = 0; i < length; i++) {\n values[i] = obj[keys[i]];\n }\n return values;\n };\n\n // Returns the results of applying the iteratee to each element of the object\n // In contrast to _.map it returns an object\n _.mapObject = function(obj, iteratee, context) {\n iteratee = cb(iteratee, context);\n var keys = _.keys(obj),\n length = keys.length,\n results = {},\n currentKey;\n for (var index = 0; index < length; index++) {\n currentKey = keys[index];\n results[currentKey] = iteratee(obj[currentKey], currentKey, obj);\n }\n return results;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var keys = _.keys(obj);\n var length = keys.length;\n var pairs = Array(length);\n for (var i = 0; i < length; i++) {\n pairs[i] = [keys[i], obj[keys[i]]];\n }\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n var keys = _.keys(obj);\n for (var i = 0, length = keys.length; i < length; i++) {\n result[obj[keys[i]]] = keys[i];\n }\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = createAssigner(_.allKeys);\n\n // Assigns a given object with all the own properties in the passed-in object(s)\n // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)\n _.extendOwn = _.assign = createAssigner(_.keys);\n\n // Returns the first key on an object that passes a predicate test\n _.findKey = function(obj, predicate, context) {\n predicate = cb(predicate, context);\n var keys = _.keys(obj), key;\n for (var i = 0, length = keys.length; i < length; i++) {\n key = keys[i];\n if (predicate(obj[key], key, obj)) return key;\n }\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(object, oiteratee, context) {\n var result = {}, obj = object, iteratee, keys;\n if (obj == null) return result;\n if (_.isFunction(oiteratee)) {\n keys = _.allKeys(obj);\n iteratee = optimizeCb(oiteratee, context);\n } else {\n keys = flatten(arguments, false, false, 1);\n iteratee = function(value, key, obj) { return key in obj; };\n obj = Object(obj);\n }\n for (var i = 0, length = keys.length; i < length; i++) {\n var key = keys[i];\n var value = obj[key];\n if (iteratee(value, key, obj)) result[key] = value;\n }\n return result;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj, iteratee, context) {\n if (_.isFunction(iteratee)) {\n iteratee = _.negate(iteratee);\n } else {\n var keys = _.map(flatten(arguments, false, false, 1), String);\n iteratee = function(value, key) {\n return !_.contains(keys, key);\n };\n }\n return _.pick(obj, iteratee, context);\n };\n\n // Fill in a given object with default properties.\n _.defaults = createAssigner(_.allKeys, true);\n\n // Creates an object that inherits from the given prototype object.\n // If additional properties are provided then they will be added to the\n // created object.\n _.create = function(prototype, props) {\n var result = baseCreate(prototype);\n if (props) _.extendOwn(result, props);\n return result;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Returns whether an object has a given set of `key:value` pairs.\n _.isMatch = function(object, attrs) {\n var keys = _.keys(attrs), length = keys.length;\n if (object == null) return !length;\n var obj = Object(object);\n for (var i = 0; i < length; i++) {\n var key = keys[i];\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\n }\n return true;\n };\n\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n if (a === b) return a !== 0 || 1 / a === 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className !== toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, regular expressions, dates, and booleans are compared by value.\n case '[object RegExp]':\n // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return '' + a === '' + b;\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive.\n // Object(NaN) is equivalent to NaN\n if (+a !== +a) return +b !== +b;\n // An `egal` comparison is performed for other numeric values.\n return +a === 0 ? 1 / +a === 1 / b : +a === +b;\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a === +b;\n }\n\n var areArrays = className === '[object Array]';\n if (!areArrays) {\n if (typeof a != 'object' || typeof b != 'object') return false;\n\n // Objects with different constructors are not equivalent, but `Object`s or `Array`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n _.isFunction(bCtor) && bCtor instanceof bCtor)\n && ('constructor' in a && 'constructor' in b)) {\n return false;\n }\n }\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n // Initializing stack of traversed objects.\n // It's done here since we only need them for objects and arrays comparison.\n aStack = aStack || [];\n bStack = bStack || [];\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] === a) return bStack[length] === b;\n }\n\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n\n // Recursively compare objects and arrays.\n if (areArrays) {\n // Compare array lengths to determine if a deep comparison is necessary.\n length = a.length;\n if (length !== b.length) return false;\n // Deep compare the contents, ignoring non-numeric properties.\n while (length--) {\n if (!eq(a[length], b[length], aStack, bStack)) return false;\n }\n } else {\n // Deep compare objects.\n var keys = _.keys(a), key;\n length = keys.length;\n // Ensure that both objects contain the same number of properties before comparing deep equality.\n if (_.keys(b).length !== length) return false;\n while (length--) {\n // Deep compare each member\n key = keys[length];\n if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return true;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;\n return _.keys(obj).length === 0;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) === '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n var type = typeof obj;\n return type === 'function' || type === 'object' && !!obj;\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.\n _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) === '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE < 9), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return _.has(obj, 'callee');\n };\n }\n\n // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,\n // IE 11 (#1621), and in Safari 8 (#1929).\n if (typeof /./ != 'function' && typeof Int8Array != 'object') {\n _.isFunction = function(obj) {\n return typeof obj == 'function' || false;\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj !== +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) === '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return obj != null && hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iteratees.\n _.identity = function(value) {\n return value;\n };\n\n // Predicate-generating functions. Often useful outside of Underscore.\n _.constant = function(value) {\n return function() {\n return value;\n };\n };\n\n _.noop = function(){};\n\n _.property = property;\n\n // Generates a function for a given object that returns a given property.\n _.propertyOf = function(obj) {\n return obj == null ? function(){} : function(key) {\n return obj[key];\n };\n };\n\n // Returns a predicate for checking whether an object has a given set of\n // `key:value` pairs.\n _.matcher = _.matches = function(attrs) {\n attrs = _.extendOwn({}, attrs);\n return function(obj) {\n return _.isMatch(obj, attrs);\n };\n };\n\n // Run a function **n** times.\n _.times = function(n, iteratee, context) {\n var accum = Array(Math.max(0, n));\n iteratee = optimizeCb(iteratee, context, 1);\n for (var i = 0; i < n; i++) accum[i] = iteratee(i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // A (possibly faster) way to get the current timestamp as an integer.\n _.now = Date.now || function() {\n return new Date().getTime();\n };\n\n // List of HTML entities for escaping.\n var escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`'\n };\n var unescapeMap = _.invert(escapeMap);\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n var createEscaper = function(map) {\n var escaper = function(match) {\n return map[match];\n };\n // Regexes for identifying a key that needs to be escaped\n var source = '(?:' + _.keys(map).join('|') + ')';\n var testRegexp = RegExp(source);\n var replaceRegexp = RegExp(source, 'g');\n return function(string) {\n string = string == null ? '' : '' + string;\n return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n };\n };\n _.escape = createEscaper(escapeMap);\n _.unescape = createEscaper(unescapeMap);\n\n // If the value of the named `property` is a function then invoke it with the\n // `object` as context; otherwise, return it.\n _.result = function(object, property, fallback) {\n var value = object == null ? void 0 : object[property];\n if (value === void 0) {\n value = fallback;\n }\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\u2028|\\u2029/g;\n\n var escapeChar = function(match) {\n return '\\\\' + escapes[match];\n };\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n // NB: `oldSettings` only exists for backwards compatibility.\n _.template = function(text, settings, oldSettings) {\n if (!settings && oldSettings) settings = oldSettings;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset).replace(escaper, escapeChar);\n index = offset + match.length;\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n } else if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n } else if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n\n // Adobe VMs need the match returned to produce the correct offest.\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + 'return __p;\\n';\n\n try {\n var render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled source as a convenience for precompilation.\n var argument = settings.variable || 'obj';\n template.source = 'function(' + argument + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function. Start chaining a wrapped Underscore object.\n _.chain = function(obj) {\n var instance = _(obj);\n instance._chain = true;\n return instance;\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(instance, obj) {\n return instance._chain ? _(obj).chain() : obj;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n _.each(_.functions(obj), function(name) {\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result(this, func.apply(_, args));\n };\n });\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];\n return result(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n _.each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result(this, method.apply(this._wrapped, arguments));\n };\n });\n\n // Extracts the result from a wrapped and chained object.\n _.prototype.value = function() {\n return this._wrapped;\n };\n\n // Provide unwrapping proxy for some methods used in engine operations\n // such as arithmetic and JSON stringification.\n _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;\n\n _.prototype.toString = function() {\n return '' + this._wrapped;\n };\n\n // AMD registration happens at the end for compatibility with AMD loaders\n // that may not enforce next-turn semantics on modules. Even though general\n // practice for AMD registration is to be anonymous, underscore registers\n // as a named module because, like jQuery, it is a base library that is\n // popular enough to be bundled in a third party lib, but not be part of\n // an AMD load request. Those cases could generate an error when an\n // anonymous define() is called outside of a loader request.\n if (typeof define === 'function' && define.amd) {\n define('underscore', [], function() {\n return _;\n });\n }\n}.call(this));\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/underscore/underscore.js\n// module id = 3\n// module chunks = 0","module.exports = {\n\t\"name\": \"bonobo-jupyter\",\n\t\"version\": \"0.0.1\",\n\t\"description\": \"Jupyter integration for Bonobo\",\n\t\"author\": \"\",\n\t\"main\": \"src/index.js\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"\"\n\t},\n\t\"keywords\": [\n\t\t\"jupyter\",\n\t\t\"widgets\",\n\t\t\"ipython\",\n\t\t\"ipywidgets\"\n\t],\n\t\"scripts\": {\n\t\t\"prepublish\": \"webpack\",\n\t\t\"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"json-loader\": \"^0.5.4\",\n\t\t\"webpack\": \"^1.12.14\"\n\t},\n\t\"dependencies\": {\n\t\t\"jupyter-js-widgets\": \"^2.0.9\",\n\t\t\"underscore\": \"^1.8.3\"\n\t}\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./package.json\n// module id = 4\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/bonobo/ext/jupyter/widget.py b/bonobo/ext/jupyter/widget.py index 8bcd317..cfacf3e 100644 --- a/bonobo/ext/jupyter/widget.py +++ b/bonobo/ext/jupyter/widget.py @@ -2,7 +2,7 @@ import ipywidgets as widgets from traitlets import List, Unicode -@widgets.register('bonobo-widget.Bonobo') +@widgets.register('bonobo-widget.bonobo') class BonoboWidget(widgets.DOMWidget): _view_name = Unicode('BonoboView').tag(sync=True) _model_name = Unicode('BonoboModel').tag(sync=True) diff --git a/setup.py b/setup.py index 618ec41..540ea1b 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ setup( 'pytest-cov (>= 2.5, < 3.0)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' ], 'docker': ['bonobo-docker'], - 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] + 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'] }, entry_points={ 'bonobo.commands': [ From 5bbfa41956bff5bcabc1eab673e81ad5ad46ddcd Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 09:45:49 +0200 Subject: [PATCH 084/143] [jupyter] minor docs edit. --- docs/guide/ext/jupyter.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/guide/ext/jupyter.rst b/docs/guide/ext/jupyter.rst index b58b70f..6e96bf6 100644 --- a/docs/guide/ext/jupyter.rst +++ b/docs/guide/ext/jupyter.rst @@ -15,20 +15,23 @@ Install `bonobo` with the **jupyter** extra:: Install the jupyter extension:: + jupyter nbextension enable --py --sys-prefix widgetsnbextension jupyter nbextension enable --py --sys-prefix bonobo.ext.jupyter Development ::::::::::: +You should favor yarn over npm to install node packages. If you prefer to use npm, it's up to you to adapt the code. + To install the widget for development, make sure you're using an editable install of bonobo (see install document):: jupyter nbextension install --py --symlink --sys-prefix bonobo.ext.jupyter jupyter nbextension enable --py --sys-prefix bonobo.ext.jupyter -If you wanna change the javascript, you should run webpack in watch mode in some terminal:: +If you want to change the javascript, you should run webpack in watch mode in some terminal:: cd bonobo/ext/jupyter/js - npm install + yarn install ./node_modules/.bin/webpack --watch To compile the widget into a distributable version (which gets packaged on PyPI when a release is made), just run From b80ff253dbecef1a8c0bbed82bac856e4b290573 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 10:47:16 +0200 Subject: [PATCH 085/143] [docs] work in progress. --- docs/tutorial/index.rst | 2 ++ docs/tutorial/tut03.rst | 7 ++++++- docs/tutorial/tut04.rst | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index c00f27a..342449b 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -26,6 +26,8 @@ that did not block you but can be a no-go for others, please consider contributi tut01 tut02 + tut03 + tut04 What's next? diff --git a/docs/tutorial/tut03.rst b/docs/tutorial/tut03.rst index dc1e4c2..2721430 100644 --- a/docs/tutorial/tut03.rst +++ b/docs/tutorial/tut03.rst @@ -1,7 +1,12 @@ Configurables and Services ========================== -TODO +This document does not exist yet, but will be available soon. + +Meanwhile, you can read the matching references: + +* :doc:`/guide/services` +* :doc:`/reference/api_config` Next :::: diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst index 18bea48..14888d5 100644 --- a/docs/tutorial/tut04.rst +++ b/docs/tutorial/tut04.rst @@ -1,4 +1,8 @@ Working with databases ====================== -TODO +This document does not exist yet, but will be available soon. + +Meanwhile, you can jump to bonobo-sqlalchemy development repository: + +* https://github.com/hartym/bonobo-sqlalchemy From 98430354e6220603eea2f9ab1e18044a0c280fbf Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 10:50:36 +0200 Subject: [PATCH 086/143] [docs] link to first bonobo-docker guide. --- docs/guide/ext/docker.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/guide/ext/docker.rst b/docs/guide/ext/docker.rst index 5937c02..8ff667f 100644 --- a/docs/guide/ext/docker.rst +++ b/docs/guide/ext/docker.rst @@ -4,11 +4,5 @@ Bonobo with Docker .. todo:: The `bonobo-docker` package is at a very alpha stage, and things will change. This section is here to give a brief overview but is neither complete nor definitive. -Installation -:::::::::::: +Read the introduction: https://www.bonobo-project.org/with/docker -Overview -:::::::: - -Details -::::::: From c34b86872f78434b9f8dc96033f1a18ee8eb1618 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 10:52:47 +0200 Subject: [PATCH 087/143] Update example datasets. --- bonobo/examples/datasets/coffeeshops.json | 242 +++++++++++----------- bonobo/examples/datasets/coffeeshops.txt | 240 ++++++++++----------- 2 files changed, 241 insertions(+), 241 deletions(-) diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json index ab07fba..60e89b1 100644 --- a/bonobo/examples/datasets/coffeeshops.json +++ b/bonobo/examples/datasets/coffeeshops.json @@ -1,182 +1,182 @@ -{"O q de poule": "53 rue du ruisseau, 75018 Paris, France", +{"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", +"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", +"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", +"Le Felteu": "1 rue Pecquay, 75004 Paris, France", +"O q de poule": "53 rue du ruisseau, 75018 Paris, France", "Le chantereine": "51 Rue Victoire, 75009 Paris, France", +"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", "La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France", "Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France", "La Renaissance": "112 Rue Championnet, 75018 Paris, France", "Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France", -"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", -"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", +"Le Sully": "6 Bd henri IV, 75004 Paris, France", +"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", +"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France", +"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France", +"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", +"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France", +"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", +"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", +"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", +"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", +"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", "Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France", "D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France", -"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", "Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France", "Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France", "Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France", -"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", "Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", -"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", -"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", -"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", "Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", "Le Bellerive": "71 quai de Seine, 75019 Paris, France", "La Bauloise": "36 rue du hameau, 75015 Paris, France", "Le Dellac": "14 rue Rougemont, 75009 Paris, France", -"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", -"Le Sully": "6 Bd henri IV, 75004 Paris, France", -"Le Felteu": "1 rue Pecquay, 75004 Paris, France", -"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", -"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France", -"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France", -"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France", -"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", -"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", -"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France", -"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", -"Au bon coin": "49 rue des Cloys, 75018 Paris, France", -"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", -"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", -"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", -"Le Supercoin": "3, rue Baudelique, 75018 Paris, France", -"Populettes": "86 bis rue Riquet, 75018 Paris, France", -"Le Couvent": "69 rue Broca, 75013 Paris, France", -"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", -"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France", -"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France", -"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France", -"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", -"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", -"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", -"La Perle": "78 rue vieille du temple, 75003 Paris, France", -"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", -"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", -"Le Descartes": "1 rue Thouin, 75005 Paris, France", "Br\u00fblerie San Jos\u00e9": "30 rue des Petits-Champs, 75002 Paris, France", "Caf\u00e9 de la Mairie (du VIII)": "rue de Lisbonne, 75008 Paris, France", -"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", -"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", +"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", +"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", "En attendant l'or": "3 rue Faidherbe, 75011 Paris, France", -"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", -"Le Village": "182 rue de Courcelles, 75017 Paris, France", -"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", -"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", -"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", -"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", -"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", +"Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France", +"Etienne": "14 rue Turbigo, Paris, 75001 Paris, France", +"L'ing\u00e9nu": "184 bd Voltaire, 75011 Paris, France", +"Le Biz": "18 rue Favart, 75002 Paris, France", +"L'Olive": "8 rue L'Olive, 75018 Paris, France", +"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", +"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", "La Marine": "55 bis quai de valmy, 75010 Paris, France", "American Kitchen": "49 rue bichat, 75010 Paris, France", +"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", "Face Bar": "82 rue des archives, 75003 Paris, France", "Le Bloc": "21 avenue Brochant, 75017 Paris, France", "La Bricole": "52 rue Liebniz, 75018 Paris, France", "le ronsard": "place maubert, 75005 Paris, France", "l'Usine": "1 rue d'Avron, 75020 Paris, France", -"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", -"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", -"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", +"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France", +"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", +"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France", "Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France", "Le Germinal": "95 avenue Emile Zola, 75015 Paris, France", -"Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France", -"Etienne": "14 rue Turbigo, Paris, 75001 Paris, France", -"L'ing\u00e9nu": "184 bd Voltaire, 75011 Paris, France", "Le refuge": "72 rue lamarck, 75018 Paris, France", -"Le Biz": "18 rue Favart, 75002 Paris, France", -"L'Olive": "8 rue L'Olive, 75018 Paris, France", -"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France", "Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", +"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", +"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", +"Le Supercoin": "3, rue Baudelique, 75018 Paris, France", +"Populettes": "86 bis rue Riquet, 75018 Paris, France", +"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", "L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France", -"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", -"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", -"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", +"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", +"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", +"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", +"Au bon coin": "49 rue des Cloys, 75018 Paris, France", +"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", +"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France", +"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", +"Le Couvent": "69 rue Broca, 75013 Paris, France", +"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", +"La Perle": "78 rue vieille du temple, 75003 Paris, France", +"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", +"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", +"Le Descartes": "1 rue Thouin, 75005 Paris, France", +"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France", +"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France", +"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", +"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", +"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", +"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", +"Le Village": "182 rue de Courcelles, 75017 Paris, France", +"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", +"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", +"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", "Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France", +"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", +"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", +"Caves populaires": "22 rue des Dames, 75017 Paris, France", +"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", +"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", +"L'anjou": "1 rue de Montholon, 75009 Paris, France", +"Le Brio": "216, rue Marcadet, 75018 Paris, France", +"Tamm Bara": "7 rue Clisson, 75013 Paris, France", +"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", +"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", "Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France", "Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France", -"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", "Le Centenaire": "104 rue amelot, 75011 Paris, France", -"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France", -"La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 Paris, France", -"Le bal du pirate": "60 rue des bergers, 75015 Paris, France", -"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", -"Le Killy Jen": "28 bis boulevard Diderot, 75012 Paris, France", -"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", -"Les Artisans": "106 rue Lecourbe, 75015 Paris, France", -"Peperoni": "83 avenue de Wagram, 75001 Paris, France", +"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", "Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France", -"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", -"zic zinc": "95 rue claude decaen, 75012 Paris, France", "L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France", -"Le Brio": "216, rue Marcadet, 75018 Paris, France", "Le Dunois": "77 rue Dunois, 75013 Paris, France", "La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France", "Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France", "Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France", -"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", -"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", -"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", -"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", -"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", -"Caves populaires": "22 rue des Dames, 75017 Paris, France", -"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", -"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", +"Le bal du pirate": "60 rue des bergers, 75015 Paris, France", "L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France", -"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", -"L'anjou": "1 rue de Montholon, 75009 Paris, France", -"Tamm Bara": "7 rue Clisson, 75013 Paris, France", -"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", -"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", -"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", -"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", +"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", +"zic zinc": "95 rue claude decaen, 75012 Paris, France", +"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France", +"La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 Paris, France", +"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", +"Le Killy Jen": "28 bis boulevard Diderot, 75012 Paris, France", +"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", +"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", +"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", +"Les Artisans": "106 rue Lecourbe, 75015 Paris, France", +"Peperoni": "83 avenue de Wagram, 75001 Paris, France", "Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", "La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France", "Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France", -"Les caves populaires": "22 rue des Dames, 75017 Paris, France", -"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", -"Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France", -"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", +"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", +"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", "La Brocante": "10 rue Rossini, 75009 Paris, France", -"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", -"Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France", -"Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France", -"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", -"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", -"maison du vin": "52 rue des plantes, 75014 Paris, France", -"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", +"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", +"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", "NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", -"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", -"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", -"le lutece": "380 rue de vaugirard, 75015 Paris, France", -"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", -"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", "O'Paris": "1 Rue des Envierges, 75020 Paris, France", -"Rivolux": "16 rue de Rivoli, 75004 Paris, France", -"Brasiloja": "16 rue Ganneron, 75018 Paris, France", "Botak cafe": "1 rue Paul albert, 75018 Paris, France", "La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France", -"Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France", -"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", -"Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France", -"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", -"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", +"Les caves populaires": "22 rue des Dames, 75017 Paris, France", +"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", +"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", +"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", +"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", +"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", +"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", +"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", +"Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France", +"Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France", +"Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France", +"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", +"maison du vin": "52 rue des plantes, 75014 Paris, France", +"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", +"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", +"le lutece": "380 rue de vaugirard, 75015 Paris, France", +"Rivolux": "16 rue de Rivoli, 75004 Paris, France", +"Brasiloja": "16 rue Ganneron, 75018 Paris, France", "Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France", "L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France", "Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France", -"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France", -"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", -"Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France", -"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", -"Le Brigadier": "12 rue Blanche, 75009 Paris, France", -"Melting Pot": "3 rue de Lagny, 75020 Paris, France", -"L'Entracte": "place de l'opera, 75002 Paris, France", -"le Zango": "58 rue Daguerre, 75014 Paris, France", -"Panem": "18 rue de Crussol, 75011 Paris, France", -"Waikiki": "10 rue d\"Ulm, 75005 Paris, France", "l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France", "Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France", "Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France", +"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France", +"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", +"Melting Pot": "3 rue de Lagny, 75020 Paris, France", +"le Zango": "58 rue Daguerre, 75014 Paris, France", +"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", +"Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France", +"Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France", +"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", +"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", "Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", +"L'Angle": "28 rue de Ponthieu, 75008 Paris, France", +"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France", +"L'Entracte": "place de l'opera, 75002 Paris, France", +"Panem": "18 rue de Crussol, 75011 Paris, France", "Au pays de Vannes": "34 bis rue de Wattignies, 75012 Paris, France", "Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France", -"L'Angle": "28 rue de Ponthieu, 75008 Paris, France", "Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France", -"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France", "L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France", "Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", -"L'horizon": "93, rue de la Roquette, 75011 Paris, France"} \ No newline at end of file +"L'horizon": "93, rue de la Roquette, 75011 Paris, France", +"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", +"Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France", +"Le Brigadier": "12 rue Blanche, 75009 Paris, France", +"Waikiki": "10 rue d\"Ulm, 75005 Paris, France"} \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt index eb3b668..5fe1ef6 100644 --- a/bonobo/examples/datasets/coffeeshops.txt +++ b/bonobo/examples/datasets/coffeeshops.txt @@ -1,182 +1,182 @@ +Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France +les montparnos, 65 boulevard Pasteur, 75015 Paris, France +Le Saint Jean, 23 rue des abbesses, 75018 Paris, France +Le Felteu, 1 rue Pecquay, 75004 Paris, France O q de poule, 53 rue du ruisseau, 75018 Paris, France Le chantereine, 51 Rue Victoire, 75009 Paris, France +Le Müller, 11 rue Feutrier, 75018 Paris, France La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France La Renaissance, 112 Rue Championnet, 75018 Paris, France Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France -Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France -les montparnos, 65 boulevard Pasteur, 75015 Paris, France +Le Sully, 6 Bd henri IV, 75004 Paris, France +Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France +Le café des amis, 125 rue Blomet, 75015 Paris, France +Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France +Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France +Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France +Le Café Livres, 10 rue Saint Martin, 75004 Paris, France +Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France +Le Square, 31 rue Saint-Dominique, 75007 Paris, France +Les Arcades, 61 rue de Ponthieu, 75008 Paris, France +Le Bosquet, 46 avenue Bosquet, 75007 Paris, France Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France -Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France -Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France -Le Saint Jean, 23 rue des abbesses, 75018 Paris, France -Le Square, 31 rue Saint-Dominique, 75007 Paris, France -Les Arcades, 61 rue de Ponthieu, 75008 Paris, France Café Lea, 5 rue Claude Bernard, 75005 Paris, France Le Bellerive, 71 quai de Seine, 75019 Paris, France La Bauloise, 36 rue du hameau, 75015 Paris, France Le Dellac, 14 rue Rougemont, 75009 Paris, France -Le Bosquet, 46 avenue Bosquet, 75007 Paris, France -Le Sully, 6 Bd henri IV, 75004 Paris, France -Le Felteu, 1 rue Pecquay, 75004 Paris, France -Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France -Le café des amis, 125 rue Blomet, 75015 Paris, France -Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France -Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France -Le Müller, 11 rue Feutrier, 75018 Paris, France -Le Café Livres, 10 rue Saint Martin, 75004 Paris, France -La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France -Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France -Au bon coin, 49 rue des Cloys, 75018 Paris, France -La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France -Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France -O'Breizh, 27 rue de Penthièvre, 75008 Paris, France -Le Supercoin, 3, rue Baudelique, 75018 Paris, France -Populettes, 86 bis rue Riquet, 75018 Paris, France -Le Couvent, 69 rue Broca, 75013 Paris, France -Café Zen, 46 rue Victoire, 75009 Paris, France -Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France -Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France -Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France -Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France -Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France -Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France -La Perle, 78 rue vieille du temple, 75003 Paris, France -Le Café frappé, 95 rue Montmartre, 75002 Paris, France -L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France -Le Descartes, 1 rue Thouin, 75005 Paris, France Brûlerie San José, 30 rue des Petits-Champs, 75002 Paris, France Café de la Mairie (du VIII), rue de Lisbonne, 75008 Paris, France -Au panini de la place, 47 rue Belgrand, 75020 Paris, France -Extra old café, 307 fg saint Antoine, 75011 Paris, France +Le General Beuret, 9 Place du General Beuret, 75015 Paris, France +Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France En attendant l'or, 3 rue Faidherbe, 75011 Paris, France -Le Pure café, 14 rue Jean Macé, 75011 Paris, France -Le Village, 182 rue de Courcelles, 75017 Paris, France -Le Malar, 88 rue Saint-Dominique, 75007 Paris, France -Pause Café, 41 rue de Charonne, 75011 Paris, France -Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France -La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France -Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France +Café Martin, 2 place Martin Nadaud, 75001 Paris, France +Etienne, 14 rue Turbigo, Paris, 75001 Paris, France +L'ingénu, 184 bd Voltaire, 75011 Paris, France +Le Biz, 18 rue Favart, 75002 Paris, France +L'Olive, 8 rue L'Olive, 75018 Paris, France +Le pari's café, 104 rue caulaincourt, 75018 Paris, France +Le Poulailler, 60 rue saint-sabin, 75011 Paris, France La Marine, 55 bis quai de valmy, 75010 Paris, France American Kitchen, 49 rue bichat, 75010 Paris, France +Chai 33, 33 Cour Saint Emilion, 75012 Paris, France Face Bar, 82 rue des archives, 75003 Paris, France Le Bloc, 21 avenue Brochant, 75017 Paris, France La Bricole, 52 rue Liebniz, 75018 Paris, France le ronsard, place maubert, 75005 Paris, France l'Usine, 1 rue d'Avron, 75020 Paris, France -La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France -Le General Beuret, 9 Place du General Beuret, 75015 Paris, France -Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France +La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France +Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France +Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France Le Germinal, 95 avenue Emile Zola, 75015 Paris, France -Café Martin, 2 place Martin Nadaud, 75001 Paris, France -Etienne, 14 rue Turbigo, Paris, 75001 Paris, France -L'ingénu, 184 bd Voltaire, 75011 Paris, France Le refuge, 72 rue lamarck, 75018 Paris, France -Le Biz, 18 rue Favart, 75002 Paris, France -L'Olive, 8 rue L'Olive, 75018 Paris, France -Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France +Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France +O'Breizh, 27 rue de Penthièvre, 75008 Paris, France +Le Supercoin, 3, rue Baudelique, 75018 Paris, France +Populettes, 86 bis rue Riquet, 75018 Paris, France +La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France -Le pari's café, 104 rue caulaincourt, 75018 Paris, France -Le Poulailler, 60 rue saint-sabin, 75011 Paris, France -Chai 33, 33 Cour Saint Emilion, 75012 Paris, France +Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France +Café Zen, 46 rue Victoire, 75009 Paris, France +La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France +Au bon coin, 49 rue des Cloys, 75018 Paris, France +La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France +Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France +Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France +Le Couvent, 69 rue Broca, 75013 Paris, France +Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France +La Perle, 78 rue vieille du temple, 75003 Paris, France +Le Café frappé, 95 rue Montmartre, 75002 Paris, France +L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France +Le Descartes, 1 rue Thouin, 75005 Paris, France +Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France +Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France +Au panini de la place, 47 rue Belgrand, 75020 Paris, France +Extra old café, 307 fg saint Antoine, 75011 Paris, France +Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France +Le Pure café, 14 rue Jean Macé, 75011 Paris, France +Le Village, 182 rue de Courcelles, 75017 Paris, France +Le Malar, 88 rue Saint-Dominique, 75007 Paris, France +Pause Café, 41 rue de Charonne, 75011 Paris, France +Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France +Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France +Caprice café, 12 avenue Jean Moulin, 75014 Paris, France +Caves populaires, 22 rue des Dames, 75017 Paris, France +Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France +Chez Prune, 36 rue Beaurepaire, 75010 Paris, France +L'anjou, 1 rue de Montholon, 75009 Paris, France +Le Brio, 216, rue Marcadet, 75018 Paris, France +Tamm Bara, 7 rue Clisson, 75013 Paris, France +La chaumière gourmande, Route de la Muette à Neuilly +Club hippique du Jardin d’Acclimatation, 75016 Paris, France Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France -Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France Le Centenaire, 104 rue amelot, 75011 Paris, France -Café Pistache, 9 rue des petits champs, 75001 Paris, France -La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 Paris, France -Le bal du pirate, 60 rue des bergers, 75015 Paris, France -bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France -Le Killy Jen, 28 bis boulevard Diderot, 75012 Paris, France -le 1 cinq, 172 rue de vaugirard, 75015 Paris, France -Les Artisans, 106 rue Lecourbe, 75015 Paris, France -Peperoni, 83 avenue de Wagram, 75001 Paris, France +Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France Ragueneau, 202 rue Saint Honoré, 75001 Paris, France -l'orillon bar, 35 rue de l'orillon, 75011 Paris, France -zic zinc, 95 rue claude decaen, 75012 Paris, France L'Inévitable, 22 rue Linné, 75005 Paris, France -Le Brio, 216, rue Marcadet, 75018 Paris, France Le Dunois, 77 rue Dunois, 75013 Paris, France La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France Le petit Bretonneau, Le petit Bretonneau - à l'intérieur de l'Hôpital, 75018 Paris, France -La chaumière gourmande, Route de la Muette à Neuilly -Club hippique du Jardin d’Acclimatation, 75016 Paris, France -Caprice café, 12 avenue Jean Moulin, 75014 Paris, France -Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France -Café beauveau, 9 rue de Miromesnil, 75008 Paris, France -Caves populaires, 22 rue des Dames, 75017 Paris, France -Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France -Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France +Le bal du pirate, 60 rue des bergers, 75015 Paris, France L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France -Chez Prune, 36 rue Beaurepaire, 75010 Paris, France -L'anjou, 1 rue de Montholon, 75009 Paris, France -Tamm Bara, 7 rue Clisson, 75013 Paris, France -La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France -Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France -Café Clochette, 16 avenue Richerand, 75010 Paris, France -L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France +l'orillon bar, 35 rue de l'orillon, 75011 Paris, France +zic zinc, 95 rue claude decaen, 75012 Paris, France +Café Pistache, 9 rue des petits champs, 75001 Paris, France +La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 Paris, France +bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France +Le Killy Jen, 28 bis boulevard Diderot, 75012 Paris, France +Café beauveau, 9 rue de Miromesnil, 75008 Paris, France +le 1 cinq, 172 rue de vaugirard, 75015 Paris, France +Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France +Les Artisans, 106 rue Lecourbe, 75015 Paris, France +Peperoni, 83 avenue de Wagram, 75001 Paris, France Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France -Les caves populaires, 22 rue des Dames, 75017 Paris, France -Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France -Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France -Trois pièces cuisine, 101 rue des dames, 75017 Paris, France +La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France +Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France La Brocante, 10 rue Rossini, 75009 Paris, France -Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France -Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France -Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France -L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France -le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France -maison du vin, 52 rue des plantes, 75014 Paris, France -Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France +Café Clochette, 16 avenue Richerand, 75010 Paris, France +L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France -Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France -Le Tournebride, 104 rue Mouffetard, 75005 Paris, France -le lutece, 380 rue de vaugirard, 75015 Paris, France -Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France -Le Fronton, 63 rue de Ponthieu, 75008 Paris, France O'Paris, 1 Rue des Envierges, 75020 Paris, France -Rivolux, 16 rue de Rivoli, 75004 Paris, France -Brasiloja, 16 rue Ganneron, 75018 Paris, France Botak cafe, 1 rue Paul albert, 75018 Paris, France La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France -Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France -Chez Miamophile, 6 rue Mélingue, 75019 Paris, France -Canopy Café associatif, 19 rue Pajol, 75018 Paris, France -Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France -Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France +Les caves populaires, 22 rue des Dames, 75017 Paris, France +Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France +Trois pièces cuisine, 101 rue des dames, 75017 Paris, France +Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France +L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France +Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France +Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France +Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France +Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France +Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France +Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France +le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France +maison du vin, 52 rue des plantes, 75014 Paris, France +Le Tournebride, 104 rue Mouffetard, 75005 Paris, France +Le Fronton, 63 rue de Ponthieu, 75008 Paris, France +le lutece, 380 rue de vaugirard, 75015 Paris, France +Rivolux, 16 rue de Rivoli, 75004 Paris, France +Brasiloja, 16 rue Ganneron, 75018 Paris, France Le café Monde et Médias, Place de la République, 75003 Paris, France L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France -Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France -L'empreinte, 54, avenue Daumesnil, 75012 Paris, France -Café Victor, 10 boulevard Victor, 75015 Paris, France -Café Varenne, 36 rue de Varenne, 75007 Paris, France -Le Brigadier, 12 rue Blanche, 75009 Paris, France -Melting Pot, 3 rue de Lagny, 75020 Paris, France -L'Entracte, place de l'opera, 75002 Paris, France -le Zango, 58 rue Daguerre, 75014 Paris, France -Panem, 18 rue de Crussol, 75011 Paris, France -Waikiki, 10 rue d"Ulm, 75005 Paris, France l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France Pari's Café, 174 avenue de Clichy, 75017 Paris, France +Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France +Café Varenne, 36 rue de Varenne, 75007 Paris, France +Melting Pot, 3 rue de Lagny, 75020 Paris, France +le Zango, 58 rue Daguerre, 75014 Paris, France +Chez Miamophile, 6 rue Mélingue, 75019 Paris, France +Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France +Canopy Café associatif, 19 rue Pajol, 75018 Paris, France +Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France +Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France +L'Angle, 28 rue de Ponthieu, 75008 Paris, France +Café Dupont, 198 rue de la Convention, 75015 Paris, France +L'Entracte, place de l'opera, 75002 Paris, France +Panem, 18 rue de Crussol, 75011 Paris, France Au pays de Vannes, 34 bis rue de Wattignies, 75012 Paris, France Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France -L'Angle, 28 rue de Ponthieu, 75008 Paris, France Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France -Café Dupont, 198 rue de la Convention, 75015 Paris, France L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France -L'horizon, 93, rue de la Roquette, 75011 Paris, France \ No newline at end of file +L'horizon, 93, rue de la Roquette, 75011 Paris, France +L'empreinte, 54, avenue Daumesnil, 75012 Paris, France +Café Victor, 10 boulevard Victor, 75015 Paris, France +Le Brigadier, 12 rue Blanche, 75009 Paris, France +Waikiki, 10 rue d"Ulm, 75005 Paris, France \ No newline at end of file From e5483de3443c6991e21393b73b34cf27fbf24840 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 5 Jun 2017 11:38:11 +0200 Subject: [PATCH 088/143] [core] I/O formats allowing both arg0 formating and kwargs based. Starting with 0.4, kwargs based will be default (BC break here, but needed for the greater good). --- bonobo/examples/files/csv_handlers.py | 4 +- bonobo/examples/files/json_handlers.py | 7 +- bonobo/examples/files/pickle_handlers.py | 83 ++++++++++++------------ bonobo/nodes/io/csv.py | 36 ++-------- bonobo/nodes/io/file.py | 34 ++++++++-- bonobo/nodes/io/json.py | 2 +- bonobo/nodes/io/pickle.py | 2 +- bonobo/settings.py | 25 +++++++ 8 files changed, 111 insertions(+), 82 deletions(-) diff --git a/bonobo/examples/files/csv_handlers.py b/bonobo/examples/files/csv_handlers.py index a9094b6..a15444d 100644 --- a/bonobo/examples/files/csv_handlers.py +++ b/bonobo/examples/files/csv_handlers.py @@ -2,8 +2,8 @@ import bonobo from bonobo.commands.run import get_default_services graph = bonobo.Graph( - bonobo.CsvReader('datasets/coffeeshops.txt'), - print, + bonobo.CsvReader('datasets/coffeeshops.txt', headers=('item',)), + bonobo.PrettyPrinter(), ) if __name__ == '__main__': diff --git a/bonobo/examples/files/json_handlers.py b/bonobo/examples/files/json_handlers.py index 86fe6a0..27dc38e 100644 --- a/bonobo/examples/files/json_handlers.py +++ b/bonobo/examples/files/json_handlers.py @@ -1,15 +1,16 @@ import bonobo +from bonobo import Bag from bonobo.commands.run import get_default_services -def get_fields(row): - return row['fields'] +def get_fields(**row): + return Bag(**row['fields']) graph = bonobo.Graph( bonobo.JsonReader('datasets/theaters.json'), get_fields, - bonobo.PrettyPrint(title_keys=('eq_nom_equipement', )), + bonobo.PrettyPrinter(), ) if __name__ == '__main__': diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py index e6f3dcc..6863076 100644 --- a/bonobo/examples/files/pickle_handlers.py +++ b/bonobo/examples/files/pickle_handlers.py @@ -1,10 +1,38 @@ +''' +This example shows how a different file system service can be injected +into a transformation (as compressing pickled objects often makes sense +anyways). The pickle itself contains a list of lists as follows: + +``` +[ + ['category', 'sms'], + ['ham', 'Go until jurong point, crazy..'], + ['ham', 'Ok lar... Joking wif u oni...'], + ['spam', 'Free entry in 2 a wkly comp to win...'], + ['ham', 'U dun say so early hor... U c already then say...'], + ['ham', 'Nah I don't think he goes to usf, he lives around here though'], + ['spam', 'FreeMsg Hey there darling it's been 3 week's now...'], + ... +] +``` + +where the first column categorizes and sms as "ham" or "spam". The second +column contains the sms itself. + +Data set taken from: +https://www.kaggle.com/uciml/sms-spam-collection-dataset/downloads/sms-spam-collection-dataset.zip + +The transformation (1) reads the pickled data, (2) marks and shortens +messages categorized as spam, and (3) prints the output. + +''' + import bonobo +from bonobo.commands.run import get_default_services from fs.tarfs import TarFS -import os -def cleanse_sms(row): - +def cleanse_sms(**row): if row['category'] == 'spam': row['sms_clean'] = '**MARKED AS SPAM** ' + row['sms'][0:50] + ( '...' if len(row['sms']) > 50 else '' @@ -16,46 +44,21 @@ def cleanse_sms(row): graph = bonobo.Graph( - bonobo.PickleReader('spam.pkl' - ), # spam.pkl is within the gzipped tarball + # spam.pkl is within the gzipped tarball + bonobo.PickleReader('spam.pkl'), cleanse_sms, - print + bonobo.PrettyPrinter(), ) -if __name__ == '__main__': - ''' - This example shows how a different file system service can be injected - into a transformation (as compressing pickled objects often makes sense - anyways). The pickle itself contains a list of lists as follows: - ``` - [ - ['category', 'sms'], - ['ham', 'Go until jurong point, crazy..'], - ['ham', 'Ok lar... Joking wif u oni...'], - ['spam', 'Free entry in 2 a wkly comp to win...'], - ['ham', 'U dun say so early hor... U c already then say...'], - ['ham', 'Nah I don't think he goes to usf, he lives around here though'], - ['spam', 'FreeMsg Hey there darling it's been 3 week's now...'], - ... - ] - ``` - - where the first column categorizes and sms as "ham" or "spam". The second - column contains the sms itself. - - Data set taken from: - https://www.kaggle.com/uciml/sms-spam-collection-dataset/downloads/sms-spam-collection-dataset.zip - - The transformation (1) reads the pickled data, (2) marks and shortens - messages categorized as spam, and (3) prints the output. - ''' - - services = { +def get_services(): + return { 'fs': - TarFS( - os.path. - join(bonobo.get_examples_path(), 'datasets', 'spam.tgz') - ) + TarFS( + bonobo.get_examples_path('datasets/spam.tgz') + ) } - bonobo.run(graph, services=services) + + +if __name__ == '__main__': + bonobo.run(graph, services=get_default_services(__file__)) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index e0412fa..bf3872d 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -3,10 +3,8 @@ import csv from bonobo.config import Option from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED -from bonobo.errors import ConfigurationError, ValidationError -from bonobo.structs import Bag +from bonobo.nodes.io.file import FileHandler, FileReader, FileWriter from bonobo.util.objects import ValueHolder -from .file import FileHandler, FileReader, FileWriter class CsvHandler(FileHandler): @@ -30,14 +28,6 @@ class CsvHandler(FileHandler): headers = Option(tuple) -def validate_csv_output_format(v): - if callable(v): - return v - if v in {'dict', 'kwargs'}: - return v - raise ValidationError('Unsupported format {!r}.'.format(v)) - - class CsvReader(CsvHandler, FileReader): """ Reads a CSV and yield the values as dicts. @@ -49,26 +39,17 @@ class CsvReader(CsvHandler, FileReader): """ skip = Option(int, default=0) - output_format = Option(validate_csv_output_format, default='dict') @ContextProcessor def csv_headers(self, context, fs, file): yield ValueHolder(self.headers) - def get_output_formater(self): - if callable(self.output_format): - return self.output_format - elif isinstance(self.output_format, str): - return getattr(self, '_format_as_' + self.output_format) - else: - raise ConfigurationError('Unsupported format {!r} for {}.'.format(self.output_format, type(self).__name__)) - def read(self, fs, file, headers): reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar) - formater = self.get_output_formater() if not headers.get(): headers.set(next(reader)) + _headers = headers.get() field_count = len(headers) @@ -78,15 +59,9 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) - yield formater(headers.get(), row) - - def _format_as_dict(self, headers, values): - return dict(zip(headers, values)) - - def _format_as_kwargs(self, headers, values): - return Bag(**dict(zip(headers, values))) + yield self.get_output(dict(zip(_headers, row))) class CsvWriter(CsvHandler, FileWriter): @@ -96,7 +71,8 @@ class CsvWriter(CsvHandler, FileWriter): headers = ValueHolder(list(self.headers) if self.headers else None) yield writer, headers - def write(self, fs, file, lineno, writer, headers, row): + def write(self, fs, file, lineno, writer, headers, *args, **kwargs): + row = self.get_input(*args, **kwargs) if not lineno: headers.set(headers.value or row.keys()) writer.writerow(headers.get()) diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index b06fae3..3e2c51d 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -1,7 +1,9 @@ +from bonobo import settings from bonobo.config import Option, Service from bonobo.config.configurables import Configurable from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED +from bonobo.structs.bags import Bag from bonobo.util.objects import ValueHolder @@ -22,6 +24,8 @@ class FileHandler(Configurable): fs = Service('fs') # type: str + ioformat = Option(settings.validate_io_format, default=settings.IOFORMAT) + @ContextProcessor def file(self, context, fs): with self.open(fs) as file: @@ -30,15 +34,35 @@ class FileHandler(Configurable): def open(self, fs): return fs.open(self.path, self.mode, encoding=self.encoding) + def get_input(self, *args, **kwargs): + if self.ioformat == settings.IOFORMAT_ARG0: + assert len(args) == 1 and not len(kwargs), 'ARG0 format implies one arg and no kwargs.' + return args[0] + + if self.ioformat == settings.IOFORMAT_KWARGS: + assert len(args) == 0 and len(kwargs), 'KWARGS format implies no arg.' + return kwargs + + raise NotImplementedError('Unsupported format.') + + def get_output(self, row): + if self.ioformat == settings.IOFORMAT_ARG0: + return row + + if self.ioformat == settings.IOFORMAT_KWARGS: + return Bag(**row) + + raise NotImplementedError('Unsupported format.') + class Reader(FileHandler): """Abstract component factory for readers. """ - def __call__(self, *args): - yield from self.read(*args) + def __call__(self, *args, **kwargs): + yield from self.read(*args, **kwargs) - def read(self, *args): + def read(self, *args, **kwargs): raise NotImplementedError('Abstract.') @@ -46,10 +70,10 @@ class Writer(FileHandler): """Abstract component factory for writers. """ - def __call__(self, *args): + def __call__(self, *args, **kwargs): return self.write(*args) - def write(self, *args): + def write(self, *args, **kwargs): raise NotImplementedError('Abstract.') diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index b2db708..74857db 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -14,7 +14,7 @@ class JsonReader(JsonHandler, FileReader): def read(self, fs, file): for line in self.loader(file): - yield line + yield self.get_output(line) class JsonWriter(JsonHandler, FileWriter): diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index cf6b5eb..c603e91 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -53,7 +53,7 @@ class PickleReader(PickleHandler, FileReader): if len(i) != item_count: raise ValueError('Received an object with %d items, expecting %d.' % (len(i), item_count, )) - yield dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i)) + yield self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))) class PickleWriter(PickleHandler, FileWriter): diff --git a/bonobo/settings.py b/bonobo/settings.py index dda7ba7..9481bb2 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -2,6 +2,8 @@ import os import logging +from bonobo.errors import ValidationError + def to_bool(s): if len(s): @@ -23,7 +25,30 @@ QUIET = to_bool(os.environ.get('QUIET', 'f')) # Logging level. LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO +# Input/Output format for transformations +IOFORMAT_ARG0 = 'arg0' +IOFORMAT_KWARGS = 'kwargs' + +IOFORMATS = { + IOFORMAT_ARG0, + IOFORMAT_KWARGS, +} + +IOFORMAT = os.environ.get('IOFORMAT', IOFORMAT_KWARGS) + + +def validate_io_format(v): + if callable(v): + return v + if v in IOFORMATS: + return v + raise ValidationError('Unsupported format {!r}.'.format(v)) + def check(): if DEBUG and QUIET: raise RuntimeError('I cannot be verbose and quiet at the same time.') + + if IOFORMAT not in IOFORMATS: + raise RuntimeError('Invalid default input/output format.') + From d19178a28eb71b49824b889bf7a4519051f8ee38 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 6 Jun 2017 08:17:58 +0200 Subject: [PATCH 089/143] Fix import to absolute. --- bonobo/nodes/io/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index 74857db..f355c02 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -1,7 +1,7 @@ import json from bonobo.config.processors import ContextProcessor -from .file import FileWriter, FileReader +from bonobo.nodes.io.file import FileWriter, FileReader class JsonHandler(): From 1ca48d885d05c8aa5505c0b9a1b9ead99503989e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 8 Jun 2017 21:47:01 +0200 Subject: [PATCH 090/143] Refactoring and fixes around ioformats. --- Makefile | 2 +- Projectfile | 3 +- bonobo/commands/run.py | 1 + bonobo/examples/files/csv_handlers.py | 2 +- bonobo/examples/files/pickle_handlers.py | 7 +--- bonobo/examples/tutorials/tut01e02.py | 6 ++- bonobo/nodes/io/csv.py | 2 +- bonobo/nodes/io/file.py | 4 +- bonobo/settings.py | 47 ++++++++++++++++-------- bonobo/util/iterators.py | 2 +- requirements-dev.txt | 4 +- requirements-docker.txt | 6 +-- requirements-jupyter.txt | 2 +- requirements.txt | 4 +- setup.py | 3 +- tests/io/test_csv.py | 10 ++--- tests/io/test_json.py | 6 +-- tests/io/test_pickle.py | 7 ++-- 18 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Makefile b/Makefile index 472b625..22a3a14 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-05 09:21:02.910936 +# Updated at 2017-06-08 21:45:05.840502 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/Projectfile b/Projectfile index 70eee1c..6fe6c2b 100644 --- a/Projectfile +++ b/Projectfile @@ -44,8 +44,9 @@ python.add_requirements( 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0', dev=[ - 'pytest-timeout >=1,<2', 'cookiecutter >=1.5,<1.6', + 'pytest-sugar >=0.8,<0.9', + 'pytest-timeout >=1,<2', ], docker=[ 'bonobo-docker', diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index b2e1bcc..c32e394 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -6,6 +6,7 @@ DEFAULT_SERVICES_ATTR = 'get_services' DEFAULT_GRAPH_FILENAME = '__main__.py' DEFAULT_GRAPH_ATTR = 'get_graph' + def get_default_services(filename, services=None): dirname = os.path.dirname(filename) services_filename = os.path.join(dirname, DEFAULT_SERVICES_FILENAME) diff --git a/bonobo/examples/files/csv_handlers.py b/bonobo/examples/files/csv_handlers.py index a15444d..33412c3 100644 --- a/bonobo/examples/files/csv_handlers.py +++ b/bonobo/examples/files/csv_handlers.py @@ -2,7 +2,7 @@ import bonobo from bonobo.commands.run import get_default_services graph = bonobo.Graph( - bonobo.CsvReader('datasets/coffeeshops.txt', headers=('item',)), + bonobo.CsvReader('datasets/coffeeshops.txt', headers=('item', )), bonobo.PrettyPrinter(), ) diff --git a/bonobo/examples/files/pickle_handlers.py b/bonobo/examples/files/pickle_handlers.py index 6863076..71a2b9a 100644 --- a/bonobo/examples/files/pickle_handlers.py +++ b/bonobo/examples/files/pickle_handlers.py @@ -52,12 +52,7 @@ graph = bonobo.Graph( def get_services(): - return { - 'fs': - TarFS( - bonobo.get_examples_path('datasets/spam.tgz') - ) - } + return {'fs': TarFS(bonobo.get_examples_path('datasets/spam.tgz'))} if __name__ == '__main__': diff --git a/bonobo/examples/tutorials/tut01e02.py b/bonobo/examples/tutorials/tut01e02.py index 3784235..78b7f43 100644 --- a/bonobo/examples/tutorials/tut01e02.py +++ b/bonobo/examples/tutorials/tut01e02.py @@ -1,7 +1,11 @@ import bonobo graph = bonobo.Graph( - ['foo', 'bar', 'baz', ], + [ + 'foo', + 'bar', + 'baz', + ], str.upper, print, ) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index bf3872d..da70444 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -59,7 +59,7 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) yield self.get_output(dict(zip(_headers, row))) diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index 3e2c51d..53ba138 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -21,10 +21,8 @@ class FileHandler(Configurable): eol = Option(str, default='\n') # type: str mode = Option(str) # type: str encoding = Option(str, default='utf-8') # type: str - fs = Service('fs') # type: str - - ioformat = Option(settings.validate_io_format, default=settings.IOFORMAT) + ioformat = Option(default=settings.IOFORMAT.get) @ContextProcessor def file(self, context, fs): diff --git a/bonobo/settings.py b/bonobo/settings.py index 9481bb2..e0e5289 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -1,6 +1,5 @@ -import os - import logging +import os from bonobo.errors import ValidationError @@ -13,6 +12,36 @@ def to_bool(s): return False +class Setting: + def __init__(self, name, default=None, validator=None): + self.name = name + + if default: + self.default = default if callable(default) else lambda: default + else: + self.default = lambda: None + + if validator: + self.validator = validator + else: + self.validator = None + + def __repr__(self): + return ''.format(self.name, self.value) + + def set(self, value): + if self.validator and not self.validator(value): + raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) + self.value = value + + def get(self): + try: + return self.value + except AttributeError: + self.value = self.default() + return self.value + + # Debug/verbose mode. DEBUG = to_bool(os.environ.get('DEBUG', 'f')) @@ -34,21 +63,9 @@ IOFORMATS = { IOFORMAT_KWARGS, } -IOFORMAT = os.environ.get('IOFORMAT', IOFORMAT_KWARGS) - - -def validate_io_format(v): - if callable(v): - return v - if v in IOFORMATS: - return v - raise ValidationError('Unsupported format {!r}.'.format(v)) +IOFORMAT = Setting('IOFORMAT', default=IOFORMAT_KWARGS, validator=IOFORMATS.__contains__) def check(): if DEBUG and QUIET: raise RuntimeError('I cannot be verbose and quiet at the same time.') - - if IOFORMAT not in IOFORMATS: - raise RuntimeError('Invalid default input/output format.') - diff --git a/bonobo/util/iterators.py b/bonobo/util/iterators.py index ae39a49..82f8518 100644 --- a/bonobo/util/iterators.py +++ b/bonobo/util/iterators.py @@ -21,7 +21,7 @@ def force_iterator(mixed): def ensure_tuple(tuple_or_mixed): if isinstance(tuple_or_mixed, tuple): return tuple_or_mixed - return (tuple_or_mixed,) + return (tuple_or_mixed, ) def tuplize(generator): diff --git a/requirements-dev.txt b/requirements-dev.txt index c499557..a8ae766 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ arrow==0.10.0 babel==2.4.0 binaryornot==0.4.3 certifi==2017.4.17 -chardet==3.0.3 +chardet==3.0.4 click==6.7 cookiecutter==1.5.1 coverage==4.4.1 @@ -19,6 +19,7 @@ poyo==0.4.1 py==1.4.34 pygments==2.2.0 pytest-cov==2.5.1 +pytest-sugar==0.8.0 pytest-timeout==1.2.0 pytest==3.1.1 python-dateutil==2.6.0 @@ -28,5 +29,6 @@ six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 +termcolor==1.1.0 urllib3==1.21.1 whichcraft==0.4.1 diff --git a/requirements-docker.txt b/requirements-docker.txt index 792a827..19ae90f 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,9 +1,9 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.4 +bonobo-docker==0.2.5 bonobo==0.3.1 certifi==2017.4.17 -chardet==3.0.3 +chardet==3.0.4 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 @@ -17,6 +17,6 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.17.3 six==1.10.0 -stevedore==1.22.0 +stevedore==1.23.0 urllib3==1.21.1 websocket-client==0.40.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 921057c..7db4a67 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -2,7 +2,7 @@ appnope==0.1.0 bleach==2.0.0 decorator==4.0.11 -entrypoints==0.2.2 +entrypoints==0.2.3 html5lib==0.999999999 ipykernel==4.6.1 ipython-genutils==0.2.0 diff --git a/requirements.txt b/requirements.txt index 38ba798..ab50600 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -e . appdirs==1.4.3 certifi==2017.4.17 -chardet==3.0.3 +chardet==3.0.4 colorama==0.3.9 enum34==1.1.6 fs==2.0.3 @@ -13,5 +13,5 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.17.3 six==1.10.0 -stevedore==1.22.0 +stevedore==1.23.0 urllib3==1.21.1 diff --git a/setup.py b/setup.py index 540ea1b..89c9ccd 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,8 @@ setup( extras_require={ 'dev': [ 'cookiecutter (>= 1.5, < 1.6)', 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', - 'pytest-cov (>= 2.5, < 3.0)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' + 'pytest-cov (>= 2.5, < 3.0)', 'pytest-sugar (>= 0.8, < 0.9)', 'pytest-timeout (>= 1, < 2)', + 'sphinx (>= 1.6, < 2.0)' ], 'docker': ['bonobo-docker'], 'jupyter': ['ipywidgets (>= 6.0.0, < 7)', 'jupyter (>= 1.0, < 1.1)'] diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 3a6fd37..47e641b 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -1,6 +1,6 @@ import pytest -from bonobo import Bag, CsvReader, CsvWriter, open_fs +from bonobo import Bag, CsvReader, CsvWriter, open_fs, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext @@ -9,7 +9,7 @@ from bonobo.util.testing import CapturingNodeExecutionContext def test_write_csv_to_file(tmpdir): fs, filename = open_fs(tmpdir), 'output.csv' - writer = CsvWriter(path=filename) + writer = CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0) context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) @@ -19,7 +19,7 @@ def test_write_csv_to_file(tmpdir): context.step() context.stop() - with fs.open(filename)as fp: + with fs.open(filename) as fp: assert fp.read() == 'foo\nbar\nbaz\n' with pytest.raises(AttributeError): @@ -31,7 +31,7 @@ def test_read_csv_from_file(tmpdir): with fs.open(filename, 'w') as fp: fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') - reader = CsvReader(path=filename, delimiter=',') + reader = CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0) context = CapturingNodeExecutionContext(reader, services={'fs': fs}) @@ -64,7 +64,7 @@ def test_read_csv_kwargs_output_formater(tmpdir): with fs.open(filename, 'w') as fp: fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') - reader = CsvReader(path=filename, delimiter=',', output_format='kwargs') + reader = CsvReader(path=filename, delimiter=',') context = CapturingNodeExecutionContext(reader, services={'fs': fs}) diff --git a/tests/io/test_json.py b/tests/io/test_json.py index 56f679f..15d9e7e 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -1,6 +1,6 @@ import pytest -from bonobo import Bag, JsonReader, JsonWriter, open_fs +from bonobo import Bag, JsonReader, JsonWriter, open_fs, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext @@ -9,7 +9,7 @@ from bonobo.util.testing import CapturingNodeExecutionContext def test_write_json_to_file(tmpdir): fs, filename = open_fs(tmpdir), 'output.json' - writer = JsonWriter(path=filename) + writer = JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0) context = NodeExecutionContext(writer, services={'fs': fs}) context.start() @@ -31,7 +31,7 @@ def test_read_json_from_file(tmpdir): fs, filename = open_fs(tmpdir), 'input.json' with fs.open(filename, 'w') as fp: fp.write('[{"x": "foo"},{"x": "bar"}]') - reader = JsonReader(path=filename) + reader = JsonReader(filename, ioformat=settings.IOFORMAT_ARG0) context = CapturingNodeExecutionContext(reader, services={'fs': fs}) diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index 368e526..1709b16 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -1,7 +1,8 @@ import pickle + import pytest -from bonobo import Bag, PickleReader, PickleWriter, open_fs +from bonobo import Bag, PickleReader, PickleWriter, open_fs, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext @@ -10,7 +11,7 @@ from bonobo.util.testing import CapturingNodeExecutionContext def test_write_pickled_dict_to_file(tmpdir): fs, filename = open_fs(tmpdir), 'output.pkl' - writer = PickleWriter(path=filename) + writer = PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0) context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) @@ -32,7 +33,7 @@ def test_read_pickled_list_from_file(tmpdir): with fs.open(filename, 'wb') as fp: fp.write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) - reader = PickleReader(path=filename) + reader = PickleReader(filename, ioformat=settings.IOFORMAT_ARG0) context = CapturingNodeExecutionContext(reader, services={'fs': fs}) From 89104af3a000bf0c27deecfea500ec0744117936 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 8 Jun 2017 21:47:13 +0200 Subject: [PATCH 091/143] Allow main as well as __main__. --- bonobo/commands/init.py | 6 ++++-- bonobo/commands/run.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index a937041..de55251 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -1,4 +1,4 @@ -def execute(name): +def execute(name, branch): try: from cookiecutter.main import cookiecutter except ImportError as exc: @@ -7,10 +7,12 @@ def execute(name): ) from exc return cookiecutter( - 'https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True + 'https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True, + checkout=branch ) def register(parser): parser.add_argument('name') + parser.add_argument('--branch', '-b', default='master') return execute diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index c32e394..cb3c62e 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -3,7 +3,7 @@ import os DEFAULT_SERVICES_FILENAME = '_services.py' DEFAULT_SERVICES_ATTR = 'get_services' -DEFAULT_GRAPH_FILENAME = '__main__.py' +DEFAULT_GRAPH_FILENAMES = ('__main__.py', 'main.py',) DEFAULT_GRAPH_ATTR = 'get_graph' @@ -49,7 +49,14 @@ def execute(filename, module, install=False, quiet=False, verbose=False): pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources) import site importlib.reload(site) - filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME) + + pathname = filename + for filename in DEFAULT_GRAPH_FILENAMES: + filename = os.path.join(pathname, filename) + if os.path.exists(filename): + break + if not os.path.exists(filename): + raise IOError('Could not find entrypoint (candidates: {}).'.format(', '.join(DEFAULT_GRAPH_FILENAMES))) elif install: raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).') context = runpy.run_path(filename, run_name='__bonobo__') From 2b584935c590e22a235b9518f0dad0a9dbd973a3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 10 Jun 2017 13:35:02 +0200 Subject: [PATCH 092/143] [deps] Weekly dependency update. --- Makefile | 2 +- bonobo/commands/init.py | 4 +++- bonobo/commands/run.py | 2 +- requirements-dev.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 22a3a14..ce3ebff 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-08 21:45:05.840502 +# Updated at 2017-06-10 13:33:51.811144 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/bonobo/commands/init.py b/bonobo/commands/init.py index de55251..948f747 100644 --- a/bonobo/commands/init.py +++ b/bonobo/commands/init.py @@ -7,7 +7,9 @@ def execute(name, branch): ) from exc return cookiecutter( - 'https://github.com/python-bonobo/cookiecutter-bonobo.git', extra_context={'name': name}, no_input=True, + 'https://github.com/python-bonobo/cookiecutter-bonobo.git', + extra_context={'name': name}, + no_input=True, checkout=branch ) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index cb3c62e..7f29d3f 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -3,7 +3,7 @@ import os DEFAULT_SERVICES_FILENAME = '_services.py' DEFAULT_SERVICES_ATTR = 'get_services' -DEFAULT_GRAPH_FILENAMES = ('__main__.py', 'main.py',) +DEFAULT_GRAPH_FILENAMES = ('__main__.py', 'main.py', ) DEFAULT_GRAPH_ATTR = 'get_graph' diff --git a/requirements-dev.txt b/requirements-dev.txt index a8ae766..c9c94bb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ pygments==2.2.0 pytest-cov==2.5.1 pytest-sugar==0.8.0 pytest-timeout==1.2.0 -pytest==3.1.1 +pytest==3.1.2 python-dateutil==2.6.0 pytz==2017.2 requests==2.17.3 From e0a00dcc91f6d9b65d4eef58a6dcf20169ce9f6b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 10 Jun 2017 13:57:27 +0200 Subject: [PATCH 093/143] release: 0.3.2 --- Makefile | 2 +- bonobo/_version.py | 2 +- docs/changelog.rst | 15 +++++++++++++++ requirements-dev.txt | 7 +++---- requirements-jupyter.txt | 4 ++-- requirements.txt | 4 ++-- tests/test_config_method.py | 2 -- 7 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index a1d49da..cf01a16 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-05-28 12:24:17.298429 +# Updated at 2017-06-10 13:54:56.092476 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/bonobo/_version.py b/bonobo/_version.py index e1424ed..73e3bb4 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.3.1' +__version__ = '0.3.2' diff --git a/docs/changelog.rst b/docs/changelog.rst index c386ff7..5fe968b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,21 @@ Changelog ========= +v.0.3.2 - 10 june 2017 +:::::::::::::::::::::: + +Weekly maintenance release. + +* Updated frozen version numbers in requirements. + + * pytest==3.1.1 + * requests==2.17.3 + * sphinx==1.6.2 + * stevedore==1.22.0 + + Note: this does not change anything when used as a dependency if you freeze your requirements, as the setup.py + requirement specifiers did not change. + v.0.3.1 - 28 may 2017 ::::::::::::::::::::: diff --git a/requirements-dev.txt b/requirements-dev.txt index c7a14c6..e604a97 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,23 +2,22 @@ alabaster==0.7.10 babel==2.4.0 certifi==2017.4.17 -chardet==3.0.3 +chardet==3.0.4 coverage==4.4.1 docutils==0.13.1 idna==2.5 imagesize==0.7.1 jinja2==2.9.6 markupsafe==1.0 -py==1.4.33 +py==1.4.34 pygments==2.2.0 pytest-cov==2.5.1 pytest-timeout==1.2.0 -pytest==3.1.1 +pytest==3.1.2 pytz==2017.2 requests==2.17.3 six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 -typing==3.6.1 urllib3==1.21.1 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 1e98481..7db4a67 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -2,11 +2,11 @@ appnope==0.1.0 bleach==2.0.0 decorator==4.0.11 -entrypoints==0.2.2 +entrypoints==0.2.3 html5lib==0.999999999 ipykernel==4.6.1 ipython-genutils==0.2.0 -ipython==6.0.0 +ipython==6.1.0 ipywidgets==6.0.0 jedi==0.10.2 jinja2==2.9.6 diff --git a/requirements.txt b/requirements.txt index 8e999c2..b4cef9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -e . appdirs==1.4.3 certifi==2017.4.17 -chardet==3.0.3 +chardet==3.0.4 colorama==0.3.9 enum34==1.1.6 fs==2.0.3 @@ -11,5 +11,5 @@ psutil==5.2.2 pytz==2017.2 requests==2.17.3 six==1.10.0 -stevedore==1.22.0 +stevedore==1.23.0 urllib3==1.21.1 diff --git a/tests/test_config_method.py b/tests/test_config_method.py index 13eb873..3a5f6a3 100644 --- a/tests/test_config_method.py +++ b/tests/test_config_method.py @@ -28,8 +28,6 @@ def test_define_with_decorator(): def Concrete(self, *args, **kwargs): calls.append((args, kwargs, )) - print('handler', Concrete.handler) - assert callable(Concrete.handler) t = Concrete('foo', bar='baz') From fb54143a7a24fd8171faa08106c2fc58c5153e42 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 10 Jun 2017 15:34:22 +0200 Subject: [PATCH 094/143] release: 0.4.0 --- Makefile | 2 +- README.rst | 12 +++---- bonobo/_version.py | 2 +- docs/changelog.rst | 68 +++++++++++++++++++++++++++++++++++++++ docs/contribute/index.rst | 4 +-- docs/faq.rst | 2 +- docs/install.rst | 4 +-- docs/tutorial/tut02.rst | 2 +- requirements-docker.txt | 3 +- 9 files changed, 83 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index b13565f..f782b7e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-10 14:17:07.161053 +# Updated at 2017-06-10 15:15:34.093885 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/README.rst b/README.rst index 59b1d10..6c56328 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ Data-processing for humans. .. image:: https://img.shields.io/pypi/v/bonobo.svg :target: https://pypi.python.org/pypi/bonobo :alt: PyPI - + .. image:: https://img.shields.io/pypi/pyversions/bonobo.svg :target: https://pypi.python.org/pypi/bonobo :alt: Versions @@ -16,20 +16,20 @@ Data-processing for humans. :target: http://docs.bonobo-project.org/ :alt: Documentation -.. image:: https://travis-ci.org/python-bonobo/bonobo.svg?branch=0.3 +.. image:: https://travis-ci.org/python-bonobo/bonobo.svg?branch=master :target: https://travis-ci.org/python-bonobo/bonobo :alt: Continuous Integration (Linux) -.. image:: https://ci.appveyor.com/api/projects/status/github/python-bonobo/bonobo?retina=true&branch=0.3&svg=true - :target: https://ci.appveyor.com/project/hartym/bonobo?branch=0.3 +.. image:: https://ci.appveyor.com/api/projects/status/github/python-bonobo/bonobo?retina=true&branch=master&svg=true + :target: https://ci.appveyor.com/project/hartym/bonobo?branch=master :alt: Continuous Integration (Windows) .. image:: https://codeclimate.com/github/python-bonobo/bonobo/badges/gpa.svg :target: https://codeclimate.com/github/python-bonobo/bonobo :alt: Code Climate -.. image:: https://img.shields.io/coveralls/python-bonobo/bonobo/0.3.svg - :target: https://coveralls.io/github/python-bonobo/bonobo?branch=0.3 +.. image:: https://img.shields.io/coveralls/python-bonobo/bonobo/master.svg + :target: https://coveralls.io/github/python-bonobo/bonobo?branch=master :alt: Coverage Bonobo is an extract-transform-load framework for python 3.5+ (see comparisons with other data tools). diff --git a/bonobo/_version.py b/bonobo/_version.py index 73e3bb4..abeeedb 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.3.2' +__version__ = '0.4.0' diff --git a/docs/changelog.rst b/docs/changelog.rst index 5fe968b..f447fc9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,74 @@ Changelog ========= +v.0.4.0 - 10 june 2017 +:::::::::::::::::::::: + +Important highlights +-------------------- + +* **BC BREAK WARNING** New IOFORMAT option determines the default expected input and output format of transformations. + New default input/output format of transformations is now kwargs-based, instead of first-argument based. The + rationale behind this is that it does not make any sense to put a dict as the only argument of a transformation + knowing that python has a well supported syntax to do so already. Of course, it may break some of your + transformations but you can require the old behaviour by setting the IOFORMAT=arg0 environment variable. + +New features +------------ + +Command line interface +...................... + +* Allow to run directories or modules using "bonobo run". +* Bonobo version command now shows where the package is installed, and an optional "--all/-a" flag show all + extensions in the same way. (#81) +* Bonobo run flag "--install/-I" allow to pip install a requirements.txt file if run targets a directory. (#71) +* Adds python logging facility configuration in bonobo cli commands. +* Bonobo init now uses cookiecutter template. + +Configuration +............. + +* `Exclusive(...)` context manager locks an object usage to one thread at a time. + ([docs](http://docs-dev.bonobo-project.org/en/develop/guide/services.html#solving-concurrency-problems)) + +Standard library +................ + +* New PrettyPrinter and deprecate old crappy modules. +* New pickle reader and writer (thanks @jelloslinger). + +Internals +--------- + +* ConsoleOutputPlugin now buffers stdout to avoid terminal conflicts. Side effect, output is only done every few tenth + of a second. + +Bugfixes +-------- + +* Fixes jupyter widget. + +Extensions +---------- + +* First release officially supporting bonobo-docker extension. See https://www.bonobo-project.org/with/docker. +* Docker extension can be now installed using the "docker" extra on bonobo (`pip install bonobo[docker]`). +* Jupyter widget now displays the status in topological order, like console. + +Miscellaneous +------------- + +* Allow "main.py" as well as "__main__.py" to be the main entrypoint of an etl job. +* Better error display (329296c). +* Better testing. +* Code sweeping (ecfdc81). +* Dependencies updated. +* Filesystem now resolve (expand) ~ in path. +* Moving project artifact management (Projectfile) to edgy.project 0.3 format. +* Refactoring and fixes around ioformats. +* Some really minor changes. + v.0.3.2 - 10 june 2017 :::::::::::::::::::::: diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst index 690cab7..5857207 100644 --- a/docs/contribute/index.rst +++ b/docs/contribute/index.rst @@ -78,7 +78,7 @@ Guidelines License ::::::: -`Bonobo is released under the apache license `_. +`Bonobo is released under the apache license `_. License for non lawyers ::::::::::::::::::::::: @@ -87,6 +87,6 @@ Use it, change it, hack it, brew it, eat it. For pleasure, non-profit, profit or basically anything else, except stealing credit. -Provided without warranty. +Provided without any warranty. diff --git a/docs/faq.rst b/docs/faq.rst index 6f3cd3f..5b25c7b 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -62,7 +62,7 @@ context. The API may evolve a bit though, because I feel it's a bit hackish, as it is. The concept will stay the same, but we need to find a better way to apply it. -To understand how it works today, look at https://github.com/python-bonobo/bonobo/blob/0.3/bonobo/io/csv.py#L63 and class hierarchy. +To understand how it works today, look at https://github.com/python-bonobo/bonobo/blob/master/bonobo/nodes/io/csv.py#L31 and class hierarchy. What is a plugin? Do I need to write one? diff --git a/docs/install.rst b/docs/install.rst index 943ffbe..41487e4 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -26,7 +26,7 @@ but editable installs (see below). .. code-block:: shell-session - $ pip install git+https://github.com/python-bonobo/bonobo.git@0.3#egg=bonobo + $ pip install git+https://github.com/python-bonobo/bonobo.git@master#egg=bonobo Editable install :::::::::::::::: @@ -37,7 +37,7 @@ python interpreter. .. code-block:: shell-session - $ pip install --editable git+https://github.com/python-bonobo/bonobo.git@0.3#egg=bonobo + $ pip install --editable git+https://github.com/python-bonobo/bonobo.git@master#egg=bonobo .. note:: You can also use the `-e` flag instead of the long version. diff --git a/docs/tutorial/tut02.rst b/docs/tutorial/tut02.rst index 685e455..ff562d1 100644 --- a/docs/tutorial/tut02.rst +++ b/docs/tutorial/tut02.rst @@ -52,7 +52,7 @@ We'll use a text file that was generated using Bonobo from the "liste-des-cafes- Mairie de Paris under the Open Database License (ODbL). You can `explore the original dataset `_. -You'll need the `example dataset `_, +You'll need the `example dataset `_, available in **Bonobo**'s repository. .. literalinclude:: ../../bonobo/examples/tutorials/tut02e01_read.py diff --git a/requirements-docker.txt b/requirements-docker.txt index 12fcf2e..fc84888 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,7 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.5 -bonobo==0.3.2 +bonobo-docker==0.2.6 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 From 2b8397f32ed2b81ee945e4b8869b2f5eb98beca7 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 11 Jun 2017 09:50:20 +0200 Subject: [PATCH 095/143] [logging] Adds logging alias for easier imports. --- bonobo/logging.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bonobo/logging.py b/bonobo/logging.py index 1561089..4626243 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -70,6 +70,9 @@ def set_level(level): def get_logger(name='bonobo'): return logging.getLogger(name) +# Compatibility with python logging +getLogger = get_logger + # Setup formating and level. setup(level=settings.LOGGING_LEVEL) From f832f8e2bcd2840cbec16a7da3d20b8276b393b0 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 11 Jun 2017 10:44:01 +0200 Subject: [PATCH 096/143] release: 0.4.1 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index abeeedb..f0ede3d 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' From 079ad57a615a05f6943d85739efe5032c26e8436 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 11 Jun 2017 17:15:17 +0200 Subject: [PATCH 097/143] Update fs from 2.0.3 to 2.0.4 --- requirements-docker.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docker.txt b/requirements-docker.txt index fc84888..457488e 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -7,7 +7,7 @@ colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 enum34==1.1.6 -fs==2.0.3 +fs==2.0.4 idna==2.5 pbr==3.0.1 psutil==5.2.2 From 6e373a583d9494190696ef342b80cec931200751 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 11 Jun 2017 17:15:19 +0200 Subject: [PATCH 098/143] Update fs from 2.0.3 to 2.0.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ab50600..8ee6330 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 enum34==1.1.6 -fs==2.0.3 +fs==2.0.4 idna==2.5 packaging==16.8 pbr==3.0.1 From 735ae43fc6447cabaeb53f946c195ba86a52bda5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 11 Jun 2017 22:14:16 +0200 Subject: [PATCH 099/143] Update bonobo-docker from 0.2.6 to 0.2.8 --- requirements-docker.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docker.txt b/requirements-docker.txt index fc84888..1443f7f 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,6 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.6 +bonobo-docker==0.2.8 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 From 6a0334dcdf1bc987e852cdf0d9953b0b5a9811ce Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 12 Jun 2017 16:24:38 +0200 Subject: [PATCH 100/143] Create index.html --- docs/_templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_templates/index.html b/docs/_templates/index.html index a2ee73b..b778554 100644 --- a/docs/_templates/index.html +++ b/docs/_templates/index.html @@ -49,7 +49,7 @@
    From dbb6491e7f2cffa429a92dc5af8012d36b1cf7ea Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 14 Jun 2017 21:38:35 +0200 Subject: [PATCH 101/143] Update requests from 2.17.3 to 2.18.1 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c9c94bb..6c33b15 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,7 +24,7 @@ pytest-timeout==1.2.0 pytest==3.1.2 python-dateutil==2.6.0 pytz==2017.2 -requests==2.17.3 +requests==2.18.1 six==1.10.0 snowballstemmer==1.2.1 sphinx==1.6.2 From c42520987f191b93866a8a88d0c30a84ab271b78 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 14 Jun 2017 21:38:37 +0200 Subject: [PATCH 102/143] Update requests from 2.17.3 to 2.18.1 --- requirements-docker.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docker.txt b/requirements-docker.txt index 21a0cac..b004174 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -12,7 +12,7 @@ idna==2.5 pbr==3.0.1 psutil==5.2.2 pytz==2017.2 -requests==2.17.3 +requests==2.18.1 six==1.10.0 stevedore==1.23.0 urllib3==1.21.1 From e8ab8a8b576a051af061ffb272b3ab7e4cf1796d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 14 Jun 2017 21:38:38 +0200 Subject: [PATCH 103/143] Update requests from 2.17.3 to 2.18.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ee6330..478879e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pbr==3.0.1 psutil==5.2.2 pyparsing==2.2.0 pytz==2017.2 -requests==2.17.3 +requests==2.18.1 six==1.10.0 stevedore==1.23.0 urllib3==1.21.1 From 8c898afc273e74e9773bde0a8a489a3defaa5474 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 17 Jun 2017 08:02:29 +0200 Subject: [PATCH 104/143] [config] Implements a "requires()" service injection decorator for functions (api may change). --- bonobo/config/__init__.py | 5 +++-- bonobo/config/services.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/bonobo/config/__init__.py b/bonobo/config/__init__.py index 8e662c4..08be544 100644 --- a/bonobo/config/__init__.py +++ b/bonobo/config/__init__.py @@ -1,7 +1,7 @@ from bonobo.config.configurables import Configurable -from bonobo.config.options import Option, Method +from bonobo.config.options import Method, Option from bonobo.config.processors import ContextProcessor -from bonobo.config.services import Container, Service, Exclusive +from bonobo.config.services import Container, Exclusive, Service, requires # bonobo.config public programming interface __all__ = [ @@ -12,4 +12,5 @@ __all__ = [ 'Method', 'Option', 'Service', + 'requires', ] diff --git a/bonobo/config/services.py b/bonobo/config/services.py index 4a91668..d792175 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -56,7 +56,11 @@ class Service(Option): inst.__options_values__[self.name] = validate_service_name(value) def resolve(self, inst, services): - return services.get(getattr(inst, self.name)) + try: + name = getattr(inst, self.name) + except AttributeError: + name = self.name + return services.get(name) class Container(dict): @@ -126,3 +130,19 @@ class Exclusive(ContextDecorator): def __exit__(self, *exc): self.get_lock().release() + + +def requires(*service_names): + def decorate(mixed): + try: + options = mixed.__options__ + except AttributeError: + mixed.__options__ = options = {} + + for service_name in service_names: + service = Service(service_name) + service.name = service_name + options[service_name] = service + return mixed + + return decorate From a65ca635cf953ba8ac4b4a1d9fb7378e53bcce27 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 17 Jun 2017 10:08:06 +0200 Subject: [PATCH 105/143] [fs] adds a defaut to current working directory in open_fs(...). --- bonobo/_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 9317e31..cf28a33 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -68,7 +68,7 @@ register_api(create_strategy) # Shortcut to filesystem2's open_fs, that we make available there for convenience. @register_api -def open_fs(fs_url, *args, **kwargs): +def open_fs(fs_url=None, *args, **kwargs): """ Wraps :func:`fs.open_fs` function with a few candies. @@ -83,6 +83,10 @@ def open_fs(fs_url, *args, **kwargs): """ from fs import open_fs as _open_fs from os.path import expanduser + from os import getcwd + + if fs_url is None: + fs_url = getcwd() return _open_fs(expanduser(str(fs_url)), *args, **kwargs) From 3c4010f9c3113ac27c4f36f482b0d4c3c9604fb6 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 17 Jun 2017 10:17:42 +0200 Subject: [PATCH 106/143] [core] Execution contexts are now context managers. --- bonobo/execution/base.py | 7 +++++++ bonobo/nodes/io/csv.py | 2 +- bonobo/strategies/executor.py | 14 +++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/bonobo/execution/base.py b/bonobo/execution/base.py index 779f212..641d761 100644 --- a/bonobo/execution/base.py +++ b/bonobo/execution/base.py @@ -58,6 +58,13 @@ class LoopingExecutionContext(Wrapper): # XXX enhancers self._enhancers = get_enhancers(self.wrapped) + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + self.stop() + def start(self): if self.started: raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self))) diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index da70444..bf3872d 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -59,7 +59,7 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) yield self.get_output(dict(zip(_headers, row))) diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index 26b810b..d2cdcbe 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -1,6 +1,5 @@ import time import traceback - from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor from bonobo.constants import BEGIN, END @@ -29,19 +28,16 @@ class ExecutorStrategy(Strategy): futures = [] for plugin_context in context.plugins: - def _runner(plugin_context=plugin_context): - try: - plugin_context.start() - plugin_context.loop() - plugin_context.stop() - except Exception as exc: - print_error(exc, traceback.format_exc(), context=plugin_context) + with plugin_context: + try: + plugin_context.loop() + except Exception as exc: + print_error(exc, traceback.format_exc(), context=plugin_context) futures.append(executor.submit(_runner)) for node_context in context.nodes: - def _runner(node_context=node_context): try: node_context.start() From 67b42274366e021768f52afb1c7bc2454d915778 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 17 Jun 2017 10:37:17 +0200 Subject: [PATCH 107/143] [stdlib] Fix I/O related nodes (especially json), there were bad bugs with ioformat. --- .gitignore | 9 +---- bonobo/logging.py | 2 +- bonobo/nodes/io/base.py | 82 +++++++++++++++++++++++++++++++++++++++ bonobo/nodes/io/csv.py | 9 +++-- bonobo/nodes/io/file.py | 82 +++------------------------------------ bonobo/nodes/io/json.py | 17 +++++--- bonobo/nodes/io/pickle.py | 10 ++--- bonobo/util/testing.py | 18 +++++++++ config/__init__.py | 0 tests/io/test_csv.py | 78 ++++++++++++++++++++----------------- tests/io/test_file.py | 60 ++++++++++++---------------- tests/io/test_json.py | 56 +++++++++++++------------- tests/io/test_pickle.py | 39 ++++++++----------- 13 files changed, 243 insertions(+), 219 deletions(-) create mode 100644 bonobo/nodes/io/base.py delete mode 100644 config/__init__.py diff --git a/.gitignore b/.gitignore index d48b40b..f16c58c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *,cover *.egg *.egg-info/ +*.iml *.log *.manifest *.mo @@ -20,25 +21,17 @@ .installed.cfg .ipynb_checkpoints .python-version -.tox/ -.webassets-cache /.idea /.release -/bonobo.iml /bonobo/examples/work_in_progress/ /bonobo/ext/jupyter/js/node_modules/ /build/ /coverage.xml -/develop-eggs/ /dist/ /docs/_build/ -/downloads/ /eggs/ /examples/private -/htmlcov/ /sdist/ /tags -celerybeat-schedule -parts/ pip-delete-this-directory.txt pip-log.txt diff --git a/bonobo/logging.py b/bonobo/logging.py index 4626243..17bdeb7 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -70,9 +70,9 @@ def set_level(level): def get_logger(name='bonobo'): return logging.getLogger(name) + # Compatibility with python logging getLogger = get_logger - # Setup formating and level. setup(level=settings.LOGGING_LEVEL) diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py new file mode 100644 index 0000000..d9b3212 --- /dev/null +++ b/bonobo/nodes/io/base.py @@ -0,0 +1,82 @@ +from bonobo import settings +from bonobo.config import Configurable, ContextProcessor, Option, Service +from bonobo.structs.bags import Bag + + +class IOFormatEnabled(Configurable): + ioformat = Option(default=settings.IOFORMAT.get) + + def get_input(self, *args, **kwargs): + if self.ioformat == settings.IOFORMAT_ARG0: + if len(args) != 1 or len(kwargs): + raise ValueError( + 'Wrong input formating: IOFORMAT=ARG0 implies one arg and no kwargs, got args={!r} and kwargs={!r}.'. + format(args, kwargs) + ) + return args[0] + + if self.ioformat == settings.IOFORMAT_KWARGS: + if len(args) or not len(kwargs): + raise ValueError( + 'Wrong input formating: IOFORMAT=KWARGS ioformat implies no arg, got args={!r} and kwargs={!r}.'. + format(args, kwargs) + ) + return kwargs + + raise NotImplementedError('Unsupported format.') + + def get_output(self, row): + if self.ioformat == settings.IOFORMAT_ARG0: + return row + + if self.ioformat == settings.IOFORMAT_KWARGS: + return Bag(**row) + + raise NotImplementedError('Unsupported format.') + + +class FileHandler(Configurable): + """Abstract component factory for file-related components. + + Args: + path (str): which path to use within the provided filesystem. + eol (str): which character to use to separate lines. + mode (str): which mode to use when opening the file. + fs (str): service name to use for filesystem. + """ + + path = Option(str, required=True, positional=True) # type: str + eol = Option(str, default='\n') # type: str + mode = Option(str) # type: str + encoding = Option(str, default='utf-8') # type: str + fs = Service('fs') # type: str + + @ContextProcessor + def file(self, context, fs): + with self.open(fs) as file: + yield file + + def open(self, fs): + return fs.open(self.path, self.mode, encoding=self.encoding) + + +class Reader: + """Abstract component factory for readers. + """ + + def __call__(self, *args, **kwargs): + yield from self.read(*args, **kwargs) + + def read(self, *args, **kwargs): + raise NotImplementedError('Abstract.') + + +class Writer: + """Abstract component factory for writers. + """ + + def __call__(self, *args, **kwargs): + return self.write(*args, **kwargs) + + def write(self, *args, **kwargs): + raise NotImplementedError('Abstract.') diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index bf3872d..ae68bd0 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -3,7 +3,8 @@ import csv from bonobo.config import Option from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED -from bonobo.nodes.io.file import FileHandler, FileReader, FileWriter +from bonobo.nodes.io.file import FileReader, FileWriter +from bonobo.nodes.io.base import FileHandler, IOFormatEnabled from bonobo.util.objects import ValueHolder @@ -28,7 +29,7 @@ class CsvHandler(FileHandler): headers = Option(tuple) -class CsvReader(CsvHandler, FileReader): +class CsvReader(IOFormatEnabled, FileReader, CsvHandler): """ Reads a CSV and yield the values as dicts. @@ -59,12 +60,12 @@ class CsvReader(CsvHandler, FileReader): for row in reader: if len(row) != field_count: - raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count,)) + raise ValueError('Got a line with %d fields, expecting %d.' % (len(row), field_count, )) yield self.get_output(dict(zip(_headers, row))) -class CsvWriter(CsvHandler, FileWriter): +class CsvWriter(IOFormatEnabled, FileWriter, CsvHandler): @ContextProcessor def writer(self, context, fs, file, lineno): writer = csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol) diff --git a/bonobo/nodes/io/file.py b/bonobo/nodes/io/file.py index 53ba138..e49d6de 100644 --- a/bonobo/nodes/io/file.py +++ b/bonobo/nodes/io/file.py @@ -1,81 +1,11 @@ -from bonobo import settings -from bonobo.config import Option, Service -from bonobo.config.configurables import Configurable +from bonobo.config import Option from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED -from bonobo.structs.bags import Bag +from bonobo.nodes.io.base import FileHandler, Reader, Writer from bonobo.util.objects import ValueHolder -class FileHandler(Configurable): - """Abstract component factory for file-related components. - - Args: - path (str): which path to use within the provided filesystem. - eol (str): which character to use to separate lines. - mode (str): which mode to use when opening the file. - fs (str): service name to use for filesystem. - """ - - path = Option(str, required=True, positional=True) # type: str - eol = Option(str, default='\n') # type: str - mode = Option(str) # type: str - encoding = Option(str, default='utf-8') # type: str - fs = Service('fs') # type: str - ioformat = Option(default=settings.IOFORMAT.get) - - @ContextProcessor - def file(self, context, fs): - with self.open(fs) as file: - yield file - - def open(self, fs): - return fs.open(self.path, self.mode, encoding=self.encoding) - - def get_input(self, *args, **kwargs): - if self.ioformat == settings.IOFORMAT_ARG0: - assert len(args) == 1 and not len(kwargs), 'ARG0 format implies one arg and no kwargs.' - return args[0] - - if self.ioformat == settings.IOFORMAT_KWARGS: - assert len(args) == 0 and len(kwargs), 'KWARGS format implies no arg.' - return kwargs - - raise NotImplementedError('Unsupported format.') - - def get_output(self, row): - if self.ioformat == settings.IOFORMAT_ARG0: - return row - - if self.ioformat == settings.IOFORMAT_KWARGS: - return Bag(**row) - - raise NotImplementedError('Unsupported format.') - - -class Reader(FileHandler): - """Abstract component factory for readers. - """ - - def __call__(self, *args, **kwargs): - yield from self.read(*args, **kwargs) - - def read(self, *args, **kwargs): - raise NotImplementedError('Abstract.') - - -class Writer(FileHandler): - """Abstract component factory for writers. - """ - - def __call__(self, *args, **kwargs): - return self.write(*args) - - def write(self, *args, **kwargs): - raise NotImplementedError('Abstract.') - - -class FileReader(Reader): +class FileReader(Reader, FileHandler): """Component factory for file-like readers. On its own, it can be used to read a file and yield one row per line, trimming the "eol" character at the end if @@ -93,7 +23,7 @@ class FileReader(Reader): yield line.rstrip(self.eol) -class FileWriter(Writer): +class FileWriter(Writer, FileHandler): """Component factory for file or file-like writers. On its own, it can be used to write in a file one line per row that comes into this component. Extending it is @@ -107,11 +37,11 @@ class FileWriter(Writer): lineno = ValueHolder(0) yield lineno - def write(self, fs, file, lineno, row): + def write(self, fs, file, lineno, line): """ Write a row on the next line of opened file in context. """ - self._write_line(file, (self.eol if lineno.value else '') + row) + self._write_line(file, (self.eol if lineno.value else '') + line) lineno += 1 return NOT_MODIFIED diff --git a/bonobo/nodes/io/json.py b/bonobo/nodes/io/json.py index f355c02..c6d9bf5 100644 --- a/bonobo/nodes/io/json.py +++ b/bonobo/nodes/io/json.py @@ -1,15 +1,17 @@ import json from bonobo.config.processors import ContextProcessor -from bonobo.nodes.io.file import FileWriter, FileReader +from bonobo.constants import NOT_MODIFIED +from bonobo.nodes.io.base import FileHandler, IOFormatEnabled +from bonobo.nodes.io.file import FileReader, FileWriter -class JsonHandler(): +class JsonHandler(FileHandler): eol = ',\n' prefix, suffix = '[', ']' -class JsonReader(JsonHandler, FileReader): +class JsonReader(IOFormatEnabled, FileReader, JsonHandler): loader = staticmethod(json.load) def read(self, fs, file): @@ -17,18 +19,21 @@ class JsonReader(JsonHandler, FileReader): yield self.get_output(line) -class JsonWriter(JsonHandler, FileWriter): +class JsonWriter(IOFormatEnabled, FileWriter, JsonHandler): @ContextProcessor def envelope(self, context, fs, file, lineno): file.write(self.prefix) yield file.write(self.suffix) - def write(self, fs, file, lineno, row): + def write(self, fs, file, lineno, *args, **kwargs): """ Write a json row on the next line of file pointed by ctx.file. :param ctx: :param row: """ - return super().write(fs, file, lineno, json.dumps(row)) + row = self.get_input(*args, **kwargs) + self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row)) + lineno += 1 + return NOT_MODIFIED diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index c603e91..e94f94a 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -1,10 +1,11 @@ import pickle -from bonobo.config.processors import ContextProcessor from bonobo.config import Option +from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED +from bonobo.nodes.io.base import FileHandler, IOFormatEnabled +from bonobo.nodes.io.file import FileReader, FileWriter from bonobo.util.objects import ValueHolder -from .file import FileReader, FileWriter, FileHandler class PickleHandler(FileHandler): @@ -19,7 +20,7 @@ class PickleHandler(FileHandler): item_names = Option(tuple) -class PickleReader(PickleHandler, FileReader): +class PickleReader(IOFormatEnabled, FileReader, PickleHandler): """ Reads a Python pickle object and yields the items in dicts. """ @@ -56,8 +57,7 @@ class PickleReader(PickleHandler, FileReader): yield self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))) -class PickleWriter(PickleHandler, FileWriter): - +class PickleWriter(IOFormatEnabled, FileWriter, PickleHandler): mode = Option(str, default='wb') def write(self, fs, file, lineno, item): diff --git a/bonobo/util/testing.py b/bonobo/util/testing.py index d5b6cc8..7c07256 100644 --- a/bonobo/util/testing.py +++ b/bonobo/util/testing.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from unittest.mock import MagicMock +from bonobo import open_fs from bonobo.execution.node import NodeExecutionContext @@ -17,3 +18,20 @@ def optional_contextmanager(cm, *, ignore=False): else: with cm: yield + + +class FilesystemTester: + def __init__(self, extension='txt', mode='w'): + self.extension = extension + self.input_data = '' + self.mode = mode + + def get_services_for_reader(self, tmpdir): + fs, filename = open_fs(tmpdir), 'input.' + self.extension + with fs.open(filename, self.mode) as fp: + fp.write(self.input_data) + return fs, filename, {'fs': fs} + + def get_services_for_writer(self, tmpdir): + fs, filename = open_fs(tmpdir), 'output.' + self.extension + return fs, filename, {'fs': fs} diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index 47e641b..dc6f71c 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -1,23 +1,21 @@ import pytest -from bonobo import Bag, CsvReader, CsvWriter, open_fs, settings +from bonobo import Bag, CsvReader, CsvWriter, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext +from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester + +csv_tester = FilesystemTester('csv') +csv_tester.input_data = 'a,b,c\na foo,b foo,c foo\na bar,b bar,c bar' -def test_write_csv_to_file(tmpdir): - fs, filename = open_fs(tmpdir), 'output.csv' +def test_write_csv_to_file_arg0(tmpdir): + fs, filename, services = csv_tester.get_services_for_writer(tmpdir) - writer = CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0) - context = NodeExecutionContext(writer, services={'fs': fs}) - - context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) - - context.start() - context.step() - context.step() - context.stop() + with NodeExecutionContext(CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: + context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) + context.step() + context.step() with fs.open(filename) as fp: assert fp.read() == 'foo\nbar\nbaz\n' @@ -26,19 +24,33 @@ def test_write_csv_to_file(tmpdir): getattr(context, 'file') -def test_read_csv_from_file(tmpdir): - fs, filename = open_fs(tmpdir), 'input.csv' - with fs.open(filename, 'w') as fp: - fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') +@pytest.mark.parametrize('add_kwargs', ({}, { + 'ioformat': settings.IOFORMAT_KWARGS, +},)) +def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): + fs, filename, services = csv_tester.get_services_for_writer(tmpdir) - reader = CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0) + with NodeExecutionContext(CsvWriter(path=filename, **add_kwargs), services=services) as context: + context.write(BEGIN, Bag(**{'foo': 'bar'}), Bag(**{'foo': 'baz', 'ignore': 'this'}), END) + context.step() + context.step() - context = CapturingNodeExecutionContext(reader, services={'fs': fs}) + with fs.open(filename) as fp: + assert fp.read() == 'foo\nbar\nbaz\n' - context.start() - context.write(BEGIN, Bag(), END) - context.step() - context.stop() + with pytest.raises(AttributeError): + getattr(context, 'file') + + +def test_read_csv_from_file_arg0(tmpdir): + fs, filename, services = csv_tester.get_services_for_reader(tmpdir) + + with CapturingNodeExecutionContext( + CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), + services=services, + ) as context: + context.write(BEGIN, Bag(), END) + context.step() assert len(context.send.mock_calls) == 2 @@ -59,19 +71,15 @@ def test_read_csv_from_file(tmpdir): } -def test_read_csv_kwargs_output_formater(tmpdir): - fs, filename = open_fs(tmpdir), 'input.csv' - with fs.open(filename, 'w') as fp: - fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') +def test_read_csv_from_file_kwargs(tmpdir): + fs, filename, services = csv_tester.get_services_for_reader(tmpdir) - reader = CsvReader(path=filename, delimiter=',') - - context = CapturingNodeExecutionContext(reader, services={'fs': fs}) - - context.start() - context.write(BEGIN, Bag(), END) - context.step() - context.stop() + with CapturingNodeExecutionContext( + CsvReader(path=filename, delimiter=','), + services=services, + ) as context: + context.write(BEGIN, Bag(), END) + context.step() assert len(context.send.mock_calls) == 2 diff --git a/tests/io/test_file.py b/tests/io/test_file.py index 1566b39..07a15eb 100644 --- a/tests/io/test_file.py +++ b/tests/io/test_file.py @@ -1,9 +1,22 @@ import pytest -from bonobo import Bag, FileReader, FileWriter, open_fs +from bonobo import Bag, FileReader, FileWriter from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext +from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester + +txt_tester = FilesystemTester('txt') +txt_tester.input_data = 'Hello\nWorld\n' + + +def test_file_writer_contextless(tmpdir): + fs, filename, services = txt_tester.get_services_for_writer(tmpdir) + + with FileWriter(path=filename).open(fs) as fp: + fp.write('Yosh!') + + with fs.open(filename) as fp: + assert fp.read() == 'Yosh!' @pytest.mark.parametrize( @@ -14,46 +27,23 @@ from bonobo.util.testing import CapturingNodeExecutionContext ] ) def test_file_writer_in_context(tmpdir, lines, output): - fs, filename = open_fs(tmpdir), 'output.txt' + fs, filename, services = txt_tester.get_services_for_writer(tmpdir) - writer = FileWriter(path=filename) - context = NodeExecutionContext(writer, services={'fs': fs}) - - context.start() - context.write(BEGIN, *map(Bag, lines), END) - for _ in range(len(lines)): - context.step() - context.stop() + with NodeExecutionContext(FileWriter(path=filename), services=services) as context: + context.write(BEGIN, *map(Bag, lines), END) + for _ in range(len(lines)): + context.step() with fs.open(filename) as fp: assert fp.read() == output -def test_file_writer_out_of_context(tmpdir): - fs, filename = open_fs(tmpdir), 'output.txt' +def test_file_reader(tmpdir): + fs, filename, services = txt_tester.get_services_for_reader(tmpdir) - writer = FileWriter(path=filename) - - with writer.open(fs) as fp: - fp.write('Yosh!') - - with fs.open(filename) as fp: - assert fp.read() == 'Yosh!' - - -def test_file_reader_in_context(tmpdir): - fs, filename = open_fs(tmpdir), 'input.txt' - - with fs.open(filename, 'w') as fp: - fp.write('Hello\nWorld\n') - - reader = FileReader(path=filename) - context = CapturingNodeExecutionContext(reader, services={'fs': fs}) - - context.start() - context.write(BEGIN, Bag(), END) - context.step() - context.stop() + with CapturingNodeExecutionContext(FileReader(path=filename), services=services) as context: + context.write(BEGIN, Bag(), END) + context.step() assert len(context.send.mock_calls) == 2 diff --git a/tests/io/test_json.py b/tests/io/test_json.py index 15d9e7e..75350ce 100644 --- a/tests/io/test_json.py +++ b/tests/io/test_json.py @@ -1,44 +1,48 @@ import pytest -from bonobo import Bag, JsonReader, JsonWriter, open_fs, settings +from bonobo import Bag, JsonReader, JsonWriter, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext +from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester + +json_tester = FilesystemTester('json') +json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]''' -def test_write_json_to_file(tmpdir): - fs, filename = open_fs(tmpdir), 'output.json' +def test_write_json_arg0(tmpdir): + fs, filename, services = json_tester.get_services_for_writer(tmpdir) - writer = JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0) - context = NodeExecutionContext(writer, services={'fs': fs}) - - context.start() - context.write(BEGIN, Bag({'foo': 'bar'}), END) - context.step() - context.stop() + with NodeExecutionContext(JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: + context.write(BEGIN, Bag({'foo': 'bar'}), END) + context.step() with fs.open(filename) as fp: assert fp.read() == '[{"foo": "bar"}]' - with pytest.raises(AttributeError): - getattr(context, 'file') - with pytest.raises(AttributeError): - getattr(context, 'first') +@pytest.mark.parametrize('add_kwargs', ({}, { + 'ioformat': settings.IOFORMAT_KWARGS, +}, )) +def test_write_json_kwargs(tmpdir, add_kwargs): + fs, filename, services = json_tester.get_services_for_writer(tmpdir) + + with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context: + context.write(BEGIN, Bag(**{'foo': 'bar'}), END) + context.step() + + with fs.open(filename) as fp: + assert fp.read() == '[{"foo": "bar"}]' -def test_read_json_from_file(tmpdir): - fs, filename = open_fs(tmpdir), 'input.json' - with fs.open(filename, 'w') as fp: - fp.write('[{"x": "foo"},{"x": "bar"}]') - reader = JsonReader(filename, ioformat=settings.IOFORMAT_ARG0) +def test_read_json_arg0(tmpdir): + fs, filename, services = json_tester.get_services_for_reader(tmpdir) - context = CapturingNodeExecutionContext(reader, services={'fs': fs}) - - context.start() - context.write(BEGIN, Bag(), END) - context.step() - context.stop() + with CapturingNodeExecutionContext( + JsonReader(filename, ioformat=settings.IOFORMAT_ARG0), + services=services, + ) as context: + context.write(BEGIN, Bag(), END) + context.step() assert len(context.send.mock_calls) == 2 diff --git a/tests/io/test_pickle.py b/tests/io/test_pickle.py index 1709b16..aff7796 100644 --- a/tests/io/test_pickle.py +++ b/tests/io/test_pickle.py @@ -2,24 +2,22 @@ import pickle import pytest -from bonobo import Bag, PickleReader, PickleWriter, open_fs, settings +from bonobo import Bag, PickleReader, PickleWriter, settings from bonobo.constants import BEGIN, END from bonobo.execution.node import NodeExecutionContext -from bonobo.util.testing import CapturingNodeExecutionContext +from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester + +pickle_tester = FilesystemTester('pkl', mode='wb') +pickle_tester.input_data = pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']]) def test_write_pickled_dict_to_file(tmpdir): - fs, filename = open_fs(tmpdir), 'output.pkl' + fs, filename, services = pickle_tester.get_services_for_writer(tmpdir) - writer = PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0) - context = NodeExecutionContext(writer, services={'fs': fs}) - - context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) - - context.start() - context.step() - context.step() - context.stop() + with NodeExecutionContext(PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context: + context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) + context.step() + context.step() with fs.open(filename, 'rb') as fp: assert pickle.loads(fp.read()) == {'foo': 'bar'} @@ -29,18 +27,13 @@ def test_write_pickled_dict_to_file(tmpdir): def test_read_pickled_list_from_file(tmpdir): - fs, filename = open_fs(tmpdir), 'input.pkl' - with fs.open(filename, 'wb') as fp: - fp.write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) + fs, filename, services = pickle_tester.get_services_for_reader(tmpdir) - reader = PickleReader(filename, ioformat=settings.IOFORMAT_ARG0) - - context = CapturingNodeExecutionContext(reader, services={'fs': fs}) - - context.start() - context.write(BEGIN, Bag(), END) - context.step() - context.stop() + with CapturingNodeExecutionContext( + PickleReader(filename, ioformat=settings.IOFORMAT_ARG0), services=services + ) as context: + context.write(BEGIN, Bag(), END) + context.step() assert len(context.send.mock_calls) == 2 From b1db4263d42838a45927857871743ec270ba734c Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 18 Jun 2017 21:30:19 +0200 Subject: [PATCH 108/143] Update dependencies. --- Makefile | 4 ++-- requirements-docker.txt | 5 +++-- requirements.txt | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f782b7e..881f553 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-10 15:15:34.093885 +# Updated at 2017-06-18 21:29:55.255815 PACKAGE ?= bonobo PYTHON ?= $(shell which python) @@ -18,7 +18,7 @@ SPHINX_BUILD ?= $(PYTHON_DIRNAME)/sphinx-build SPHINX_OPTIONS ?= SPHINX_SOURCEDIR ?= docs SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build -YAPF ?= $(PYTHON_DIRNAME)/yapf +YAPF ?= $(PYTHON) -m yapf YAPF_OPTIONS ?= -rip VERSION ?= $(shell git describe 2>/dev/null || echo dev) diff --git a/requirements-docker.txt b/requirements-docker.txt index b004174..dadce09 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -6,14 +6,15 @@ chardet==3.0.4 colorama==0.3.9 docker-pycreds==0.2.1 docker==2.3.0 -enum34==1.1.6 fs==2.0.4 idna==2.5 +packaging==16.8 pbr==3.0.1 psutil==5.2.2 +pyparsing==2.2.0 pytz==2017.2 requests==2.18.1 six==1.10.0 stevedore==1.23.0 urllib3==1.21.1 -websocket-client==0.40.0 +websocket-client==0.42.1 diff --git a/requirements.txt b/requirements.txt index 478879e..1b284bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ appdirs==1.4.3 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 -enum34==1.1.6 fs==2.0.4 idna==2.5 packaging==16.8 From 1d273767851e75e3cc92096515410e92594831d9 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 18 Jun 2017 21:35:30 +0200 Subject: [PATCH 109/143] release: 0.4.2 --- bonobo/_version.py | 2 +- bonobo/strategies/executor.py | 2 ++ docs/changelog.rst | 17 +++++++++++++++++ tests/io/test_csv.py | 10 +++++----- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index f0ede3d..a987347 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.4.1' +__version__ = '0.4.2' diff --git a/bonobo/strategies/executor.py b/bonobo/strategies/executor.py index d2cdcbe..44d206e 100644 --- a/bonobo/strategies/executor.py +++ b/bonobo/strategies/executor.py @@ -28,6 +28,7 @@ class ExecutorStrategy(Strategy): futures = [] for plugin_context in context.plugins: + def _runner(plugin_context=plugin_context): with plugin_context: try: @@ -38,6 +39,7 @@ class ExecutorStrategy(Strategy): futures.append(executor.submit(_runner)) for node_context in context.nodes: + def _runner(node_context=node_context): try: node_context.start() diff --git a/docs/changelog.rst b/docs/changelog.rst index f447fc9..ebd2fac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,23 @@ Changelog ========= +v.0.4.2 - 18 june 2017 +:::::::::::::::::::::: + +* [config] Implements a "requires()" service injection decorator for functions (api may change). +* [core] Execution contexts are now context managers. +* [fs] adds a defaut to current working directory in open_fs(...). +* [logging] Adds logging alias for easier imports. +* [stdlib] Fix I/O related nodes (especially json), there were bad bugs with ioformat. + +Dependency updates +------------------ + +* Update bonobo-docker from 0.2.6 to 0.2.8 +* Update dependencies. +* Update fs from 2.0.3 to 2.0.4 +* Update requests from 2.17.3 to 2.18.1 + v.0.4.0 - 10 june 2017 :::::::::::::::::::::: diff --git a/tests/io/test_csv.py b/tests/io/test_csv.py index dc6f71c..9a9480c 100644 --- a/tests/io/test_csv.py +++ b/tests/io/test_csv.py @@ -26,7 +26,7 @@ def test_write_csv_to_file_arg0(tmpdir): @pytest.mark.parametrize('add_kwargs', ({}, { 'ioformat': settings.IOFORMAT_KWARGS, -},)) +}, )) def test_write_csv_to_file_kwargs(tmpdir, add_kwargs): fs, filename, services = csv_tester.get_services_for_writer(tmpdir) @@ -46,8 +46,8 @@ def test_read_csv_from_file_arg0(tmpdir): fs, filename, services = csv_tester.get_services_for_reader(tmpdir) with CapturingNodeExecutionContext( - CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), - services=services, + CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0), + services=services, ) as context: context.write(BEGIN, Bag(), END) context.step() @@ -75,8 +75,8 @@ def test_read_csv_from_file_kwargs(tmpdir): fs, filename, services = csv_tester.get_services_for_reader(tmpdir) with CapturingNodeExecutionContext( - CsvReader(path=filename, delimiter=','), - services=services, + CsvReader(path=filename, delimiter=','), + services=services, ) as context: context.write(BEGIN, Bag(), END) context.step() From d0345880913ab7de1f3bc5c014be1a18d008d9a3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 4 Jul 2017 10:51:33 +0200 Subject: [PATCH 110/143] Update dependencies. --- Makefile | 2 +- requirements-dev.txt | 2 +- requirements-docker.txt | 6 +++--- requirements-jupyter.txt | 4 ++-- requirements.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 881f553..10094af 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-06-18 21:29:55.255815 +# Updated at 2017-07-04 10:50:55.775681 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6c33b15..55ba71c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,7 +27,7 @@ pytz==2017.2 requests==2.18.1 six==1.10.0 snowballstemmer==1.2.1 -sphinx==1.6.2 +sphinx==1.6.3 sphinxcontrib-websupport==1.0.1 termcolor==1.1.0 urllib3==1.21.1 diff --git a/requirements-docker.txt b/requirements-docker.txt index dadce09..0a39353 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,6 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.8 +bonobo-docker==0.2.9 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 @@ -9,7 +9,7 @@ docker==2.3.0 fs==2.0.4 idna==2.5 packaging==16.8 -pbr==3.0.1 +pbr==3.1.1 psutil==5.2.2 pyparsing==2.2.0 pytz==2017.2 @@ -17,4 +17,4 @@ requests==2.18.1 six==1.10.0 stevedore==1.23.0 urllib3==1.21.1 -websocket-client==0.42.1 +websocket-client==0.44.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index 7db4a67..d6a6fdb 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -11,7 +11,7 @@ ipywidgets==6.0.0 jedi==0.10.2 jinja2==2.9.6 jsonschema==2.6.0 -jupyter-client==5.0.1 +jupyter-client==5.1.0 jupyter-console==5.1.0 jupyter-core==4.3.0 jupyter==1.0.0 @@ -24,7 +24,7 @@ pandocfilters==1.4.1 pexpect==4.2.1 pickleshare==0.7.4 prompt-toolkit==1.0.14 -ptyprocess==0.5.1 +ptyprocess==0.5.2 pygments==2.2.0 python-dateutil==2.6.0 pyzmq==16.0.2 diff --git a/requirements.txt b/requirements.txt index 1b284bc..093a6a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ colorama==0.3.9 fs==2.0.4 idna==2.5 packaging==16.8 -pbr==3.0.1 +pbr==3.1.1 psutil==5.2.2 pyparsing==2.2.0 pytz==2017.2 From 896bc79abe443075cfdb27c56bb64be5dad0215f Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 4 Jul 2017 10:55:45 +0200 Subject: [PATCH 111/143] release: 0.4.3 --- bonobo/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_version.py b/bonobo/_version.py index a987347..908c0bb 100644 --- a/bonobo/_version.py +++ b/bonobo/_version.py @@ -1 +1 @@ -__version__ = '0.4.2' +__version__ = '0.4.3' From 5062221e7866e707cd2c42073f93d929c2059ef5 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 11:15:03 +0200 Subject: [PATCH 112/143] [config] Refactoring of configurables, allowing partially configured objects. Configurables did not allow more than one "method" option, and mixed scenarios (options+methods+...) were sometimes flaky, forcing the user to know what order was the right one. Now, all options work the same, sharing the same "order" namespace. Backward incompatible change: Options are now required by default, unless a default is provided. Also adds a few candies for debugging/testing, found in the bonobo.util.inspect module. --- bonobo/_api.py | 3 +- bonobo/config/__init__.py | 2 +- bonobo/config/configurables.py | 200 +++++++++++++++++++-------- bonobo/config/options.py | 60 +++++--- bonobo/config/processors.py | 10 +- bonobo/config/services.py | 4 +- bonobo/ext/opendatasoft.py | 4 +- bonobo/nodes/__init__.py | 11 +- bonobo/nodes/basics.py | 14 +- bonobo/nodes/io/csv.py | 2 +- bonobo/nodes/io/pickle.py | 2 +- bonobo/nodes/throttle.py | 55 ++++++++ bonobo/settings.py | 2 +- bonobo/util/collections.py | 6 + bonobo/util/inspect.py | 114 +++++++++++++++ tests/config/test_configurables.py | 57 +++++++- tests/config/test_methods.py | 80 ++++++++--- tests/config/test_methods_partial.py | 66 +++++++++ tests/test_basics.py | 1 + 19 files changed, 573 insertions(+), 120 deletions(-) create mode 100644 bonobo/nodes/throttle.py create mode 100644 bonobo/util/collections.py create mode 100644 bonobo/util/inspect.py create mode 100644 tests/config/test_methods_partial.py diff --git a/bonobo/_api.py b/bonobo/_api.py index cf28a33..89b6d4c 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,6 +1,6 @@ from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrinter, PickleWriter, PickleReader, Tee, count, identity, noop, pprint + PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop, pprint from bonobo.strategies import create_strategy from bonobo.util.objects import get_name @@ -104,6 +104,7 @@ register_api_group( PrettyPrinter, PickleReader, PickleWriter, + RateLimited, Tee, count, identity, diff --git a/bonobo/config/__init__.py b/bonobo/config/__init__.py index 08be544..6a4247e 100644 --- a/bonobo/config/__init__.py +++ b/bonobo/config/__init__.py @@ -3,7 +3,7 @@ from bonobo.config.options import Method, Option from bonobo.config.processors import ContextProcessor from bonobo.config.services import Container, Exclusive, Service, requires -# bonobo.config public programming interface +# Bonobo's Config API __all__ = [ 'Configurable', 'Container', diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 43cb8c2..01db9e0 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -1,12 +1,14 @@ -from bonobo.config.options import Method, Option -from bonobo.config.processors import ContextProcessor -from bonobo.errors import ConfigurationError, AbstractError +from bonobo.util.inspect import isoption, iscontextprocessor +from bonobo.errors import AbstractError +from bonobo.util.collections import sortedlist __all__ = [ 'Configurable', 'Option', ] +get_creation_counter = lambda v: v._creation_counter + class ConfigurableMeta(type): """ @@ -15,36 +17,77 @@ class ConfigurableMeta(type): def __init__(cls, what, bases=None, dict=None): super().__init__(what, bases, dict) - cls.__options__ = {} - cls.__positional_options__ = [] - cls.__processors__ = [] - cls.__wrappable__ = None + + cls.__processors = sortedlist() + cls.__methods = sortedlist() + cls.__options = sortedlist() + cls.__names = set() + + # cls.__kwoptions = [] for typ in cls.__mro__: - for name, value in typ.__dict__.items(): - if isinstance(value, Option): - if isinstance(value, ContextProcessor): - cls.__processors__.append(value) - else: - if not value.name: - value.name = name + for name, value in filter(lambda x: isoption(x[1]), typ.__dict__.items()): + if iscontextprocessor(value): + cls.__processors.insort((value._creation_counter, value)) + continue - if isinstance(value, Method): - if cls.__wrappable__: - raise ConfigurationError( - 'Cannot define more than one "Method" option in a configurable. That may change in the future.' - ) - cls.__wrappable__ = name + if not value.name: + value.name = name - if not name in cls.__options__: - cls.__options__[name] = value + if not name in cls.__names: + cls.__names.add(name) + cls.__options.insort((not value.positional, value._creation_counter, name, value)) - if value.positional: - cls.__positional_options__.append(name) + @property + def __options__(cls): + return ((name, option) for _, _, name, option in cls.__options) - # This can be done before, more efficiently. Not so bad neither as this is only done at type() creation time - # (aka class Xxx(...) time) and there should not be hundreds of processors. Still not very elegant. - cls.__processors__ = sorted(cls.__processors__, key=lambda v: v._creation_counter) + @property + def __options_dict__(cls): + return dict(cls.__options__) + + @property + def __processors__(cls): + return (processor for _, processor in cls.__processors) + + def __repr__(self): + return ' '.join(('= position + 1 else None + position += 1 + + return self.__options_values + + def __getattr__(self, item): + _dict = self.func.__options_dict__ + if item in _dict: + return _dict[item].__get__(self, self.func) + return getattr(self.func, item) class Configurable(metaclass=ConfigurableMeta): @@ -54,61 +97,108 @@ class Configurable(metaclass=ConfigurableMeta): """ - def __new__(cls, *args, **kwargs): - if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'): - return type(args[0].__name__, (cls, ), {cls.__wrappable__: args[0]}) + def __new__(cls, *args, _final=False, **kwargs): + """ + Custom instance builder. If not all options are fulfilled, will return a :class:`PartiallyConfigured` instance + which is just a :class:`functools.partial` object that behaves like a :class:`Configurable` instance. - return super(Configurable, cls).__new__(cls) - - def __init__(self, *args, **kwargs): - super().__init__() - - # initialize option's value dictionary, used by descriptor implementation (see Option). - self.__options_values__ = {} + The special `_final` argument can be used to force final instance to be created, or an error raised if options + are missing. + :param args: + :param _final: bool + :param kwargs: + :return: Configurable or PartiallyConfigured + """ + options = tuple(cls.__options__) # compute missing options, given the kwargs. missing = set() - for name, option in type(self).__options__.items(): + for name, option in options: if option.required and not option.name in kwargs: missing.add(name) # transform positional arguments in keyword arguments if possible. position = 0 - for positional_option in self.__positional_options__: - if len(args) <= position: - break - kwargs[positional_option] = args[position] - position += 1 - if positional_option in missing: - missing.remove(positional_option) + for name, option in options: + if not option.positional: + break # option orders make all positional options first, job done. - # complain if there are still missing options. - if len(missing): - raise TypeError( - '{}() missing {} required option{}: {}.'.format( - type(self).__name__, - len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing))) - ) - ) + if not isoption(getattr(cls, name)): + missing.remove(name) + continue + + if len(args) <= position: + break # no more positional arguments given. + + position += 1 + if name in missing: + missing.remove(name) # complain if there is more options than possible. - extraneous = set(kwargs.keys()) - set(type(self).__options__.keys()) + extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set()) if len(extraneous): raise TypeError( '{}() got {} unexpected option{}: {}.'.format( - type(self).__name__, + cls.__name__, len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous))) ) ) + # missing options? we'll return a partial instance to finish the work later, unless we're required to be + # "final". + if len(missing): + if _final: + raise TypeError( + '{}() missing {} required option{}: {}.'.format( + cls.__name__, + len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing))) + ) + ) + return PartiallyConfigured(cls, *args, **kwargs) + + return super(Configurable, cls).__new__(cls) + + def __init__(self, *args, **kwargs): + # initialize option's value dictionary, used by descriptor implementation (see Option). + self._options_values = { + **kwargs + } + # set option values. for name, value in kwargs.items(): setattr(self, name, value) + position = 0 + for name, option in self.__options__: + if not option.positional: + break # option orders make all positional options first + + # value was overriden? Skip. + maybe_value = getattr(type(self), name) + if not isoption(maybe_value): + continue + + if len(args) <= position: + break + + if name in self._options_values: + raise ValueError('Already got a value for option {}'.format(name)) + + setattr(self, name, args[position]) + position += 1 + def __call__(self, *args, **kwargs): """ You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override. """ return self.call(*args, **kwargs) + @property + def __options__(self): + return type(self).__options__ + + @property + def __processors__(self): + return type(self).__processors__ + def call(self, *args, **kwargs): raise AbstractError('Not implemented.') diff --git a/bonobo/config/options.py b/bonobo/config/options.py index 51f4a20..82604fb 100644 --- a/bonobo/config/options.py +++ b/bonobo/config/options.py @@ -1,3 +1,6 @@ +from bonobo.util.inspect import istype + + class Option: """ An Option is a descriptor for Configurable's parameters. @@ -14,7 +17,9 @@ class Option: If an option is required, an error will be raised if no value is provided (at runtime). If it is not, option will have the default value if user does not override it at runtime. - (default: False) + Ignored if a default is provided, meaning that the option cannot be required. + + (default: True) .. attribute:: positional @@ -48,10 +53,10 @@ class Option: _creation_counter = 0 - def __init__(self, type=None, *, required=False, positional=False, default=None): + def __init__(self, type=None, *, required=True, positional=False, default=None): self.name = None self.type = type - self.required = required + self.required = required if default is None else False self.positional = positional self.default = default @@ -60,12 +65,27 @@ class Option: Option._creation_counter += 1 def __get__(self, inst, typ): - if not self.name in inst.__options_values__: - inst.__options_values__[self.name] = self.get_default() - return inst.__options_values__[self.name] + # XXX If we call this on the type, then either return overriden value or ... ??? + if inst is None: + return vars(type).get(self.name, self) + + if not self.name in inst._options_values: + inst._options_values[self.name] = self.get_default() + + return inst._options_values[self.name] def __set__(self, inst, value): - inst.__options_values__[self.name] = self.clean(value) + inst._options_values[self.name] = self.clean(value) + + def __repr__(self): + return '<{positional}{typename} {type}{name} default={default!r}{required}>'.format( + typename=type(self).__name__, + type='({})'.format(self.type) if istype(self.type) else '', + name=self.name, + positional='*' if self.positional else '**', + default=self.default, + required=' (required)' if self.required else '', + ) def clean(self, value): return self.type(value) if self.type else value @@ -105,20 +125,18 @@ class Method(Option): """ - def __init__(self): - super().__init__(None, required=False) - - def __get__(self, inst, typ): - if not self.name in inst.__options_values__: - inst.__options_values__[self.name] = getattr(inst, self.name) - return inst.__options_values__[self.name] + def __init__(self, *, required=True, positional=True): + super().__init__(None, required=required, positional=positional) def __set__(self, inst, value): - if isinstance(value, str): - raise ValueError('should be callable') - inst.__options_values__[self.name] = self.type(value) if self.type else value - - def clean(self, value): if not hasattr(value, '__call__'): - raise ValueError('{} value must be callable.'.format(type(self).__name__)) - return value + raise TypeError('Option of type {!r} is expecting a callable value, got {!r} object (which is not).'.format( + type(self).__name__, type(value).__name__)) + inst._options_values[self.name] = self.type(value) if self.type else value + + def __call__(self, *args, **kwargs): + # only here to trick IDEs into thinking this is callable. + raise NotImplementedError('You cannot call the descriptor') + + + diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index d441b6e..27f8703 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -74,8 +74,7 @@ class ContextCurrifier: def __init__(self, wrapped, *initial_context): self.wrapped = wrapped self.context = tuple(initial_context) - self._stack = [] - self._stack_values = [] + self._stack, self._stack_values = None, None def __iter__(self): yield from self.wrapped @@ -86,8 +85,10 @@ class ContextCurrifier: return self.wrapped(*self.context, *args, **kwargs) def setup(self, *context): - if len(self._stack): + if self._stack is not None: raise RuntimeError('Cannot setup context currification twice.') + + self._stack, self._stack_values = list(), list() for processor in resolve_processors(self.wrapped): _processed = processor(self.wrapped, *context, *self.context) _append_to_context = next(_processed) @@ -97,7 +98,7 @@ class ContextCurrifier: self._stack.append(_processed) def teardown(self): - while len(self._stack): + while self._stack: processor = self._stack.pop() try: # todo yield from ? how to ? @@ -108,6 +109,7 @@ class ContextCurrifier: else: # No error ? We should have had StopIteration ... raise RuntimeError('Context processors should not yield more than once.') + self._stack, self._stack_values = None, None @contextmanager def as_contextmanager(self, *context): diff --git a/bonobo/config/services.py b/bonobo/config/services.py index d792175..1fe066d 100644 --- a/bonobo/config/services.py +++ b/bonobo/config/services.py @@ -53,7 +53,7 @@ class Service(Option): super().__init__(str, required=False, default=name) def __set__(self, inst, value): - inst.__options_values__[self.name] = validate_service_name(value) + inst._options_values[self.name] = validate_service_name(value) def resolve(self, inst, services): try: @@ -75,7 +75,7 @@ class Container(dict): def args_for(self, mixed): try: - options = mixed.__options__ + options = dict(mixed.__options__) except AttributeError: options = {} diff --git a/bonobo/ext/opendatasoft.py b/bonobo/ext/opendatasoft.py index 4be3134..2dc54c0 100644 --- a/bonobo/ext/opendatasoft.py +++ b/bonobo/ext/opendatasoft.py @@ -13,13 +13,13 @@ def path_str(path): class OpenDataSoftAPI(Configurable): - dataset = Option(str, required=True) + dataset = Option(str, positional=True) endpoint = Option(str, default='{scheme}://{netloc}{path}') scheme = Option(str, default='https') netloc = Option(str, default='data.opendatasoft.com') path = Option(path_str, default='/api/records/1.0/search/') rows = Option(int, default=500) - limit = Option(int, default=None) + limit = Option(int, required=False) timezone = Option(str, default='Europe/Paris') kwargs = Option(dict, default=dict) diff --git a/bonobo/nodes/__init__.py b/bonobo/nodes/__init__.py index c25b580..2cdd1e9 100644 --- a/bonobo/nodes/__init__.py +++ b/bonobo/nodes/__init__.py @@ -1,9 +1,8 @@ -from bonobo.nodes.io import __all__ as _all_io -from bonobo.nodes.io import * - -from bonobo.nodes.basics import __all__ as _all_basics from bonobo.nodes.basics import * - +from bonobo.nodes.basics import __all__ as _all_basics from bonobo.nodes.filter import Filter +from bonobo.nodes.io import * +from bonobo.nodes.io import __all__ as _all_io +from bonobo.nodes.throttle import RateLimited -__all__ = _all_basics + _all_io + ['Filter'] +__all__ = _all_basics + _all_io + ['Filter', 'RateLimited'] diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index c21757a..c1ead61 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -1,16 +1,16 @@ import functools -from pprint import pprint as _pprint - import itertools + from colorama import Fore, Style from bonobo import settings from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor +from bonobo.constants import NOT_MODIFIED from bonobo.structs.bags import Bag +from bonobo.util.compat import deprecated from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL -from bonobo.constants import NOT_MODIFIED __all__ = [ 'identity', @@ -87,8 +87,12 @@ class PrettyPrinter(Configurable): ) -pprint = PrettyPrinter() -pprint.__name__ = 'pprint' +_pprint = PrettyPrinter() + + +@deprecated +def pprint(*args, **kwargs): + return _pprint(*args, **kwargs) def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True): diff --git a/bonobo/nodes/io/csv.py b/bonobo/nodes/io/csv.py index ae68bd0..75fffe8 100644 --- a/bonobo/nodes/io/csv.py +++ b/bonobo/nodes/io/csv.py @@ -26,7 +26,7 @@ class CsvHandler(FileHandler): """ delimiter = Option(str, default=';') quotechar = Option(str, default='"') - headers = Option(tuple) + headers = Option(tuple, required=False) class CsvReader(IOFormatEnabled, FileReader, CsvHandler): diff --git a/bonobo/nodes/io/pickle.py b/bonobo/nodes/io/pickle.py index e94f94a..d9da55f 100644 --- a/bonobo/nodes/io/pickle.py +++ b/bonobo/nodes/io/pickle.py @@ -17,7 +17,7 @@ class PickleHandler(FileHandler): """ - item_names = Option(tuple) + item_names = Option(tuple, required=False) class PickleReader(IOFormatEnabled, FileReader, PickleHandler): diff --git a/bonobo/nodes/throttle.py b/bonobo/nodes/throttle.py new file mode 100644 index 0000000..2f08cd3 --- /dev/null +++ b/bonobo/nodes/throttle.py @@ -0,0 +1,55 @@ +import threading +import time + +from bonobo.config import Configurable, ContextProcessor, Method, Option + + +class RateLimitBucket(threading.Thread): + daemon = True + + @property + def stopped(self): + return self._stop_event.is_set() + + def __init__(self, initial=1, period=1, amount=1): + super(RateLimitBucket, self).__init__() + self.semaphore = threading.BoundedSemaphore(initial) + self.amount = amount + self.period = period + + self._stop_event = threading.Event() + + def stop(self): + self._stop_event.set() + + def run(self): + while not self.stopped: + time.sleep(self.period) + for _ in range(self.amount): + self.semaphore.release() + + def wait(self): + return self.semaphore.acquire() + + +class RateLimited(Configurable): + handler = Method() + + initial = Option(int, positional=True, default=1) + period = Option(int, positional=True, default=1) + amount = Option(int, positional=True, default=1) + + @ContextProcessor + def bucket(self, context): + print(context) + bucket = RateLimitBucket(self.initial, self.amount, self.period) + bucket.start() + print(bucket) + yield bucket + bucket.stop() + bucket.join() + + def call(self, bucket, *args, **kwargs): + print(bucket, args, kwargs) + bucket.wait() + return self.handler(*args, **kwargs) diff --git a/bonobo/settings.py b/bonobo/settings.py index e0e5289..8e8a780 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -27,7 +27,7 @@ class Setting: self.validator = None def __repr__(self): - return ''.format(self.name, self.value) + return ''.format(self.name, self.get()) def set(self, value): if self.validator and not self.validator(value): diff --git a/bonobo/util/collections.py b/bonobo/util/collections.py new file mode 100644 index 0000000..b97630a --- /dev/null +++ b/bonobo/util/collections.py @@ -0,0 +1,6 @@ +import bisect + + +class sortedlist(list): + def insort(self, x): + bisect.insort(self, x) \ No newline at end of file diff --git a/bonobo/util/inspect.py b/bonobo/util/inspect.py new file mode 100644 index 0000000..72fcc7e --- /dev/null +++ b/bonobo/util/inspect.py @@ -0,0 +1,114 @@ +from collections import namedtuple + + +def isconfigurabletype(mixed): + """ + Check if the given argument is an instance of :class:`bonobo.config.ConfigurableMeta`, meaning it has all the + plumbery necessary to build :class:`bonobo.config.Configurable`-like instances. + + :param mixed: + :return: bool + """ + from bonobo.config.configurables import ConfigurableMeta + return isinstance(mixed, ConfigurableMeta) + + +def isconfigurable(mixed): + """ + Check if the given argument is an instance of :class:`bonobo.config.Configurable`. + + :param mixed: + :return: bool + """ + from bonobo.config.configurables import Configurable + return isinstance(mixed, Configurable) + + +def isoption(mixed): + """ + Check if the given argument is an instance of :class:`bonobo.config.Option`. + + :param mixed: + :return: bool + """ + + from bonobo.config.options import Option + return isinstance(mixed, Option) + + +def ismethod(mixed): + """ + Check if the given argument is an instance of :class:`bonobo.config.Method`. + + :param mixed: + :return: bool + """ + from bonobo.config.options import Method + return isinstance(mixed, Method) + + +def iscontextprocessor(x): + """ + Check if the given argument is an instance of :class:`bonobo.config.ContextProcessor`. + + :param mixed: + :return: bool + """ + from bonobo.config.processors import ContextProcessor + return isinstance(x, ContextProcessor) + + +def istype(mixed): + """ + Check if the given argument is a type object. + + :param mixed: + :return: bool + """ + return isinstance(mixed, type) + + +ConfigurableInspection = namedtuple('ConfigurableInspection', + [ + 'type', + 'instance', + 'options', + 'processors', + 'partial', + ]) + +ConfigurableInspection.__enter__ = lambda self: self +ConfigurableInspection.__exit__ = lambda *exc_details: None + + +def inspect_node(mixed, *, _partial=None): + """ + If the given argument is somehow a :class:`bonobo.config.Configurable` object (either a subclass, an instance, or + a partially configured instance), then it will return a :class:`ConfigurableInspection` namedtuple, used to inspect + the configurable metadata (options). If you want to get the option values, you don't need this, it is only usefull + to perform introspection on a configurable. + + If it's not looking like a configurable, it will raise a :class:`TypeError`. + + :param mixed: + :return: ConfigurableInspection + + :raise: TypeError + """ + if isconfigurabletype(mixed): + inst, typ = None, mixed + elif isconfigurable(mixed): + inst, typ = mixed, type(mixed) + elif hasattr(mixed, 'func'): + return inspect_node(mixed.func, _partial=(mixed.args, mixed.keywords)) + else: + raise TypeError( + 'Not a Configurable, nor a Configurable instance and not even a partially configured Configurable. Check your inputs.') + + return ConfigurableInspection( + typ, + inst, + list(typ.__options__), + list(typ.__processors__), + _partial, + ) diff --git a/tests/config/test_configurables.py b/tests/config/test_configurables.py index 178c188..f1c5387 100644 --- a/tests/config/test_configurables.py +++ b/tests/config/test_configurables.py @@ -2,12 +2,17 @@ import pytest from bonobo.config.configurables import Configurable from bonobo.config.options import Option +from bonobo.util.inspect import inspect_node + + +class NoOptConfigurable(Configurable): + pass class MyConfigurable(Configurable): - required_str = Option(str, required=True) + required_str = Option(str) default_str = Option(str, default='foo') - integer = Option(int) + integer = Option(int, required=False) class MyHarderConfigurable(MyConfigurable): @@ -25,14 +30,20 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable): def test_missing_required_option_error(): + with inspect_node(MyConfigurable()) as ni: + assert ni.partial + with pytest.raises(TypeError) as exc: - MyConfigurable() + MyConfigurable(_final=True) assert exc.match('missing 1 required option:') def test_missing_required_options_error(): + with inspect_node(MyHarderConfigurable()) as ni: + assert ni.partial + with pytest.raises(TypeError) as exc: - MyHarderConfigurable() + MyHarderConfigurable(_final=True) assert exc.match('missing 2 required options:') @@ -50,6 +61,10 @@ def test_extraneous_options_error(): def test_defaults(): o = MyConfigurable(required_str='hello') + + with inspect_node(o) as ni: + assert not ni.partial + assert o.required_str == 'hello' assert o.default_str == 'foo' assert o.integer == None @@ -57,6 +72,10 @@ def test_defaults(): def test_str_type_factory(): o = MyConfigurable(required_str=42) + + with inspect_node(o) as ni: + assert not ni.partial + assert o.required_str == '42' assert o.default_str == 'foo' assert o.integer == None @@ -64,6 +83,10 @@ def test_str_type_factory(): def test_int_type_factory(): o = MyConfigurable(required_str='yo', default_str='bar', integer='42') + + with inspect_node(o) as ni: + assert not ni.partial + assert o.required_str == 'yo' assert o.default_str == 'bar' assert o.integer == 42 @@ -71,6 +94,10 @@ def test_int_type_factory(): def test_bool_type_factory(): o = MyHarderConfigurable(required_str='yes', also_required='True') + + with inspect_node(o) as ni: + assert not ni.partial + assert o.required_str == 'yes' assert o.default_str == 'foo' assert o.integer == None @@ -79,6 +106,10 @@ def test_bool_type_factory(): def test_option_resolution_order(): o = MyBetterConfigurable() + + with inspect_node(o) as ni: + assert not ni.partial + assert o.required_str == 'kaboom' assert o.default_str == 'foo' assert o.integer == None @@ -86,3 +117,21 @@ def test_option_resolution_order(): def test_option_positional(): o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello') + + with inspect_node(o) as ni: + assert not ni.partial + + assert o.first == '1' + assert o.second == '2' + assert o.third == '3' + assert o.required_str == 'hello' + assert o.default_str == 'foo' + assert o.integer is None + + +def test_no_opt_configurable(): + o = NoOptConfigurable() + + with inspect_node(o) as ni: + assert not ni.partial + diff --git a/tests/config/test_methods.py b/tests/config/test_methods.py index 3a5f6a3..a4e4ebb 100644 --- a/tests/config/test_methods.py +++ b/tests/config/test_methods.py @@ -1,7 +1,5 @@ -import pytest - from bonobo.config import Configurable, Method, Option -from bonobo.errors import ConfigurationError +from bonobo.util.inspect import inspect_node class MethodBasedConfigurable(Configurable): @@ -13,22 +11,56 @@ class MethodBasedConfigurable(Configurable): self.handler(*args, **kwargs) -def test_one_wrapper_only(): - with pytest.raises(ConfigurationError): +def test_multiple_wrapper_suppored(): + class TwoMethods(Configurable): + h1 = Method(required=True) + h2 = Method(required=True) - class TwoMethods(Configurable): - h1 = Method() - h2 = Method() + with inspect_node(TwoMethods) as ci: + assert ci.type == TwoMethods + assert not ci.instance + assert len(ci.options) == 2 + assert not len(ci.processors) + assert not ci.partial + + @TwoMethods + def OneMethod(): + pass + + with inspect_node(OneMethod) as ci: + assert ci.type == TwoMethods + assert not ci.instance + assert len(ci.options) == 2 + assert not len(ci.processors) + assert ci.partial + + @OneMethod + def transformation(): + pass + + with inspect_node(transformation) as ci: + assert ci.type == TwoMethods + assert ci.instance + assert len(ci.options) == 2 + assert not len(ci.processors) + assert not ci.partial def test_define_with_decorator(): calls = [] - @MethodBasedConfigurable - def Concrete(self, *args, **kwargs): - calls.append((args, kwargs, )) + def my_handler(*args, **kwargs): + calls.append((args, kwargs,)) + + Concrete = MethodBasedConfigurable(my_handler) assert callable(Concrete.handler) + assert Concrete.handler == my_handler + + with inspect_node(Concrete) as ci: + assert ci.type == MethodBasedConfigurable + assert ci.partial + t = Concrete('foo', bar='baz') assert callable(t.handler) @@ -37,13 +69,29 @@ def test_define_with_decorator(): assert len(calls) == 1 +def test_late_binding_method_decoration(): + calls = [] + + @MethodBasedConfigurable(foo='foo') + def Concrete(*args, **kwargs): + calls.append((args, kwargs,)) + + assert callable(Concrete.handler) + t = Concrete(bar='baz') + + assert callable(t.handler) + assert len(calls) == 0 + t() + assert len(calls) == 1 + + def test_define_with_argument(): calls = [] def concrete_handler(*args, **kwargs): - calls.append((args, kwargs, )) + calls.append((args, kwargs,)) - t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler) + t = MethodBasedConfigurable(concrete_handler, 'foo', bar='baz') assert callable(t.handler) assert len(calls) == 0 t() @@ -55,7 +103,7 @@ def test_define_with_inheritance(): class Inheriting(MethodBasedConfigurable): def handler(self, *args, **kwargs): - calls.append((args, kwargs, )) + calls.append((args, kwargs,)) t = Inheriting('foo', bar='baz') assert callable(t.handler) @@ -71,8 +119,8 @@ def test_inheritance_then_decorate(): pass @Inheriting - def Concrete(self, *args, **kwargs): - calls.append((args, kwargs, )) + def Concrete(*args, **kwargs): + calls.append((args, kwargs,)) assert callable(Concrete.handler) t = Concrete('foo', bar='baz') diff --git a/tests/config/test_methods_partial.py b/tests/config/test_methods_partial.py new file mode 100644 index 0000000..fdb1111 --- /dev/null +++ b/tests/config/test_methods_partial.py @@ -0,0 +1,66 @@ +from unittest.mock import MagicMock + +from bonobo.config import Configurable, ContextProcessor, Method, Option +from bonobo.util.inspect import inspect_node + + +class Bobby(Configurable): + handler = Method() + handler2 = Method() + foo = Option(positional=True) + bar = Option(required=False) + + @ContextProcessor + def think(self, context): + yield 'different' + + def call(self, think, *args, **kwargs): + self.handler('1', *args, **kwargs) + self.handler2('2', *args, **kwargs) + + +def test_partial(): + C = Bobby + + # inspect the configurable class + with inspect_node(C) as ci: + assert ci.type == Bobby + assert not ci.instance + assert len(ci.options) == 4 + assert len(ci.processors) == 1 + assert not ci.partial + + # instanciate a partial instance ... + f1 = MagicMock() + C = C(f1) + + with inspect_node(C) as ci: + assert ci.type == Bobby + assert not ci.instance + assert len(ci.options) == 4 + assert len(ci.processors) == 1 + assert ci.partial + assert ci.partial[0] == (f1,) + assert not len(ci.partial[1]) + + # instanciate a more complete partial instance ... + f2 = MagicMock() + C = C(f2) + + with inspect_node(C) as ci: + assert ci.type == Bobby + assert not ci.instance + assert len(ci.options) == 4 + assert len(ci.processors) == 1 + assert ci.partial + assert ci.partial[0] == (f1, f2,) + assert not len(ci.partial[1]) + + c = C('foo') + + with inspect_node(c) as ci: + assert ci.type == Bobby + assert ci.instance + assert len(ci.options) == 4 + assert len(ci.processors) == 1 + assert not ci.partial diff --git a/tests/test_basics.py b/tests/test_basics.py index 283e3d7..5230b0b 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -5,6 +5,7 @@ import pytest import bonobo from bonobo.config.processors import ContextCurrifier from bonobo.constants import NOT_MODIFIED +from bonobo.util.inspect import inspect_node def test_count(): From 2ff19c18a68a69b5e9228739906635d1d1a9ce5e Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 11:34:34 +0200 Subject: [PATCH 113/143] [errors] Implements unrecoverable errors, that will raise and stop the transformation. Used when an invalid ioformat is used. --- bonobo/errors.py | 14 ++ bonobo/examples/datasets/coffeeshops.json | 250 +++++++++++----------- bonobo/examples/datasets/coffeeshops.txt | 248 ++++++++++----------- bonobo/execution/node.py | 5 +- bonobo/nodes/io/base.py | 13 +- 5 files changed, 274 insertions(+), 256 deletions(-) diff --git a/bonobo/errors.py b/bonobo/errors.py index 564950d..8510a50 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -60,3 +60,17 @@ class ConfigurationError(Exception): class MissingServiceImplementationError(KeyError): pass + + +class UnrecoverableError(Exception): + """Flag for errors that must interrupt the workflow, either because they will happen for sure on each node run, or + because you know that your transformation has no point continuing runnning after a bad event.""" + +class UnrecoverableValueError(UnrecoverableError, ValueError): + pass + +class UnrecoverableRuntimeError(UnrecoverableError, RuntimeError): + pass + +class UnrecoverableNotImplementedError(UnrecoverableError, NotImplementedError): + pass diff --git a/bonobo/examples/datasets/coffeeshops.json b/bonobo/examples/datasets/coffeeshops.json index 60e89b1..391b5e8 100644 --- a/bonobo/examples/datasets/coffeeshops.json +++ b/bonobo/examples/datasets/coffeeshops.json @@ -1,182 +1,182 @@ -{"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", -"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", -"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", -"Le Felteu": "1 rue Pecquay, 75004 Paris, France", +{"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", +"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France", +"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", +"Le Bellerive": "71 quai de Seine, 75019 Paris, France", +"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", "O q de poule": "53 rue du ruisseau, 75018 Paris, France", +"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France", "Le chantereine": "51 Rue Victoire, 75009 Paris, France", "Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", -"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France", -"Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France", -"La Renaissance": "112 Rue Championnet, 75018 Paris, France", "Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France", -"Le Sully": "6 Bd henri IV, 75004 Paris, France", -"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", -"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France", -"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France", -"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", -"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France", -"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", -"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", -"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", -"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", +"La Bauloise": "36 rue du hameau, 75015 Paris, France", +"Le Dellac": "14 rue Rougemont, 75009 Paris, France", "Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", +"Le Sully": "6 Bd henri IV, 75004 Paris, France", +"Le Felteu": "1 rue Pecquay, 75004 Paris, France", "Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France", "D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France", "Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France", +"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", +"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", +"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", +"La Renaissance": "112 Rue Championnet, 75018 Paris, France", +"Le Square": "31 rue Saint-Dominique, 75007 Paris, France", +"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France", +"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France", +"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France", +"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France", "Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France", "Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France", -"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", -"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", -"Le Bellerive": "71 quai de Seine, 75019 Paris, France", -"La Bauloise": "36 rue du hameau, 75015 Paris, France", -"Le Dellac": "14 rue Rougemont, 75009 Paris, France", +"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France", +"Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France", +"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France", +"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France", +"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", +"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", +"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", +"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", +"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France", +"l'Usine": "1 rue d'Avron, 75020 Paris, France", +"La Bricole": "52 rue Liebniz, 75018 Paris, France", +"le ronsard": "place maubert, 75005 Paris, France", +"Face Bar": "82 rue des archives, 75003 Paris, France", +"American Kitchen": "49 rue bichat, 75010 Paris, France", +"La Marine": "55 bis quai de valmy, 75010 Paris, France", +"Le Bloc": "21 avenue Brochant, 75017 Paris, France", +"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", +"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", +"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", +"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", +"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", +"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", +"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", +"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France", +"Le Supercoin": "3, rue Baudelique, 75018 Paris, France", +"Populettes": "86 bis rue Riquet, 75018 Paris, France", +"Au bon coin": "49 rue des Cloys, 75018 Paris, France", +"Le Couvent": "69 rue Broca, 75013 Paris, France", +"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", +"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", +"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France", +"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", +"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", +"La Perle": "78 rue vieille du temple, 75003 Paris, France", +"Le Descartes": "1 rue Thouin, 75005 Paris, France", +"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", +"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France", +"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", +"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France", +"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", +"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", +"Le Village": "182 rue de Courcelles, 75017 Paris, France", +"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", +"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", +"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", +"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", +"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France", "Br\u00fblerie San Jos\u00e9": "30 rue des Petits-Champs, 75002 Paris, France", "Caf\u00e9 de la Mairie (du VIII)": "rue de Lisbonne, 75008 Paris, France", -"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", -"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", -"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France", "Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France", "Etienne": "14 rue Turbigo, Paris, 75001 Paris, France", "L'ing\u00e9nu": "184 bd Voltaire, 75011 Paris, France", -"Le Biz": "18 rue Favart, 75002 Paris, France", "L'Olive": "8 rue L'Olive, 75018 Paris, France", -"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", -"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", -"La Marine": "55 bis quai de valmy, 75010 Paris, France", -"American Kitchen": "49 rue bichat, 75010 Paris, France", -"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France", -"Face Bar": "82 rue des archives, 75003 Paris, France", -"Le Bloc": "21 avenue Brochant, 75017 Paris, France", -"La Bricole": "52 rue Liebniz, 75018 Paris, France", -"le ronsard": "place maubert, 75005 Paris, France", -"l'Usine": "1 rue d'Avron, 75020 Paris, France", -"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France", -"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France", -"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France", -"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France", +"Le Biz": "18 rue Favart, 75002 Paris, France", +"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France", +"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France", "Le Germinal": "95 avenue Emile Zola, 75015 Paris, France", +"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France", "Le refuge": "72 rue lamarck, 75018 Paris, France", -"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", -"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France", -"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France", -"Le Supercoin": "3, rue Baudelique, 75018 Paris, France", -"Populettes": "86 bis rue Riquet, 75018 Paris, France", -"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France", -"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France", -"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France", -"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France", -"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France", -"Au bon coin": "49 rue des Cloys, 75018 Paris, France", -"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France", -"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France", -"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France", -"Le Couvent": "69 rue Broca, 75013 Paris, France", -"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France", -"La Perle": "78 rue vieille du temple, 75003 Paris, France", -"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France", -"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France", -"Le Descartes": "1 rue Thouin, 75005 Paris, France", -"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France", -"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France", -"Au panini de la place": "47 rue Belgrand, 75020 Paris, France", -"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France", -"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France", -"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France", -"Le Village": "182 rue de Courcelles, 75017 Paris, France", -"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France", -"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France", -"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France", -"Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France", -"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", -"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", -"Caves populaires": "22 rue des Dames, 75017 Paris, France", -"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", -"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", -"L'anjou": "1 rue de Montholon, 75009 Paris, France", -"Le Brio": "216, rue Marcadet, 75018 Paris, France", -"Tamm Bara": "7 rue Clisson, 75013 Paris, France", -"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", -"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", -"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France", -"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France", -"Le Centenaire": "104 rue amelot, 75011 Paris, France", -"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", -"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France", -"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France", +"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France", "Le Dunois": "77 rue Dunois, 75013 Paris, France", "La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France", "Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France", "Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France", +"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly", +"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France", "Le bal du pirate": "60 rue des bergers, 75015 Paris, France", +"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France", "L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France", "l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", "zic zinc": "95 rue claude decaen, 75012 Paris, France", +"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France", +"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France", +"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France", +"Le Centenaire": "104 rue amelot, 75011 Paris, France", +"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France", +"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France", "Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France", "La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 Paris, France", -"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", "Le Killy Jen": "28 bis boulevard Diderot, 75012 Paris, France", "Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", "le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", -"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", "Les Artisans": "106 rue Lecourbe, 75015 Paris, France", "Peperoni": "83 avenue de Wagram, 75001 Paris, France", -"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", -"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France", -"Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France", -"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", -"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", -"La Brocante": "10 rue Rossini, 75009 Paris, France", -"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", -"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", -"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", -"O'Paris": "1 Rue des Envierges, 75020 Paris, France", +"Le Brio": "216, rue Marcadet, 75018 Paris, France", +"Tamm Bara": "7 rue Clisson, 75013 Paris, France", +"Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France", +"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France", +"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France", +"Caves populaires": "22 rue des Dames, 75017 Paris, France", +"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France", +"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France", +"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France", +"L'anjou": "1 rue de Montholon, 75009 Paris, France", "Botak cafe": "1 rue Paul albert, 75018 Paris, France", -"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France", -"Les caves populaires": "22 rue des Dames, 75017 Paris, France", -"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", -"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", -"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", -"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", -"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", -"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", -"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", "Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France", "Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France", "Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France", +"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France", "le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", +"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France", "maison du vin": "52 rue des plantes, 75014 Paris, France", "Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", "Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", +"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", +"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France", +"Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France", +"Les caves populaires": "22 rue des Dames, 75017 Paris, France", +"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France", +"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France", +"La Brocante": "10 rue Rossini, 75009 Paris, France", +"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France", +"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France", +"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France", +"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France", +"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", +"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", +"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", +"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", +"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", "le lutece": "380 rue de vaugirard, 75015 Paris, France", +"O'Paris": "1 Rue des Envierges, 75020 Paris, France", "Rivolux": "16 rue de Rivoli, 75004 Paris, France", "Brasiloja": "16 rue Ganneron, 75018 Paris, France", -"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France", -"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France", -"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France", -"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France", -"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France", -"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France", -"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France", -"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", -"Melting Pot": "3 rue de Lagny, 75020 Paris, France", -"le Zango": "58 rue Daguerre, 75014 Paris, France", -"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", "Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France", "Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France", -"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", "Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", -"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", +"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France", "L'Angle": "28 rue de Ponthieu, 75008 Paris, France", +"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France", "Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France", +"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", "L'Entracte": "place de l'opera, 75002 Paris, France", "Panem": "18 rue de Crussol, 75011 Paris, France", "Au pays de Vannes": "34 bis rue de Wattignies, 75012 Paris, France", -"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France", -"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France", +"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France", "L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France", -"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", +"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France", "L'horizon": "93, rue de la Roquette, 75011 Paris, France", "L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", "Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France", +"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France", "Le Brigadier": "12 rue Blanche, 75009 Paris, France", -"Waikiki": "10 rue d\"Ulm, 75005 Paris, France"} \ No newline at end of file +"Waikiki": "10 rue d\"Ulm, 75005 Paris, France", +"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France", +"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France", +"Melting Pot": "3 rue de Lagny, 75020 Paris, France", +"le Zango": "58 rue Daguerre, 75014 Paris, France", +"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France", +"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France", +"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France", +"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", +"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France"} \ No newline at end of file diff --git a/bonobo/examples/datasets/coffeeshops.txt b/bonobo/examples/datasets/coffeeshops.txt index 5fe1ef6..b87eacb 100644 --- a/bonobo/examples/datasets/coffeeshops.txt +++ b/bonobo/examples/datasets/coffeeshops.txt @@ -1,182 +1,182 @@ -Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France les montparnos, 65 boulevard Pasteur, 75015 Paris, France -Le Saint Jean, 23 rue des abbesses, 75018 Paris, France -Le Felteu, 1 rue Pecquay, 75004 Paris, France +Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France +Café Lea, 5 rue Claude Bernard, 75005 Paris, France +Le Bellerive, 71 quai de Seine, 75019 Paris, France +Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France O q de poule, 53 rue du ruisseau, 75018 Paris, France +Le café des amis, 125 rue Blomet, 75015 Paris, France Le chantereine, 51 Rue Victoire, 75009 Paris, France Le Müller, 11 rue Feutrier, 75018 Paris, France -La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France -Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France -La Renaissance, 112 Rue Championnet, 75018 Paris, France Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France -Le Sully, 6 Bd henri IV, 75004 Paris, France -Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France -Le café des amis, 125 rue Blomet, 75015 Paris, France -Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France -Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France -Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France -Le Café Livres, 10 rue Saint Martin, 75004 Paris, France -Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France -Le Square, 31 rue Saint-Dominique, 75007 Paris, France -Les Arcades, 61 rue de Ponthieu, 75008 Paris, France +La Bauloise, 36 rue du hameau, 75015 Paris, France +Le Dellac, 14 rue Rougemont, 75009 Paris, France Le Bosquet, 46 avenue Bosquet, 75007 Paris, France +Le Sully, 6 Bd henri IV, 75004 Paris, France +Le Felteu, 1 rue Pecquay, 75004 Paris, France Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France +Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France +Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France +Le Saint Jean, 23 rue des abbesses, 75018 Paris, France +La Renaissance, 112 Rue Championnet, 75018 Paris, France +Le Square, 31 rue Saint-Dominique, 75007 Paris, France +Les Arcades, 61 rue de Ponthieu, 75008 Paris, France +Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France +Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France +Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France -Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France -Café Lea, 5 rue Claude Bernard, 75005 Paris, France -Le Bellerive, 71 quai de Seine, 75019 Paris, France -La Bauloise, 36 rue du hameau, 75015 Paris, France -Le Dellac, 14 rue Rougemont, 75009 Paris, France +La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France +Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France +Le Café Livres, 10 rue Saint Martin, 75004 Paris, France +Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France +Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France +Le pari's café, 104 rue caulaincourt, 75018 Paris, France +Le Poulailler, 60 rue saint-sabin, 75011 Paris, France +Chai 33, 33 Cour Saint Emilion, 75012 Paris, France +L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France +l'Usine, 1 rue d'Avron, 75020 Paris, France +La Bricole, 52 rue Liebniz, 75018 Paris, France +le ronsard, place maubert, 75005 Paris, France +Face Bar, 82 rue des archives, 75003 Paris, France +American Kitchen, 49 rue bichat, 75010 Paris, France +La Marine, 55 bis quai de valmy, 75010 Paris, France +Le Bloc, 21 avenue Brochant, 75017 Paris, France +La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France +Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France +La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France +Café Zen, 46 rue Victoire, 75009 Paris, France +O'Breizh, 27 rue de Penthièvre, 75008 Paris, France +Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France +Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France +La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France +Le Supercoin, 3, rue Baudelique, 75018 Paris, France +Populettes, 86 bis rue Riquet, 75018 Paris, France +Au bon coin, 49 rue des Cloys, 75018 Paris, France +Le Couvent, 69 rue Broca, 75013 Paris, France +La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France +L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France +Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France +Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France +Le Café frappé, 95 rue Montmartre, 75002 Paris, France +La Perle, 78 rue vieille du temple, 75003 Paris, France +Le Descartes, 1 rue Thouin, 75005 Paris, France +Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France +Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France +Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France +Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France +Le Malar, 88 rue Saint-Dominique, 75007 Paris, France +Au panini de la place, 47 rue Belgrand, 75020 Paris, France +Le Village, 182 rue de Courcelles, 75017 Paris, France +Pause Café, 41 rue de Charonne, 75011 Paris, France +Le Pure café, 14 rue Jean Macé, 75011 Paris, France +Extra old café, 307 fg saint Antoine, 75011 Paris, France +Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France +En attendant l'or, 3 rue Faidherbe, 75011 Paris, France Brûlerie San José, 30 rue des Petits-Champs, 75002 Paris, France Café de la Mairie (du VIII), rue de Lisbonne, 75008 Paris, France -Le General Beuret, 9 Place du General Beuret, 75015 Paris, France -Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France -En attendant l'or, 3 rue Faidherbe, 75011 Paris, France Café Martin, 2 place Martin Nadaud, 75001 Paris, France Etienne, 14 rue Turbigo, Paris, 75001 Paris, France L'ingénu, 184 bd Voltaire, 75011 Paris, France -Le Biz, 18 rue Favart, 75002 Paris, France L'Olive, 8 rue L'Olive, 75018 Paris, France -Le pari's café, 104 rue caulaincourt, 75018 Paris, France -Le Poulailler, 60 rue saint-sabin, 75011 Paris, France -La Marine, 55 bis quai de valmy, 75010 Paris, France -American Kitchen, 49 rue bichat, 75010 Paris, France -Chai 33, 33 Cour Saint Emilion, 75012 Paris, France -Face Bar, 82 rue des archives, 75003 Paris, France -Le Bloc, 21 avenue Brochant, 75017 Paris, France -La Bricole, 52 rue Liebniz, 75018 Paris, France -le ronsard, place maubert, 75005 Paris, France -l'Usine, 1 rue d'Avron, 75020 Paris, France -La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France -Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France -Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France -Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France +Le Biz, 18 rue Favart, 75002 Paris, France +Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France +Le General Beuret, 9 Place du General Beuret, 75015 Paris, France Le Germinal, 95 avenue Emile Zola, 75015 Paris, France +Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France Le refuge, 72 rue lamarck, 75018 Paris, France -Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France -Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France -O'Breizh, 27 rue de Penthièvre, 75008 Paris, France -Le Supercoin, 3, rue Baudelique, 75018 Paris, France -Populettes, 86 bis rue Riquet, 75018 Paris, France -La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France -L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France -Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France -Café Zen, 46 rue Victoire, 75009 Paris, France -La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France -Au bon coin, 49 rue des Cloys, 75018 Paris, France -La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France -Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France -Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France -Le Couvent, 69 rue Broca, 75013 Paris, France -Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France -La Perle, 78 rue vieille du temple, 75003 Paris, France -Le Café frappé, 95 rue Montmartre, 75002 Paris, France -L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France -Le Descartes, 1 rue Thouin, 75005 Paris, France -Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France -Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France -Au panini de la place, 47 rue Belgrand, 75020 Paris, France -Extra old café, 307 fg saint Antoine, 75011 Paris, France -Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France -Le Pure café, 14 rue Jean Macé, 75011 Paris, France -Le Village, 182 rue de Courcelles, 75017 Paris, France -Le Malar, 88 rue Saint-Dominique, 75007 Paris, France -Pause Café, 41 rue de Charonne, 75011 Paris, France -Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France -Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France -Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France -Caprice café, 12 avenue Jean Moulin, 75014 Paris, France -Caves populaires, 22 rue des Dames, 75017 Paris, France -Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France -Chez Prune, 36 rue Beaurepaire, 75010 Paris, France -L'anjou, 1 rue de Montholon, 75009 Paris, France -Le Brio, 216, rue Marcadet, 75018 Paris, France -Tamm Bara, 7 rue Clisson, 75013 Paris, France -La chaumière gourmande, Route de la Muette à Neuilly -Club hippique du Jardin d’Acclimatation, 75016 Paris, France -Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France -Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France -Le Centenaire, 104 rue amelot, 75011 Paris, France -Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France -Ragueneau, 202 rue Saint Honoré, 75001 Paris, France -L'Inévitable, 22 rue Linné, 75005 Paris, France +Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France Le Dunois, 77 rue Dunois, 75013 Paris, France La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France Le petit Bretonneau, Le petit Bretonneau - à l'intérieur de l'Hôpital, 75018 Paris, France +La chaumière gourmande, Route de la Muette à Neuilly +Club hippique du Jardin d’Acclimatation, 75016 Paris, France Le bal du pirate, 60 rue des bergers, 75015 Paris, France +Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France l'orillon bar, 35 rue de l'orillon, 75011 Paris, France zic zinc, 95 rue claude decaen, 75012 Paris, France +Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France +Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France +Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France +Le Centenaire, 104 rue amelot, 75011 Paris, France +Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France +Ragueneau, 202 rue Saint Honoré, 75001 Paris, France Café Pistache, 9 rue des petits champs, 75001 Paris, France La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 Paris, France -bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France Le Killy Jen, 28 bis boulevard Diderot, 75012 Paris, France Café beauveau, 9 rue de Miromesnil, 75008 Paris, France le 1 cinq, 172 rue de vaugirard, 75015 Paris, France -Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France Les Artisans, 106 rue Lecourbe, 75015 Paris, France Peperoni, 83 avenue de Wagram, 75001 Paris, France -Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France -La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France -Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France -La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France -Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France -La Brocante, 10 rue Rossini, 75009 Paris, France -Café Clochette, 16 avenue Richerand, 75010 Paris, France -L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France -NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France -O'Paris, 1 Rue des Envierges, 75020 Paris, France +Le Brio, 216, rue Marcadet, 75018 Paris, France +Tamm Bara, 7 rue Clisson, 75013 Paris, France +Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France +bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France +Caprice café, 12 avenue Jean Moulin, 75014 Paris, France +Caves populaires, 22 rue des Dames, 75017 Paris, France +Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France +Chez Prune, 36 rue Beaurepaire, 75010 Paris, France +L'Inévitable, 22 rue Linné, 75005 Paris, France +L'anjou, 1 rue de Montholon, 75009 Paris, France Botak cafe, 1 rue Paul albert, 75018 Paris, France -La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France -Les caves populaires, 22 rue des Dames, 75017 Paris, France -Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France -Trois pièces cuisine, 101 rue des dames, 75017 Paris, France -Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France -L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France -Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France -Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France -Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France +L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France +Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France maison du vin, 52 rue des plantes, 75014 Paris, France Le Tournebride, 104 rue Mouffetard, 75005 Paris, France Le Fronton, 63 rue de Ponthieu, 75008 Paris, France +Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France +La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France +Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France +Les caves populaires, 22 rue des Dames, 75017 Paris, France +Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France +Trois pièces cuisine, 101 rue des dames, 75017 Paris, France +La Brocante, 10 rue Rossini, 75009 Paris, France +Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France +Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France +Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France +La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France +La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France +Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France +Café Clochette, 16 avenue Richerand, 75010 Paris, France +L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France +NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France le lutece, 380 rue de vaugirard, 75015 Paris, France +O'Paris, 1 Rue des Envierges, 75020 Paris, France Rivolux, 16 rue de Rivoli, 75004 Paris, France Brasiloja, 16 rue Ganneron, 75018 Paris, France -Le café Monde et Médias, Place de la République, 75003 Paris, France -L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France -Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France -l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France -Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France -Pari's Café, 174 avenue de Clichy, 75017 Paris, France -Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France -Café Varenne, 36 rue de Varenne, 75007 Paris, France -Melting Pot, 3 rue de Lagny, 75020 Paris, France -le Zango, 58 rue Daguerre, 75014 Paris, France -Chez Miamophile, 6 rue Mélingue, 75019 Paris, France Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France Canopy Café associatif, 19 rue Pajol, 75018 Paris, France -Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France -Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France +Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France L'Angle, 28 rue de Ponthieu, 75008 Paris, France +Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France Café Dupont, 198 rue de la Convention, 75015 Paris, France +Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France L'Entracte, place de l'opera, 75002 Paris, France Panem, 18 rue de Crussol, 75011 Paris, France Au pays de Vannes, 34 bis rue de Wattignies, 75012 Paris, France -Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France -Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France +l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France -Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France +Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France L'horizon, 93, rue de la Roquette, 75011 Paris, France L'empreinte, 54, avenue Daumesnil, 75012 Paris, France Café Victor, 10 boulevard Victor, 75015 Paris, France +Café Varenne, 36 rue de Varenne, 75007 Paris, France Le Brigadier, 12 rue Blanche, 75009 Paris, France -Waikiki, 10 rue d"Ulm, 75005 Paris, France \ No newline at end of file +Waikiki, 10 rue d"Ulm, 75005 Paris, France +Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France +Pari's Café, 174 avenue de Clichy, 75017 Paris, France +Melting Pot, 3 rue de Lagny, 75020 Paris, France +le Zango, 58 rue Daguerre, 75014 Paris, France +Chez Miamophile, 6 rue Mélingue, 75019 Paris, France +Le café Monde et Médias, Place de la République, 75003 Paris, France +Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France +Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France +L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France \ No newline at end of file diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 635068e..4edb75e 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -3,7 +3,7 @@ from queue import Empty from time import sleep from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED -from bonobo.errors import InactiveReadableError +from bonobo.errors import InactiveReadableError, UnrecoverableError from bonobo.execution.base import LoopingExecutionContext from bonobo.structs.bags import Bag from bonobo.structs.inputs import Input @@ -93,6 +93,9 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): except Empty: sleep(self.PERIOD) continue + except UnrecoverableError as exc: + self.handle_error(exc, traceback.format_exc()) + break except Exception as exc: # pylint: disable=broad-except self.handle_error(exc, traceback.format_exc()) diff --git a/bonobo/nodes/io/base.py b/bonobo/nodes/io/base.py index d9b3212..58088d0 100644 --- a/bonobo/nodes/io/base.py +++ b/bonobo/nodes/io/base.py @@ -1,5 +1,6 @@ from bonobo import settings from bonobo.config import Configurable, ContextProcessor, Option, Service +from bonobo.errors import UnrecoverableValueError, UnrecoverableNotImplementedError from bonobo.structs.bags import Bag @@ -9,21 +10,21 @@ class IOFormatEnabled(Configurable): def get_input(self, *args, **kwargs): if self.ioformat == settings.IOFORMAT_ARG0: if len(args) != 1 or len(kwargs): - raise ValueError( + raise UnrecoverableValueError( 'Wrong input formating: IOFORMAT=ARG0 implies one arg and no kwargs, got args={!r} and kwargs={!r}.'. - format(args, kwargs) + format(args, kwargs) ) return args[0] if self.ioformat == settings.IOFORMAT_KWARGS: if len(args) or not len(kwargs): - raise ValueError( + raise UnrecoverableValueError( 'Wrong input formating: IOFORMAT=KWARGS ioformat implies no arg, got args={!r} and kwargs={!r}.'. - format(args, kwargs) + format(args, kwargs) ) return kwargs - raise NotImplementedError('Unsupported format.') + raise UnrecoverableNotImplementedError('Unsupported format.') def get_output(self, row): if self.ioformat == settings.IOFORMAT_ARG0: @@ -32,7 +33,7 @@ class IOFormatEnabled(Configurable): if self.ioformat == settings.IOFORMAT_KWARGS: return Bag(**row) - raise NotImplementedError('Unsupported format.') + raise UnrecoverableNotImplementedError('Unsupported format.') class FileHandler(Configurable): From 0bcdbd70ab948c03480f04f7f99a931dfd703c2d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 11:52:47 +0200 Subject: [PATCH 114/143] [nodes] Removes old pretty printers (bonobo.pprint, bonobo.PrettyPrint) in favor of simpler bonobo.PrettyPrinter implementation. /!\ BC break /!\ --- bonobo/_api.py | 3 +-- bonobo/nodes/basics.py | 49 ------------------------------------------ 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 89b6d4c..6cf328c 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,6 +1,6 @@ from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop, pprint + PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop from bonobo.strategies import create_strategy from bonobo.util.objects import get_name @@ -109,7 +109,6 @@ register_api_group( count, identity, noop, - pprint, ) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index c1ead61..85b2114 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -1,14 +1,11 @@ import functools import itertools -from colorama import Fore, Style - from bonobo import settings from bonobo.config import Configurable, Option from bonobo.config.processors import ContextProcessor from bonobo.constants import NOT_MODIFIED from bonobo.structs.bags import Bag -from bonobo.util.compat import deprecated from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL @@ -17,7 +14,6 @@ __all__ = [ 'Limit', 'Tee', 'count', - 'pprint', 'PrettyPrinter', 'noop', ] @@ -87,51 +83,6 @@ class PrettyPrinter(Configurable): ) -_pprint = PrettyPrinter() - - -@deprecated -def pprint(*args, **kwargs): - return _pprint(*args, **kwargs) - - -def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True): - from bonobo.constants import NOT_MODIFIED - - def _pprint(*args, **kwargs): - nonlocal title_keys, sort, print_values - - row = args[0] - for key in title_keys: - if key in row: - print(Style.BRIGHT, row.get(key), Style.RESET_ALL, sep='') - break - - if print_values: - for k in sorted(row) if sort else row: - print( - ' • ', - Fore.BLUE, - k, - Style.RESET_ALL, - ' : ', - Fore.BLACK, - '(', - type(row[k]).__name__, - ')', - Style.RESET_ALL, - ' ', - repr(row[k]), - CLEAR_EOL, - ) - - yield NOT_MODIFIED - - _pprint.__name__ = 'pprint' - - return _pprint - - def noop(*args, **kwargs): # pylint: disable=unused-argument from bonobo.constants import NOT_MODIFIED return NOT_MODIFIED From 6ef25deac9a624f115459e3fbc7cdfb2320d1b2a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 12:01:37 +0200 Subject: [PATCH 115/143] [config] Adds test for requires() decorator. --- bonobo/errors.py | 11 +++++++---- tests/config/test_services.py | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/bonobo/errors.py b/bonobo/errors.py index 8510a50..08b97d4 100644 --- a/bonobo/errors.py +++ b/bonobo/errors.py @@ -58,19 +58,22 @@ class ConfigurationError(Exception): pass -class MissingServiceImplementationError(KeyError): - pass - - class UnrecoverableError(Exception): """Flag for errors that must interrupt the workflow, either because they will happen for sure on each node run, or because you know that your transformation has no point continuing runnning after a bad event.""" + class UnrecoverableValueError(UnrecoverableError, ValueError): pass + class UnrecoverableRuntimeError(UnrecoverableError, RuntimeError): pass + class UnrecoverableNotImplementedError(UnrecoverableError, NotImplementedError): pass + + +class MissingServiceImplementationError(UnrecoverableError, KeyError): + pass diff --git a/tests/config/test_services.py b/tests/config/test_services.py index b762dbe..ff81e82 100644 --- a/tests/config/test_services.py +++ b/tests/config/test_services.py @@ -3,7 +3,7 @@ import time import pytest -from bonobo.config import Configurable, Container, Exclusive, Service +from bonobo.config import Configurable, Container, Exclusive, Service, requires from bonobo.config.services import validate_service_name @@ -94,3 +94,23 @@ def test_exclusive(): 'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3', '2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4' ] + + +def test_requires(): + vcr = VCR() + + services = Container( + output=vcr.append + ) + + @requires('output') + def append(out, x): + out(x) + + svcargs = services.args_for(append) + assert len(svcargs) == 1 + assert svcargs[0] == vcr.append + + + + From 9801c75720ceb5715e805c26d4be07c734155cac Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 12:41:14 +0200 Subject: [PATCH 116/143] [settings] Better impl. of Setting class, tests for it and refactor hardcoded settings to use it. --- bonobo/_api.py | 2 +- bonobo/commands/__init__.py | 6 ++-- bonobo/commands/run.py | 4 +-- bonobo/ext/console.py | 4 +-- bonobo/logging.py | 2 +- bonobo/nodes/basics.py | 2 +- bonobo/settings.py | 53 ++++++++++++++++++++++++------- tests/test_settings.py | 63 +++++++++++++++++++++++++++++++++++++ 8 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 tests/test_settings.py diff --git a/bonobo/_api.py b/bonobo/_api.py index 6cf328c..ab890c6 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -45,7 +45,7 @@ def run(graph, strategy=None, plugins=None, services=None): from bonobo import settings settings.check() - if not settings.QUIET: # pragma: no cover + if not settings.QUIET.get(): # pragma: no cover if _is_interactive_console(): from bonobo.ext.console import ConsoleOutputPlugin if ConsoleOutputPlugin not in plugins: diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 59e6dfb..4e183a3 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -27,9 +27,9 @@ def entrypoint(args=None): args = parser.parse_args(args).__dict__ if args.pop('debug', False): - settings.DEBUG = True - settings.LOGGING_LEVEL = logging.DEBUG - logging.set_level(settings.LOGGING_LEVEL) + settings.DEBUG.set(True) + settings.LOGGING_LEVEL.set(logging.DEBUG) + logging.set_level(settings.LOGGING_LEVEL.get()) logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) commands[args.pop('command')](**args) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 7f29d3f..6de6bf6 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -31,10 +31,10 @@ def execute(filename, module, install=False, quiet=False, verbose=False): from bonobo import Graph, run, settings if quiet: - settings.QUIET = True + settings.QUIET.set(True) if verbose: - settings.DEBUG = True + settings.DEBUG.set(True) if filename: if os.path.isdir(filename): diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index f30fae0..acf464b 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -65,7 +65,7 @@ class ConsoleOutputPlugin(Plugin): for i in context.graph.topologically_sorted_indexes: node = context[i] - name_suffix = '({})'.format(i) if settings.DEBUG else '' + name_suffix = '({})'.format(i) if settings.DEBUG.get() else '' if node.alive: _line = ''.join( ( @@ -100,7 +100,7 @@ class ConsoleOutputPlugin(Plugin): print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr) def _write(self, graph_context, rewind): - if settings.PROFILE: + if settings.PROFILE.get(): if self.counter % 10 and self._append_cache: append = self._append_cache else: diff --git a/bonobo/logging.py b/bonobo/logging.py index 17bdeb7..3784600 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -75,4 +75,4 @@ def get_logger(name='bonobo'): getLogger = get_logger # Setup formating and level. -setup(level=settings.LOGGING_LEVEL) +setup(level=settings.LOGGING_LEVEL.get()) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 85b2114..164eeb1 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -69,7 +69,7 @@ def _count_counter(self, context): class PrettyPrinter(Configurable): def call(self, *args, **kwargs): - formater = self._format_quiet if settings.QUIET else self._format_console + formater = self._format_quiet if settings.QUIET.get() else self._format_console for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): print(formater(i, item, value)) diff --git a/bonobo/settings.py b/bonobo/settings.py index 8e8a780..e5edd83 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -5,6 +5,10 @@ from bonobo.errors import ValidationError def to_bool(s): + if s is None: + return False + if type(s) is bool: + return s if len(s): if s.lower() in ('f', 'false', 'n', 'no', '0'): return False @@ -13,7 +17,18 @@ def to_bool(s): class Setting: - def __init__(self, name, default=None, validator=None): + __all__ = {} + + @classmethod + def clear_all(cls): + for setting in Setting.__all__.values(): + setting.clear() + + def __new__(cls, name, *args, **kwargs): + Setting.__all__[name] = super().__new__(cls) + return Setting.__all__[name] + + def __init__(self, name, default=None, validator=None, formatter=None): self.name = name if default: @@ -21,15 +36,14 @@ class Setting: else: self.default = lambda: None - if validator: - self.validator = validator - else: - self.validator = None + self.validator = validator + self.formatter = formatter def __repr__(self): return ''.format(self.name, self.get()) def set(self, value): + value = self.formatter(value) if self.formatter else value if self.validator and not self.validator(value): raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) self.value = value @@ -38,21 +52,35 @@ class Setting: try: return self.value except AttributeError: - self.value = self.default() + value = os.environ.get(self.name, None) + if value is None: + value = self.default() + self.set(value) return self.value + def clear(self): + try: + del self.value + except AttributeError: + pass + # Debug/verbose mode. -DEBUG = to_bool(os.environ.get('DEBUG', 'f')) +DEBUG = Setting('DEBUG', formatter=to_bool, default=False) # Profile mode. -PROFILE = to_bool(os.environ.get('PROFILE', 'f')) +PROFILE = Setting('PROFILE', formatter=to_bool, default=False) # Quiet mode. -QUIET = to_bool(os.environ.get('QUIET', 'f')) +QUIET = Setting('QUIET', formatter=to_bool, default=False) # Logging level. -LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO +LOGGING_LEVEL = Setting( + 'LOGGING_LEVEL', + formatter=logging._checkLevel, + validator=logging._checkLevel, + default=lambda: logging.DEBUG if DEBUG.get() else logging.INFO +) # Input/Output format for transformations IOFORMAT_ARG0 = 'arg0' @@ -67,5 +95,8 @@ IOFORMAT = Setting('IOFORMAT', default=IOFORMAT_KWARGS, validator=IOFORMATS.__co def check(): - if DEBUG and QUIET: + if DEBUG.get() and QUIET.get(): raise RuntimeError('I cannot be verbose and quiet at the same time.') + + +clear_all = Setting.clear_all diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..c8313c5 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,63 @@ +import logging +from os import environ +from unittest.mock import patch + +import pytest + +from bonobo import settings + +TEST_SETTING = 'TEST_SETTING' + + +def test_to_bool(): + assert not settings.to_bool('') + assert not settings.to_bool('FALSE') + assert not settings.to_bool('NO') + assert not settings.to_bool('0') + + assert settings.to_bool('yup') + assert settings.to_bool('True') + assert settings.to_bool('yes') + assert settings.to_bool('1') + + +def test_setting(): + s = settings.Setting(TEST_SETTING) + assert s.get() is None + + with patch.dict(environ, {TEST_SETTING: 'hello'}): + assert s.get() is None + s.clear() + assert s.get() == 'hello' + + s = settings.Setting(TEST_SETTING, default='nope') + assert s.get() is 'nope' + + with patch.dict(environ, {TEST_SETTING: 'hello'}): + assert s.get() == 'nope' + s.clear() + assert s.get() == 'hello' + + +def test_default_settings(): + settings.clear_all() + + assert settings.DEBUG.get() == False + assert settings.PROFILE.get() == False + assert settings.QUIET.get() == False + assert settings.LOGGING_LEVEL.get() == logging._checkLevel('INFO') + + with patch.dict(environ, {'DEBUG': 't'}): + settings.clear_all() + assert settings.LOGGING_LEVEL.get() == logging._checkLevel('DEBUG') + + settings.clear_all() + + +def test_check(): + settings.check() + with patch.dict(environ, {'DEBUG': 't', 'PROFILE': 't', 'QUIET': 't'}): + settings.clear_all() + with pytest.raises(RuntimeError): + settings.check() + settings.clear_all() From 8de6f50523d74ff08d81a0764d19259393c705ae Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 13:08:53 +0200 Subject: [PATCH 117/143] [examples] Fix examples, fix termination bug with unrecoverable errors. --- bonobo/examples/datasets/fablabs.py | 8 ++++---- bonobo/examples/nodes/filter.py | 7 +++++-- bonobo/examples/nodes/slow.py | 3 +++ bonobo/examples/tutorials/tut02e02_write.py | 2 +- bonobo/execution/node.py | 1 + bonobo/structs/inputs.py | 18 +++++++++++------- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/bonobo/examples/datasets/fablabs.py b/bonobo/examples/datasets/fablabs.py index be95fe1..33ed91c 100644 --- a/bonobo/examples/datasets/fablabs.py +++ b/bonobo/examples/datasets/fablabs.py @@ -73,15 +73,15 @@ def display(row): print( ' - {}address{}: {address}'. - format(Fore.BLUE, Style.RESET_ALL, address=', '.join(address)) + format(Fore.BLUE, Style.RESET_ALL, address=', '.join(address)) ) print( ' - {}links{}: {links}'. - format(Fore.BLUE, Style.RESET_ALL, links=', '.join(row['links'])) + format(Fore.BLUE, Style.RESET_ALL, links=', '.join(row['links'])) ) print( ' - {}geometry{}: {geometry}'. - format(Fore.BLUE, Style.RESET_ALL, **row) + format(Fore.BLUE, Style.RESET_ALL, **row) ) print( ' - {}source{}: {source}'.format( @@ -96,8 +96,8 @@ graph = bonobo.Graph( ), normalize, filter_france, + bonobo.JsonWriter(path='fablabs.txt', ioformat='arg0'), bonobo.Tee(display), - bonobo.JsonWriter(path='fablabs.txt'), ) if __name__ == '__main__': diff --git a/bonobo/examples/nodes/filter.py b/bonobo/examples/nodes/filter.py index bf390e9..4f7219a 100644 --- a/bonobo/examples/nodes/filter.py +++ b/bonobo/examples/nodes/filter.py @@ -9,13 +9,16 @@ class OddOnlyFilter(Filter): @Filter -def MultiplesOfThreeOnlyFilter(self, i): +def multiples_of_three(i): return not (i % 3) graph = bonobo.Graph( lambda: tuple(range(50)), OddOnlyFilter(), - MultiplesOfThreeOnlyFilter(), + multiples_of_three, print, ) + +if __name__ == '__main__': + bonobo.run(graph) diff --git a/bonobo/examples/nodes/slow.py b/bonobo/examples/nodes/slow.py index b9623af..ecaaf44 100644 --- a/bonobo/examples/nodes/slow.py +++ b/bonobo/examples/nodes/slow.py @@ -14,3 +14,6 @@ graph = bonobo.Graph( pause, print, ) + +if __name__ == '__main__': + bonobo.run(graph) diff --git a/bonobo/examples/tutorials/tut02e02_write.py b/bonobo/examples/tutorials/tut02e02_write.py index 1d41ac2..664bca6 100644 --- a/bonobo/examples/tutorials/tut02e02_write.py +++ b/bonobo/examples/tutorials/tut02e02_write.py @@ -8,7 +8,7 @@ def split_one(line): graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one, - bonobo.JsonWriter('coffeeshops.json'), + bonobo.JsonWriter('coffeeshops.json', ioformat='arg0'), ) if __name__ == '__main__': diff --git a/bonobo/execution/node.py b/bonobo/execution/node.py index 4edb75e..45691a6 100644 --- a/bonobo/execution/node.py +++ b/bonobo/execution/node.py @@ -95,6 +95,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext): continue except UnrecoverableError as exc: self.handle_error(exc, traceback.format_exc()) + self.input.shutdown() break except Exception as exc: # pylint: disable=broad-except self.handle_error(exc, traceback.format_exc()) diff --git a/bonobo/structs/inputs.py b/bonobo/structs/inputs.py index cf9a6ec..7cfe12f 100644 --- a/bonobo/structs/inputs.py +++ b/bonobo/structs/inputs.py @@ -77,6 +77,12 @@ class Input(Queue, Readable, Writable): return Queue.put(self, data, block, timeout) + def _decrement_runlevel(self): + if self._runlevel == 1: + self.on_finalize() + self._runlevel -= 1 + self.on_end() + def get(self, block=True, timeout=None): if not self.alive: raise InactiveReadableError('Cannot get() on an inactive {}.'.format(Readable.__name__)) @@ -84,13 +90,7 @@ class Input(Queue, Readable, Writable): data = Queue.get(self, block, timeout) if data == END: - if self._runlevel == 1: - self.on_finalize() - - self._runlevel -= 1 - - # callback - self.on_end() + self._decrement_runlevel() if not self.alive: raise InactiveReadableError( @@ -100,6 +100,10 @@ class Input(Queue, Readable, Writable): return data + def shutdown(self): + while self._runlevel >= 1: + self._decrement_runlevel() + def empty(self): self.mutex.acquire() while self._qsize() and self.queue[0] == END: From 4a2c7280d6f8b54f55fa3402dd992a14bed04ddb Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 13:09:46 +0200 Subject: [PATCH 118/143] [misc] Fixes formatting. --- bonobo/config/configurables.py | 7 +++---- bonobo/config/options.py | 9 ++++----- bonobo/examples/datasets/fablabs.py | 6 +++--- bonobo/nodes/io/base.py | 4 ++-- bonobo/util/inspect.py | 20 +++++++++++--------- tests/config/test_configurables.py | 1 - tests/config/test_methods.py | 10 +++++----- tests/config/test_methods_partial.py | 4 ++-- tests/config/test_services.py | 8 +------- 9 files changed, 31 insertions(+), 38 deletions(-) diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 01db9e0..7b40303 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -51,7 +51,7 @@ class ConfigurableMeta(type): return (processor for _, processor in cls.__processors) def __repr__(self): - return ' '.join((' Date: Wed, 5 Jul 2017 19:28:42 +0200 Subject: [PATCH 119/143] [doc] Documentation, my dear. Half of the work, looks you are a little behind on quotas ... --- bonobo/examples/tutorials/tut02e01_read.py | 9 +- bonobo/examples/tutorials/tut02e02_write.py | 16 +- .../examples/tutorials/tut02e03_writeasmap.py | 18 +- docs/_templates/index.html | 207 +++++++++--------- docs/guide/ext/sqlalchemy.rst | 1 + docs/index.rst | 2 +- docs/install.rst | 48 ++-- docs/tutorial/index.rst | 19 +- docs/tutorial/tut01.rst | 56 ++--- docs/tutorial/tut02.rst | 29 ++- docs/tutorial/tut03.rst | 190 +++++++++++++++- 11 files changed, 418 insertions(+), 177 deletions(-) diff --git a/bonobo/examples/tutorials/tut02e01_read.py b/bonobo/examples/tutorials/tut02e01_read.py index 0eb6786..362051a 100644 --- a/bonobo/examples/tutorials/tut02e01_read.py +++ b/bonobo/examples/tutorials/tut02e01_read.py @@ -5,7 +5,10 @@ graph = bonobo.Graph( print, ) + +def get_services(): + return {'fs': bonobo.open_examples_fs('datasets')} + + if __name__ == '__main__': - bonobo.run( - graph, services={'fs': bonobo.open_examples_fs('datasets')} - ) + bonobo.run(graph, services=get_services()) diff --git a/bonobo/examples/tutorials/tut02e02_write.py b/bonobo/examples/tutorials/tut02e02_write.py index 1d41ac2..e5a8445 100644 --- a/bonobo/examples/tutorials/tut02e02_write.py +++ b/bonobo/examples/tutorials/tut02e02_write.py @@ -8,10 +8,18 @@ def split_one(line): graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one, - bonobo.JsonWriter('coffeeshops.json'), + bonobo.JsonWriter( + 'coffeeshops.json', fs='fs.output', ioformat='arg0' + ), ) + +def get_services(): + return { + 'fs': bonobo.open_examples_fs('datasets'), + 'fs.output': bonobo.open_fs(), + } + + if __name__ == '__main__': - bonobo.run( - graph, services={'fs': bonobo.open_examples_fs('datasets')} - ) + bonobo.run(graph, services=get_services()) diff --git a/bonobo/examples/tutorials/tut02e03_writeasmap.py b/bonobo/examples/tutorials/tut02e03_writeasmap.py index 3fe4c08..e234f22 100644 --- a/bonobo/examples/tutorials/tut02e03_writeasmap.py +++ b/bonobo/examples/tutorials/tut02e03_writeasmap.py @@ -1,4 +1,6 @@ -import bonobo, json +import json + +import bonobo def split_one_to_map(line): @@ -18,10 +20,16 @@ class MyJsonWriter(bonobo.JsonWriter): graph = bonobo.Graph( bonobo.FileReader('coffeeshops.txt'), split_one_to_map, - MyJsonWriter('coffeeshops.json'), + MyJsonWriter('coffeeshops.json', fs='fs.output', ioformat='arg0'), ) + +def get_services(): + return { + 'fs': bonobo.open_examples_fs('datasets'), + 'fs.output': bonobo.open_fs(), + } + + if __name__ == '__main__': - bonobo.run( - graph, services={'fs': bonobo.open_examples_fs('datasets')} - ) + bonobo.run(graph, services=get_services()) diff --git a/docs/_templates/index.html b/docs/_templates/index.html index b778554..8f9185a 100644 --- a/docs/_templates/index.html +++ b/docs/_templates/index.html @@ -2,105 +2,116 @@ {% set title = _('Bonobo — Data processing for humans') %} {% block body %} -
    - Bonobo is ALPHA software. Some APIs will change. -
    +

    + +

    -

    - -

    - -

    - {% trans %} - Bonobo is a line-by-line data-processing toolkit for python 3.5+ (extract-transform-load - framework) emphasizing simple and atomic data transformations defined using a directed graph of plain old - python objects (functions, iterables, generators, ...). - {% endtrans %} -

    - -

    {% trans %}Documentation{% endtrans %}

    - -
    -
    - - - - - - - - - - - - -
    - - - -
    - - - -
    - - - -
    - -

    Features

    - -
      -
    • - {% trans %} - 10 minutes to get started: Know some python? Writing your first data processor is an affair - of minutes. - {% endtrans %} -
    • -
    • - {% trans %} - Data sources and targets: HTML, JSON, XML, SQL databases, NoSQL databases, HTTP/REST APIs, - streaming APIs, python objects... - {% endtrans %} -
    • -
    • - {% trans %} - Service injection: Abstract the transformation dependencies to easily switch data sources and - dependant libraries. You'll be able to specify the concrete implementations or configurations at - runtime, for example to switch a database connection string or an API endpoint. - {% endtrans %} -
    • -
    • - {% trans %} - Plugins: Easily add features to all your transformations by using builtin plugins (Jupyter, - Console, ...) or write your own. - {% endtrans %} -
    • -
    • - {% trans %} - Bonobo is young, and the todo-list is huge. Read the roadmap. - {% endtrans %} -
    • -
    - -

    {% trans %} - You can also download PDF/EPUB versions of the Bonobo documentation: - PDF version, - EPUB version. +

    + {% trans %} + Bonobo is a line-by-line data-processing toolkit for python 3.5+ (extract-transform-load + framework, or ETL) emphasizing simple and atomic data transformations defined using a directed graph of plain old + python objects (functions, iterables, generators, ...). {% endtrans %} -

    +

    +
    + Bonobo is ALPHA software. Some APIs will change. +
    + + +

    {% trans %}Documentation{% endtrans %}

    + + + + + + + + + + + + + + +
    + + + +
    + + + +
    + + + +
    + +

    Features

    + +
      +
    • + {% trans %} + 10 minutes to get started: Know some python? Writing your first data processor is an affair + of minutes. + {% endtrans %} +
    • +
    • + {% trans %} + Data sources and targets: HTML, JSON, XML, SQL databases, NoSQL databases, HTTP/REST APIs, + streaming APIs, python objects... + {% endtrans %} +
    • +
    • + {% trans %} + Service injection: Abstract the transformation dependencies to easily switch data sources and + dependant libraries. You'll be able to specify the concrete implementations or configurations at + runtime, for example to switch a database connection string or an API endpoint. + {% endtrans %} +
    • +
    • + {% trans %} + Plugins: Easily add features to all your transformations by using builtin plugins (Jupyter, + Console, ...) or write your own. + {% endtrans %} +
    • +
    • + {% trans %} + Bonobo is young, and the todo-list is huge. Read the roadmap. + {% endtrans %} +
    • +
    + +

    {% trans %} + You can also download PDF/EPUB versions of the Bonobo documentation: + PDF version, + EPUB version. + {% endtrans %} +

    + +

    Table of contents

    + + +
    + {{ toctree(maxdepth=2, collapse=False)}} +
    {% endblock %} diff --git a/docs/guide/ext/sqlalchemy.rst b/docs/guide/ext/sqlalchemy.rst index 0f9c549..d7da4e8 100644 --- a/docs/guide/ext/sqlalchemy.rst +++ b/docs/guide/ext/sqlalchemy.rst @@ -4,6 +4,7 @@ Bonobo with SQLAlchemy .. todo:: The `bonobo-sqlalchemy` package is at a very alpha stage, and things will change. This section is here to give a brief overview but is neither complete nor definitive. +Read the introduction: https://www.bonobo-project.org/with/sqlalchemy Installation :::::::::::: diff --git a/docs/index.rst b/docs/index.rst index c2eeb8d..8fbcd6e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,8 +8,8 @@ Bonobo tutorial/index guide/index reference/index - contribute/index faq + contribute/index genindex modindex diff --git a/docs/install.rst b/docs/install.rst index 41487e4..ac951e5 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,36 +4,47 @@ Installation Create an ETL project ::::::::::::::::::::: -If you only want to use Bonobo to code ETLs, your easiest option to get started is to use our -`cookiecutter template `_. +Creating a project and starting to write code should take less than a minute: + +.. code-block:: shell-session + + $ pip install --upgrade bonobo cookiecutter + $ bonobo init my-etl-project + $ bonobo run my-etl-project + +Once you bootstrapped a project, you can start editing the default example transformation by editing +`my-etl-project/main.py`. + +Other installation options +:::::::::::::::::::::::::: Install from PyPI -::::::::::::::::: +----------------- -You can also install it directly from the `Python Package Index `_. +You can install it directly from the `Python Package Index `_ (like we did above). .. code-block:: shell-session $ pip install bonobo Install from source -::::::::::::::::::: +------------------- If you want to install an unreleased version, you can use git urls with pip. This is useful when using bonobo as a -dependency of your code and you want to try a forked version of bonobo with your software. You can use the git+http -string in your `requirements.txt` file. However, the best option for development on bonobo directly is not this one, -but editable installs (see below). +dependency of your code and you want to try a forked version of bonobo with your software. You can use a `git+http` +string in your `requirements.txt` file. However, the best option for development on bonobo is an editable install (see +below). .. code-block:: shell-session - $ pip install git+https://github.com/python-bonobo/bonobo.git@master#egg=bonobo + $ pip install git+https://github.com/python-bonobo/bonobo.git@develop#egg=bonobo Editable install -:::::::::::::::: +---------------- -If you plan on making patches to Bonobo, you should install it as an "editable" package, which is a really great pip feature. -Pip will clone your repository in a source directory and create a symlink for it in the site-package directory of your -python interpreter. +If you plan on making patches to Bonobo, you should install it as an "editable" package, which is a really great pip +feature. Pip will clone your repository in a source directory and create a symlink for it in the site-package directory +of your python interpreter. .. code-block:: shell-session @@ -63,20 +74,17 @@ I usually name the git remote for the main bonobo repository "upstream", and my $ git remote rename origin upstream $ git remote add origin git@github.com:hartym/bonobo.git + $ git fetch --all Of course, replace my github username by the one you used to fork bonobo. You should be good to go! Windows support ::::::::::::::: -There are problems on the windows platform, mostly due to the fact bonobo was not developed by experienced windows users. +There are minor issues on the windows platform, mostly due to the fact bonobo was not developed by experienced windows +users. We're trying to look into that but energy available to provide serious support on windows is very limited. + If you have experience in this domain and you're willing to help, you're more than welcome! - - -.. todo:: - - Better install docs, especially on how to use different forks or branches, etc. - diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 342449b..d449df5 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -9,17 +9,26 @@ python code in charge of handling similar shaped independant lines of data. Bonobo *is not* a statistical or data-science tool. If you're looking for a data-analysis tool in python, use Pandas. -Bonobo is a lean manufacturing assembly line for data that let you focus on the actual work instead of the plumbery. +Bonobo is a lean manufacturing assembly line for data that let you focus on the actual work instead of the plumbery +(execution contexts, parallelism, error handling, console output, logging, ...). Bonobo uses simple python and should be quick and easy to learn. Tutorial :::::::: -Warning: the documentation is still in progress. Although all content here should be accurate, you may feel a lack of -completeness, for which we plaid guilty and apologize. If there is something blocking, please come on our -`slack channel `_ and complain, we'll figure something out. If there is something -that did not block you but can be a no-go for others, please consider contributing to the docs. +.. note:: + + Good documentation is not easy to write. We do our best to make it better and better. + + Although all content here should be accurate, you may feel a lack of completeness, for which we plaid guilty and + apologize. + + If you're stuck, please come and ask on our `slack channel `_, we'll figure + something out. + + If you're not stuck but had trouble understanding something, please consider contributing to the docs (via github + pull requests). .. toctree:: :maxdepth: 2 diff --git a/docs/tutorial/tut01.rst b/docs/tutorial/tut01.rst index 0357926..d6aa604 100644 --- a/docs/tutorial/tut01.rst +++ b/docs/tutorial/tut01.rst @@ -19,7 +19,7 @@ can run. .. code-block:: shell-session - bonobo init tutorial + $ bonobo init tutorial This will create a `tutorial` directory (`content description here `_). @@ -27,15 +27,15 @@ To run this project, use: .. code-block:: shell-session - bonobo run tutorial + $ bonobo run tutorial Write a first transformation :::::::::::::::::::::::::::: -Open `tutorial/__main__.py`, and delete all the code here. +Open `tutorial/main.py`, and delete all the code here. -A transformation can be whatever python can call, having inputs and outputs. Simplest transformations are functions. +A transformation can be whatever python can call. Simplest transformations are functions and generators. Let's write one: @@ -48,10 +48,10 @@ Easy. .. note:: - This is about the same as :func:`str.upper`, and in the real world, you'd use it directly. + This function is very similar to :func:`str.upper`, which you can use directly. Let's write two more transformations for the "extract" and "load" steps. In this example, we'll generate the data from -scratch, and we'll use stdout to simulate data-persistence. +scratch, and we'll use stdout to "simulate" data-persistence. .. code-block:: python @@ -68,16 +68,16 @@ on things returned, and a normal function will just be seen as a generator that .. note:: - Once again, :func:`print` would be used directly in a real-world transformation. + Once again, you should use the builtin :func:`print` directly instead of this `load()` function. Create a transformation graph ::::::::::::::::::::::::::::: -Bonobo main roles are two things: +Amongst other features, Bonobo will mostly help you there with the following: * Execute the transformations in independant threads -* Pass the outputs of one thread to other(s) thread(s). +* Pass the outputs of one thread to other(s) thread(s) inputs. To do this, it needs to know what data-flow you want to achieve, and you'll use a :class:`bonobo.Graph` to describe it. @@ -109,17 +109,17 @@ To do this, it needs to know what data-flow you want to achieve, and you'll use Execute the job ::::::::::::::: -Save `tutorial/__main__.py` and execute your transformation: +Save `tutorial/main.py` and execute your transformation again: .. code-block:: shell-session - bonobo run tutorial + $ bonobo run tutorial This example is available in :mod:`bonobo.examples.tutorials.tut01e01`, and you can also run it as a module: .. code-block:: shell-session - bonobo run -m bonobo.examples.tutorials.tut01e01 + $ bonobo run -m bonobo.examples.tutorials.tut01e01 Rewrite it using builtins @@ -127,27 +127,17 @@ Rewrite it using builtins There is a much simpler way to describe an equivalent graph: -.. code-block:: python +.. literalinclude:: ../../bonobo/examples/tutorials/tut01e02.py + :language: python - import bonobo +The `extract()` generator has been replaced by a list, as Bonobo will interpret non-callable iterables as a no-input +generator. - graph = bonobo.Graph( - ['foo', 'bar', 'baz',], - str.upper, - print, - ) - - if __name__ == '__main__': - bonobo.run(graph) - -We use a shortcut notation for the generator, with a list. Bonobo will wrap an iterable as a generator by itself if it -is added in a graph. - -This example is available in :mod:`bonobo.examples.tutorials.tut01e02`, and you can also run it as a module: +This example is also available in :mod:`bonobo.examples.tutorials.tut01e02`, and you can also run it as a module: .. code-block:: shell-session - bonobo run -m bonobo.examples.tutorials.tut01e02 + $ bonobo run -m bonobo.examples.tutorials.tut01e02 You can now jump to the next part (:doc:`tut02`), or read a small summary of concepts and definitions introduced here below. @@ -188,19 +178,19 @@ cases. Concepts and definitions :::::::::::::::::::::::: -* Transformation: a callable that takes input (as call parameters) and returns output(s), either as its return value or +* **Transformation**: a callable that takes input (as call parameters) and returns output(s), either as its return value or by yielding values (a.k.a returning a generator). -* Transformation graph (or Graph): a set of transformations tied together in a :class:`bonobo.Graph` instance, which is +* **Transformation graph (or Graph)**: a set of transformations tied together in a :class:`bonobo.Graph` instance, which is a directed acyclic graph (or DAG). -* Node: a graph element, most probably a transformation in a graph. +* **Node**: a graph element, most probably a transformation in a graph. -* Execution strategy (or strategy): a way to run a transformation graph. It's responsibility is mainly to parallelize +* **Execution strategy (or strategy)**: a way to run a transformation graph. It's responsibility is mainly to parallelize (or not) the transformations, on one or more process and/or computer, and to setup the right queuing mechanism for transformations' inputs and outputs. -* Execution context (or context): a wrapper around a node that holds the state for it. If the node needs state, there +* **Execution context (or context)**: a wrapper around a node that holds the state for it. If the node needs state, there are tools available in bonobo to feed it to the transformation using additional call parameters, keeping transformations stateless. diff --git a/docs/tutorial/tut02.rst b/docs/tutorial/tut02.rst index ff562d1..b1545f9 100644 --- a/docs/tutorial/tut02.rst +++ b/docs/tutorial/tut02.rst @@ -23,16 +23,18 @@ When run, the execution strategy wraps every component in a thread (assuming you :class:`bonobo.strategies.ThreadPoolExecutorStrategy`). Bonobo will send each line of data in the input node's thread (here, `A`). Now, each time `A` *yields* or *returns* -something, it will be pushed on `B` input :class:`queue.Queue`, and will be consumed by `B`'s thread. +something, it will be pushed on `B` input :class:`queue.Queue`, and will be consumed by `B`'s thread. Meanwhile, `A` +will continue to run, if it's not done. -When there is more than one node linked as the output of a node (for example, with `B`, `C`, and `D`) , the same thing +When there is more than one node linked as the output of a node (for example, with `B`, `C`, and `D`), the same thing happens except that each result coming out of `B` will be sent to both on `C` and `D` input :class:`queue.Queue`. One thing to keep in mind here is that as the objects are passed from thread to thread, you need to write "pure" transformations (see :doc:`/guide/purity`). You generally don't have to think about it. Just be aware that your nodes will run in parallel, and don't worry -too much about blocking nodes, as they won't block other nodes. +too much about nodes running blocking operations, as they will run in parallel. As soon as a line of output is ready, +the next nodes will start consuming it. That being said, let's manipulate some files. @@ -52,18 +54,33 @@ We'll use a text file that was generated using Bonobo from the "liste-des-cafes- Mairie de Paris under the Open Database License (ODbL). You can `explore the original dataset `_. -You'll need the `example dataset `_, -available in **Bonobo**'s repository. +You'll need the `"coffeeshops.txt" example dataset `_, +available in **Bonobo**'s repository: + +.. code-block:: shell-session + + $ curl https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/datasets/coffeeshops.txt > `python -c 'import bonobo; print(bonobo.get_examples_path("datasets/coffeeshops.txt"))'` + +.. note:: + + The "example dataset download" step will be easier in the future. + + https://github.com/python-bonobo/bonobo/issues/134 .. literalinclude:: ../../bonobo/examples/tutorials/tut02e01_read.py :language: python -You can run this example as a module: +You can also run this example as a module (but you'll still need the dataset...): .. code-block:: shell-session $ bonobo run -m bonobo.examples.tutorials.tut02e01_read +.. note:: + + Don't focus too much on the `get_services()` function for now. It is required, with this exact name, but we'll get + into that in a few minutes. + Writing to files :::::::::::::::: diff --git a/docs/tutorial/tut03.rst b/docs/tutorial/tut03.rst index 2721430..325bc9d 100644 --- a/docs/tutorial/tut03.rst +++ b/docs/tutorial/tut03.rst @@ -1,9 +1,195 @@ Configurables and Services ========================== -This document does not exist yet, but will be available soon. +.. note:: -Meanwhile, you can read the matching references: + This section lacks completeness, sorry for that (but you can still read it!). + +In the last section, we used a few new tools. + +Class-based transformations and configurables +::::::::::::::::::::::::::::::::::::::::::::: + +Bonobo is a bit dumb. If something is callable, it considers it can be used as a transformation, and it's up to the +user to provide callables that logically fits in a graph. + +You can use plain python objects with a `__call__()` method, and it ill just work. + +As a lot of transformations needs common machinery, there is a few tools to quickly build transformations, most of +them requiring your class to subclass :class:`bonobo.config.Configurable`. + +Configurables allows to use the following features: + +* You can add **Options** (using the :class:`bonobo.config.Option` descriptor). Options can be positional, or keyword + based, can have a default value and will be consumed from the constructor arguments. + + .. code-block:: python + + from bonobo.config import Configurable, Option + + class PrefixIt(Configurable): + prefix = Option(str, positional=True, default='>>>') + + def call(self, row): + return self.prefix + ' ' + row + + prefixer = PrefixIt('$') + +* You can add **Services** (using the :class:`bonobo.config.Service` descriptor). Services are a subclass of + :class:`bonobo.config.Option`, sharing the same basics, but specialized in the definition of "named services" that + will be resolved at runtime (a.k.a for which we will provide an implementation at runtime). We'll dive more into that + in the next section + + .. code-block:: python + + from bonobo.config import Configurable, Option, Service + + class HttpGet(Configurable): + url = Option(default='https://jsonplaceholder.typicode.com/users') + http = Service('http.client') + + def call(self, http): + resp = http.get(self.url) + + for row in resp.json(): + yield row + + http_get = HttpGet() + + +* You can add **Methods** (using the :class:`bonobo.config.Method` descriptor). :class:`bonobo.config.Method` is a + subclass of :class:`bonobo.config.Option` that allows to pass callable parameters, either to the class constructor, + or using the class as a decorator. + + .. code-block:: python + + from bonobo.config import Configurable, Method + + class Applier(Configurable): + apply = Method() + + def call(self, row): + return self.apply(row) + + @Applier + def Prefixer(self, row): + return 'Hello, ' + row + + prefixer = Prefixer() + +* You can add **ContextProcessors**, which are an advanced feature we won't introduce here. If you're familiar with + pytest, you can think of them as pytest fixtures, execution wise. + +Services +:::::::: + +The motivation behind services is mostly separation of concerns, testability and deployability. + +Usually, your transformations will depend on services (like a filesystem, an http client, a database, a rest api, ...). +Those services can very well be hardcoded in the transformations, but there is two main drawbacks: + +* You won't be able to change the implementation depending on the current environment (development laptop versus + production servers, bug-hunting session versus execution, etc.) +* You won't be able to test your transformations without testing the associated services. + +To overcome those caveats of hardcoding things, we define Services in the configurable, which are basically +string-options of the service names, and we provide an implementation at the last moment possible. + +There are two ways of providing implementations: + +* Either file-wide, by providing a `get_services()` function that returns a dict of named implementations (we did so + with filesystems in the previous step, :doc:`tut02.rst`) +* Either directory-wide, by providing a `get_services()` function in a specially named `_services.py` file. + +The first is simpler if you only have one transformation graph in one file, the second allows to group coherent +transformations together in a directory and share the implementations. + +Let's see how to use it, starting from the previous service example: + +.. code-block:: python + + from bonobo.config import Configurable, Option, Service + + class HttpGet(Configurable): + url = Option(default='https://jsonplaceholder.typicode.com/users') + http = Service('http.client') + + def call(self, http): + resp = http.get(self.url) + + for row in resp.json(): + yield row + +We defined an "http.client" service, that obviously should have a `get()` method, returning responses that have a +`json()` method. + +Let's provide two implementations for that. The first one will be using `requests `_, +that coincidally satisfies the described interface: + +.. code-block:: python + + import bonobo + import requests + + def get_services(): + return { + 'http.client': requests + } + + graph = bonobo.Graph( + HttpGet(), + print, + ) + +If you run this code, you should see some mock data returned by the webservice we called (assuming it's up and you can +reach it). + +Now, the second implementation will replace that with a mock, used for testing purposes: + +.. code-block:: python + + class HttpResponseStub: + def json(self): + return [ + {'id': 1, 'name': 'Leanne Graham', 'username': 'Bret', 'email': 'Sincere@april.biz', 'address': {'street': 'Kulas Light', 'suite': 'Apt. 556', 'city': 'Gwenborough', 'zipcode': '92998-3874', 'geo': {'lat': '-37.3159', 'lng': '81.1496'}}, 'phone': '1-770-736-8031 x56442', 'website': 'hildegard.org', 'company': {'name': 'Romaguera-Crona', 'catchPhrase': 'Multi-layered client-server neural-net', 'bs': 'harness real-time e-markets'}}, + {'id': 2, 'name': 'Ervin Howell', 'username': 'Antonette', 'email': 'Shanna@melissa.tv', 'address': {'street': 'Victor Plains', 'suite': 'Suite 879', 'city': 'Wisokyburgh', 'zipcode': '90566-7771', 'geo': {'lat': '-43.9509', 'lng': '-34.4618'}}, 'phone': '010-692-6593 x09125', 'website': 'anastasia.net', 'company': {'name': 'Deckow-Crist', 'catchPhrase': 'Proactive didactic contingency', 'bs': 'synergize scalable supply-chains'}}, + ] + + class HttpStub: + def get(self, url): + return HttpResponseStub() + + def get_services(): + return { + 'http.client': HttpStub() + } + + graph = bonobo.Graph( + HttpGet(), + print, + ) + +The `Graph` definition staying the exact same, you can easily substitute the `_services.py` file depending on your +environment (the way you're doing this is out of bonobo scope and heavily depends on your usual way of managing +configuration files on different platforms). + +Starting with bonobo 0.5 (not yet released), you will be able to use service injections with function-based +transformations too, using the `bonobo.config.requires` decorator to mark a dependency. + +.. code-block:: python + + from bonobo.config import requires + + @requires('http.client') + def http_get(http): + resp = http.get('https://jsonplaceholder.typicode.com/users') + + for row in resp.json(): + yield row + + +Read more +::::::::: * :doc:`/guide/services` * :doc:`/reference/api_config` From 5d59a72310e37ad5d691fa9896567a107c247784 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 6 Jul 2017 11:29:55 +0200 Subject: [PATCH 120/143] [core] Adds a .copy() method to graph structure. --- bonobo/structs/graphs.py | 11 +++++++++++ tests/structs/test_graphs.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/bonobo/structs/graphs.py b/bonobo/structs/graphs.py index ccafb6b..fe7c1df 100644 --- a/bonobo/structs/graphs.py +++ b/bonobo/structs/graphs.py @@ -1,3 +1,5 @@ +from copy import copy + from bonobo.constants import BEGIN @@ -62,6 +64,15 @@ class Graph: return self + def copy(self): + g = Graph() + + g.edges = copy(self.edges) + g.named = copy(self.named) + g.nodes = copy(self.nodes) + + return g + @property def topologically_sorted_indexes(self): """Iterate in topological order, based on networkx's topological_sort() function. diff --git a/tests/structs/test_graphs.py b/tests/structs/test_graphs.py index af1a6df..7f3a58d 100644 --- a/tests/structs/test_graphs.py +++ b/tests/structs/test_graphs.py @@ -71,3 +71,23 @@ def test_graph_topological_sort(): assert g.topologically_sorted_indexes.index(3) < g.topologically_sorted_indexes.index(4) assert g[3] == sentinel.b1 assert g[4] == sentinel.b2 + + +def test_copy(): + g1 = Graph() + g2 = g1.copy() + + assert g1 is not g2 + + assert len(g1) == 0 + assert len(g2) == 0 + + g1.add_chain([]) + + assert len(g1) == 1 + assert len(g2) == 0 + + g2.add_chain([], identity) + + assert len(g1) == 1 + assert len(g2) == 2 From 0f23f1a940e280c30e6d988d9f107795840bff60 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 6 Jul 2017 12:40:55 +0200 Subject: [PATCH 121/143] [docs] First draft of sqlalchemy tutorial. --- docs/install.rst | 2 +- docs/tutorial/tut04.rst | 197 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 4 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index ac951e5..87df3d3 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -13,7 +13,7 @@ Creating a project and starting to write code should take less than a minute: $ bonobo run my-etl-project Once you bootstrapped a project, you can start editing the default example transformation by editing -`my-etl-project/main.py`. +`my-etl-project/main.py`. Now, you can head to :doc:`tutorial/index`. Other installation options :::::::::::::::::::::::::: diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst index 14888d5..69e1846 100644 --- a/docs/tutorial/tut04.rst +++ b/docs/tutorial/tut04.rst @@ -1,8 +1,199 @@ Working with databases ====================== -This document does not exist yet, but will be available soon. +Databases (and especially SQL databases here) are not the focus of Bonobo, thus support for it is not (and will never +be) included in the main package. Instead, working with databases is done using third party, well maintained and +specialized packages, like SQLAlchemy, or other database access libraries from the python cheese shop. -Meanwhile, you can jump to bonobo-sqlalchemy development repository: +.. note:: + + SQLAlchemy extension is not yet complete. Things may be not optimal, and some APIs will change. You can still try, + of course. + + Consider the following document as a "preview" (yes, it should work, yes it may break in the future). + + Also, note that for early development stages, we explicitely support only PostreSQL, although it may work well + with `any other database supported by SQLAlchemy `_. + +First, read https://www.bonobo-project.org/with/sqlalchemy for instructions on how to install. You **do need** the +bleeding edge version of `bonobo` and `bonobo-sqlalchemy` to make this work. + +Additional requirements +::::::::::::::::::::::: + +Once you installed `bonobo_sqlalchemy` (read https://www.bonobo-project.org/with/sqlalchemy to use bleeding edge +version), install the following additional packages: + +.. code-block:: shell-session + + $ pip install -U python-dotenv psycopg2 awesome-slugify + +Those packages are not required by the extension, but `python-dotenv` will help us configure the database DSN, and +`psycopg2` is required by SQLAlchemy to connect to PostgreSQL databases. Also, we'll use a slugifier to create unique +identifiers for the database (maybe not what you'd do in the real world, but very much sufficient for example purpose). + +Configure a database engine +::::::::::::::::::::::::::: + +Open your `_services.py` file and replace the code: + +.. code-block:: python + + import bonobo + import dotenv + + from bonobo_sqlalchemy.util import create_postgresql_engine + + dotenv.load_dotenv(dotenv.find_dotenv()) + + def get_services(): + return { + 'fs': bonobo.open_fs(), + 'db': create_postgresql_engine(name='tutorial') + } + +The `create_postgresql_engine` is a tiny function building the DSN from reasonable defaults, that you can override +either by providing kwargs, or with system environment variables. If you want to override something, open the `.env` +file and add values for one or more of `POSTGRES_NAME`, `POSTGRES_USER`, 'POSTGRES_PASS`, `POSTGRES_HOST`, +`POSTGRES_PORT`. Please note that kwargs always have precedence on environment, but that you should prefer using +environment variables for anything that is not immutable from one platform to another. + +Let's create a `tutorial/pgdb.py` job: + +.. code-block:: python + + import bonobo + import bonobo_sqlalchemy + + from bonobo.examples.tutorials.tut02e03_writeasmap import graph, split_one_to_map + + graph = graph.copy() + graph.add_chain( + bonobo_sqlalchemy.InsertOrUpdate('coffeeshops'), + _input=split_one_to_map + ) + +Notes here: + +* We use the code from :doc:`tut02`, which is bundled with bonobo in the `bonobo.examples.tutorials` package. +* We "fork" the graph, by creating a copy and appending a new "chain", starting at a point that exists in the other + graph. +* We use :class:`bonobo_sqlalchemy.InsertOrUpdate` (which role, in case it is not obvious, is to create database rows if + they do not exist yet, or update the existing row, based on a "discriminant" criteria (by default, "id")). + +If we run this transformation (with `bonobo run tutorial/pgdb.py`), we should get an error: + +.. code-block:: text + + | File ".../lib/python3.6/site-packages/psycopg2/__init__.py", line 130, in connect + | conn = _connect(dsn, connection_factory=connection_factory, **kwasync) + | sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) FATAL: database "tutorial" does not exist + | + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File ".../bonobo-devkit/bonobo/bonobo/strategies/executor.py", line 45, in _runner + | node_context.start() + | File ".../bonobo-devkit/bonobo/bonobo/execution/base.py", line 75, in start + | self._stack.setup(self) + | File ".../bonobo-devkit/bonobo/bonobo/config/processors.py", line 94, in setup + | _append_to_context = next(_processed) + | File ".../bonobo-devkit/bonobo-sqlalchemy/bonobo_sqlalchemy/writers.py", line 43, in create_connection + | raise UnrecoverableError('Could not create SQLAlchemy connection: {}.'.format(str(exc).replace('\n', ''))) from exc + | bonobo.errors.UnrecoverableError: Could not create SQLAlchemy connection: (psycopg2.OperationalError) FATAL: database "tutorial" does not exist. + +The database we requested do not exist. It is not the role of bonobo to do database administration, and thus there is +no tool here to create neither the database, nor the tables we want to use. + +There are however tools in `sqlalchemy` to manage tables, so we'll create the database by ourselves, and ask sqlalchemy +to create the table: + +.. code-block:: shell-session + + $ psql -U postgres -h localhost + + psql (9.6.1, server 9.6.3) + Type "help" for help. + + postgres=# CREATE ROLE tutorial WITH LOGIN PASSWORD 'tutorial'; + CREATE ROLE + postgres=# CREATE DATABASE tutorial WITH OWNER=tutorial TEMPLATE=template0 ENCODING='utf-8'; + CREATE DATABASE + +Now, let's use a little trick and add this section to `pgdb.py`: + +.. code-block:: python + + import logging, sys + + from bonobo.commands.run import get_default_services + from sqlalchemy import Table, Column, String, Integer, MetaData + + def main(): + services = get_default_services(__file__) + + if len(sys.argv) == 2 and sys.argv[1] == 'reset': + engine = services.get('sqlalchemy.engine') + metadata = MetaData() + + coffee_table = Table( + 'coffeeshops', + metadata, + Column('id', String(255), primary_key=True), + Column('name', String(255)), + Column('address', String(255)), + ) + + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + metadata.drop_all(engine) + metadata.create_all(engine) + else: + return bonobo.run(graph, services=services) + + if __name__ == '__main__': + main() + +.. note:: + + We're using private API of bonobo here, which is unsatisfactory, discouraged and may change. Some way to get the + service dictionnary will be added to the public api in a future release of bonobo. + +Now run: + +.. code-block:: python + + $ python tutorial/pgdb.py reset + +Database and table should now exist. + +Let's prepare our data for database, and change the `.add_chain(..)` call to do it prior to `InsertOrUpdate(...)` + +.. code-block:: python + + from slugify import slugify_url + + def format_for_db(row): + name, address = list(row.items())[0] + return { + 'id': slugify_url(name), + 'name': name, + 'address': address, + } + + # ... + + graph = graph.copy() + graph.add_chain( + format_for_db, + bonobo_sqlalchemy.InsertOrUpdate('coffeeshops'), + _input=split_one_to_map + ) + +You can now run the script (either with `bonobo run tutorial/pgdb.py` or directly with the python interpreter, as we +added a "main" section) and the dataset should be inserted in your database. If you run it again, no new rows are +created. + +Note that as we forked the graph from :doc:`tut02`, the transformation also writes the data to `coffeeshops.json`, as +before. -* https://github.com/hartym/bonobo-sqlalchemy From 71386ea30c54119f63dbb700698408d7fec3c2ee Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 6 Jul 2017 12:46:19 +0200 Subject: [PATCH 122/143] [doc] sqla: move logger usage to service, fix service name. --- docs/tutorial/tut04.rst | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst index 69e1846..6cd7675 100644 --- a/docs/tutorial/tut04.rst +++ b/docs/tutorial/tut04.rst @@ -39,17 +39,21 @@ Open your `_services.py` file and replace the code: .. code-block:: python - import bonobo - import dotenv - + import bonobo, dotenv, logging, os from bonobo_sqlalchemy.util import create_postgresql_engine dotenv.load_dotenv(dotenv.find_dotenv()) + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) def get_services(): return { - 'fs': bonobo.open_fs(), - 'db': create_postgresql_engine(name='tutorial') + 'fs': bonobo.open_examples_fs('datasets'), + 'fs.output': bonobo.open_fs(), + 'sqlalchemy.engine': create_postgresql_engine(**{ + 'name': 'tutorial', + 'user': 'tutorial', + 'pass': 'tutorial', + }) } The `create_postgresql_engine` is a tiny function building the DSN from reasonable defaults, that you can override @@ -125,15 +129,15 @@ Now, let's use a little trick and add this section to `pgdb.py`: .. code-block:: python - import logging, sys - - from bonobo.commands.run import get_default_services + import sys from sqlalchemy import Table, Column, String, Integer, MetaData def main(): + from bonobo.commands.run import get_default_services services = get_default_services(__file__) - - if len(sys.argv) == 2 and sys.argv[1] == 'reset': + if len(sys.argv) == 1: + return bonobo.run(graph, services=services) + elif len(sys.argv) == 2 and sys.argv[1] == 'reset': engine = services.get('sqlalchemy.engine') metadata = MetaData() @@ -145,11 +149,10 @@ Now, let's use a little trick and add this section to `pgdb.py`: Column('address', String(255)), ) - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) metadata.drop_all(engine) metadata.create_all(engine) else: - return bonobo.run(graph, services=services) + raise NotImplementedError('I do not understand.') if __name__ == '__main__': main() From a1074341394f5f208611d1bb0585dfe3cb695633 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 6 Jul 2017 12:46:19 +0200 Subject: [PATCH 123/143] [doc] sqla: move logger usage to service, fix service name. --- docs/tutorial/tut04.rst | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst index 69e1846..6cd7675 100644 --- a/docs/tutorial/tut04.rst +++ b/docs/tutorial/tut04.rst @@ -39,17 +39,21 @@ Open your `_services.py` file and replace the code: .. code-block:: python - import bonobo - import dotenv - + import bonobo, dotenv, logging, os from bonobo_sqlalchemy.util import create_postgresql_engine dotenv.load_dotenv(dotenv.find_dotenv()) + logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) def get_services(): return { - 'fs': bonobo.open_fs(), - 'db': create_postgresql_engine(name='tutorial') + 'fs': bonobo.open_examples_fs('datasets'), + 'fs.output': bonobo.open_fs(), + 'sqlalchemy.engine': create_postgresql_engine(**{ + 'name': 'tutorial', + 'user': 'tutorial', + 'pass': 'tutorial', + }) } The `create_postgresql_engine` is a tiny function building the DSN from reasonable defaults, that you can override @@ -125,15 +129,15 @@ Now, let's use a little trick and add this section to `pgdb.py`: .. code-block:: python - import logging, sys - - from bonobo.commands.run import get_default_services + import sys from sqlalchemy import Table, Column, String, Integer, MetaData def main(): + from bonobo.commands.run import get_default_services services = get_default_services(__file__) - - if len(sys.argv) == 2 and sys.argv[1] == 'reset': + if len(sys.argv) == 1: + return bonobo.run(graph, services=services) + elif len(sys.argv) == 2 and sys.argv[1] == 'reset': engine = services.get('sqlalchemy.engine') metadata = MetaData() @@ -145,11 +149,10 @@ Now, let's use a little trick and add this section to `pgdb.py`: Column('address', String(255)), ) - logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) metadata.drop_all(engine) metadata.create_all(engine) else: - return bonobo.run(graph, services=services) + raise NotImplementedError('I do not understand.') if __name__ == '__main__': main() From 7f30df93c38ded267f3923799f7a50aaeebc3bca Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Thu, 6 Jul 2017 12:52:19 +0200 Subject: [PATCH 124/143] [doc] sqla tutorial: adds some titles. --- docs/tutorial/tut04.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/tut04.rst b/docs/tutorial/tut04.rst index 6cd7675..2a1ef71 100644 --- a/docs/tutorial/tut04.rst +++ b/docs/tutorial/tut04.rst @@ -18,8 +18,8 @@ specialized packages, like SQLAlchemy, or other database access libraries from t First, read https://www.bonobo-project.org/with/sqlalchemy for instructions on how to install. You **do need** the bleeding edge version of `bonobo` and `bonobo-sqlalchemy` to make this work. -Additional requirements -::::::::::::::::::::::: +Requirements +:::::::::::: Once you installed `bonobo_sqlalchemy` (read https://www.bonobo-project.org/with/sqlalchemy to use bleeding edge version), install the following additional packages: @@ -62,6 +62,9 @@ file and add values for one or more of `POSTGRES_NAME`, `POSTGRES_USER`, 'POSTGR `POSTGRES_PORT`. Please note that kwargs always have precedence on environment, but that you should prefer using environment variables for anything that is not immutable from one platform to another. +Add database operation to the graph +::::::::::::::::::::::::::::::::::: + Let's create a `tutorial/pgdb.py` job: .. code-block:: python @@ -110,6 +113,9 @@ If we run this transformation (with `bonobo run tutorial/pgdb.py`), we should ge The database we requested do not exist. It is not the role of bonobo to do database administration, and thus there is no tool here to create neither the database, nor the tables we want to use. +Create database and table +::::::::::::::::::::::::: + There are however tools in `sqlalchemy` to manage tables, so we'll create the database by ourselves, and ask sqlalchemy to create the table: @@ -170,6 +176,9 @@ Now run: Database and table should now exist. +Format the data +::::::::::::::: + Let's prepare our data for database, and change the `.add_chain(..)` call to do it prior to `InsertOrUpdate(...)` .. code-block:: python @@ -193,6 +202,9 @@ Let's prepare our data for database, and change the `.add_chain(..)` call to do _input=split_one_to_map ) +Run! +:::: + You can now run the script (either with `bonobo run tutorial/pgdb.py` or directly with the python interpreter, as we added a "main" section) and the dataset should be inserted in your database. If you run it again, no new rows are created. From 53d6ac5887d70b74a726f10e808941ad1d151c45 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Tue, 11 Jul 2017 16:25:32 +0200 Subject: [PATCH 125/143] [nodes] Adds arg0_to_kwargs and kwargs_to_arg0 transformations. --- bonobo/nodes/basics.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 164eeb1..c0434ed 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -10,11 +10,13 @@ from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL __all__ = [ - 'identity', 'Limit', - 'Tee', - 'count', 'PrettyPrinter', + 'Tee', + 'arg0_to_kwargs', + 'count', + 'identity', + 'kwargs_to_arg0', 'noop', ] @@ -86,3 +88,11 @@ class PrettyPrinter(Configurable): def noop(*args, **kwargs): # pylint: disable=unused-argument from bonobo.constants import NOT_MODIFIED return NOT_MODIFIED + + +def arg0_to_kwargs(row): + return Bag(**row) + + +def kwargs_to_arg0(**row): + return Bag(row) From f2a9a45fd134715c929dc69d5ec25f77152768a3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 15 Jul 2017 10:14:30 +0200 Subject: [PATCH 126/143] [nodes] Adds arg0_to_kwargs and kwargs_to_arg0 transformations. --- bonobo/_api.py | 6 ++++-- bonobo/nodes/basics.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index ab890c6..6b2a72d 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,6 +1,6 @@ from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop + PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop, arg0_to_kwargs, kwargs_to_arg0 from bonobo.strategies import create_strategy from bonobo.util.objects import get_name @@ -101,13 +101,15 @@ register_api_group( JsonReader, JsonWriter, Limit, - PrettyPrinter, PickleReader, PickleWriter, + PrettyPrinter, RateLimited, Tee, + arg0_to_kwargs, count, identity, + kwargs_to_arg0, noop, ) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index c0434ed..ea05c29 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -91,8 +91,22 @@ def noop(*args, **kwargs): # pylint: disable=unused-argument def arg0_to_kwargs(row): + """ + Transform items in a stream from "arg0" format (each call only has one positional argument, which is a dict-like + object) to "kwargs" format (each call only has keyword arguments that represent a row). + + :param row: + :return: bonobo.Bag + """ return Bag(**row) def kwargs_to_arg0(**row): + """ + Transform items in a stream from "kwargs" format (each call only has keyword arguments that represent a row) to + "arg0" format (each call only has one positional argument, which is a dict-like object) . + + :param **row: + :return: bonobo.Bag + """ return Bag(row) From fbd0ee9862332f070282ef6c8f682a99ca8638dd Mon Sep 17 00:00:00 2001 From: Parthiv20 Date: Sat, 15 Jul 2017 10:34:30 +0200 Subject: [PATCH 127/143] Update tut02.rst --- docs/tutorial/tut02.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/tut02.rst b/docs/tutorial/tut02.rst index b1545f9..9ad5ae3 100644 --- a/docs/tutorial/tut02.rst +++ b/docs/tutorial/tut02.rst @@ -59,7 +59,7 @@ available in **Bonobo**'s repository: .. code-block:: shell-session - $ curl https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/datasets/coffeeshops.txt > `python -c 'import bonobo; print(bonobo.get_examples_path("datasets/coffeeshops.txt"))'` + $ curl https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/datasets/coffeeshops.txt > `python3 -c 'import bonobo; print(bonobo.get_examples_path("datasets/coffeeshops.txt"))'` .. note:: From 0ac561a474629d57f73d1cf060e0e9c286462f09 Mon Sep 17 00:00:00 2001 From: Vitalii Vokhmin Date: Sat, 15 Jul 2017 11:43:57 +0200 Subject: [PATCH 128/143] Fix #113. Add flush() method to IOBuffer --- bonobo/ext/console.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index f30fae0..edc6436 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -23,6 +23,9 @@ class IOBuffer(): finally: previous.close() + def flush(self): + self.current.flush() + class ConsoleOutputPlugin(Plugin): """ From f4a018bfe22e12c02573c56fe927961ab79a5404 Mon Sep 17 00:00:00 2001 From: Alex Vykaliuk Date: Sat, 15 Jul 2017 12:07:08 +0200 Subject: [PATCH 129/143] Do not fail in ipykernel without ipywidgets. --- bonobo/_api.py | 13 ++++++++++--- bonobo/ext/jupyter/plugin.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index ab890c6..37ff996 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -1,3 +1,5 @@ +import logging + from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop @@ -52,9 +54,14 @@ def run(graph, strategy=None, plugins=None, services=None): plugins.append(ConsoleOutputPlugin) if _is_jupyter_notebook(): - from bonobo.ext.jupyter import JupyterOutputPlugin - if JupyterOutputPlugin not in plugins: - plugins.append(JupyterOutputPlugin) + try: + from bonobo.ext.jupyter import JupyterOutputPlugin + except ImportError: + logging.warning( + 'Failed to load jupyter widget. Easiest way is to install the optional "jupyter" ' 'dependencies with «pip install bonobo[jupyter]», but you can also install a specific ' 'version by yourself.') + else: + if JupyterOutputPlugin not in plugins: + plugins.append(JupyterOutputPlugin) return strategy.execute(graph, plugins=plugins, services=services) diff --git a/bonobo/ext/jupyter/plugin.py b/bonobo/ext/jupyter/plugin.py index a72141c..715b057 100644 --- a/bonobo/ext/jupyter/plugin.py +++ b/bonobo/ext/jupyter/plugin.py @@ -1,11 +1,11 @@ +import logging + from bonobo.ext.jupyter.widget import BonoboWidget from bonobo.plugins import Plugin try: import IPython.core.display except ImportError as e: - import logging - logging.exception( 'You must install Jupyter to use the bonobo Jupyter extension. Easiest way is to install the ' 'optional "jupyter" dependencies with «pip install bonobo[jupyter]», but you can also install a ' From 8f184ddbb09b93812bca297882350cc7166a28ee Mon Sep 17 00:00:00 2001 From: Vitalii Vokhmin Date: Sat, 15 Jul 2017 12:39:24 +0200 Subject: [PATCH 130/143] Add a link to Contributing guide in README --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6c56328..588ed8e 100644 --- a/README.rst +++ b/README.rst @@ -55,6 +55,8 @@ Homepage: https://www.bonobo-project.org/ (`Roadmap `_ and `contributors `_. - + .. image:: https://img.shields.io/pypi/l/bonobo.svg :target: https://pypi.python.org/pypi/bonobo :alt: License From 9c988027638205f6edbefd4d9dd3c9ebc0b7265a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 15 Jul 2017 12:46:51 +0200 Subject: [PATCH 131/143] [misc] ordering of imports --- bonobo/_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index 70090b7..26df660 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -2,7 +2,7 @@ import logging from bonobo.structs import Bag, Graph, Token from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ - PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop, arg0_to_kwargs, kwargs_to_arg0 + PickleReader, PickleWriter, PrettyPrinter, RateLimited, Tee, arg0_to_kwargs, count, identity, kwargs_to_arg0, noop from bonobo.strategies import create_strategy from bonobo.util.objects import get_name From d13b8b28e572e6f4ac175151de44dd6c9eba6f63 Mon Sep 17 00:00:00 2001 From: Alex Vykaliuk Date: Sat, 15 Jul 2017 12:52:58 +0200 Subject: [PATCH 132/143] Add ability to install requirements with for a requirements.txt residing in the same dir --- bonobo/commands/run.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 6de6bf6..fb93e77 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -26,6 +26,20 @@ def get_default_services(filename, services=None): return services or {} +def _install_requirements(requirements): + """Install requirements given a path to requirements.txt file.""" + import importlib + import pip + + pip.main(['install', '-r', requirements]) + # Some shenanigans to be sure everything is importable after this, especially .egg-link files which + # are referenced in *.pth files and apparently loaded by site.py at some magic bootstrap moment of the + # python interpreter. + pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources) + import site + importlib.reload(site) + + def execute(filename, module, install=False, quiet=False, verbose=False): import runpy from bonobo import Graph, run, settings @@ -39,16 +53,8 @@ def execute(filename, module, install=False, quiet=False, verbose=False): if filename: if os.path.isdir(filename): if install: - import importlib - import pip requirements = os.path.join(filename, 'requirements.txt') - pip.main(['install', '-r', requirements]) - # Some shenanigans to be sure everything is importable after this, especially .egg-link files which - # are referenced in *.pth files and apparently loaded by site.py at some magic bootstrap moment of the - # python interpreter. - pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources) - import site - importlib.reload(site) + _install_requirements(requirements) pathname = filename for filename in DEFAULT_GRAPH_FILENAMES: @@ -58,7 +64,8 @@ def execute(filename, module, install=False, quiet=False, verbose=False): if not os.path.exists(filename): raise IOError('Could not find entrypoint (candidates: {}).'.format(', '.join(DEFAULT_GRAPH_FILENAMES))) elif install: - raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).') + requirements = os.path.join(os.path.dirname(filename), 'requirements.txt') + _install_requirements(requirements) context = runpy.run_path(filename, run_name='__bonobo__') elif module: context = runpy.run_module(module, run_name='__bonobo__') From 75c15ae1f8c2213da29ca2186d1a95fc0ddc03d3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 15 Jul 2017 13:56:51 +0200 Subject: [PATCH 133/143] [tests] Adds runners descriptions. --- tests/test_commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 280308d..593cfd6 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -10,10 +10,12 @@ from bonobo.commands import entrypoint def runner_entrypoint(*args): + """ Run bonobo using the python command entrypoint directly (bonobo.commands.entrypoint). """ return entrypoint(list(args)) def runner_module(*args): + """ Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ...".""" with patch.object(sys, 'argv', ['bonobo', *args]): return runpy.run_path(__main__.__file__, run_name='__main__') From 7aee728b8dd14aeed9f38866e529744017d9440e Mon Sep 17 00:00:00 2001 From: Alex Vykaliuk Date: Sat, 15 Jul 2017 14:24:44 +0200 Subject: [PATCH 134/143] Add tests for --install of run command --- tests/test_commands.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 280308d..df59115 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,3 +1,4 @@ +import os import runpy import sys from unittest.mock import patch @@ -70,6 +71,24 @@ def test_run_path(runner, capsys): assert out[2].startswith('Baz ') +@all_runners +def test_install_requirements_for_dir(runner): + dirname = get_examples_path('types') + with patch('pip.main') as pip_mock: + runner('run', '--install', dirname) + pip_mock.assert_called_once_with( + ['install', '-r', os.path.join(dirname, 'requirements.txt')]) + + +@all_runners +def test_install_requirements_for_file(runner): + dirname = get_examples_path('types') + with patch('pip.main') as pip_mock: + runner('run', '--install', os.path.join(dirname, 'strings.py')) + pip_mock.assert_called_once_with( + ['install', '-r', os.path.join(dirname, 'requirements.txt')]) + + @all_runners def test_version(runner, capsys): runner('version') From a8ed0e432220ae4e8b35653f23a69ba816da5180 Mon Sep 17 00:00:00 2001 From: Alex Vykaliuk Date: Sat, 15 Jul 2017 14:52:22 +0200 Subject: [PATCH 135/143] Move patch one level up because importlib brakes all the CI tools. --- tests/test_commands.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index df59115..e2d7b7b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -74,19 +74,19 @@ def test_run_path(runner, capsys): @all_runners def test_install_requirements_for_dir(runner): dirname = get_examples_path('types') - with patch('pip.main') as pip_mock: + with patch('bonobo.commands.run._install_requirements') as install_mock: runner('run', '--install', dirname) - pip_mock.assert_called_once_with( - ['install', '-r', os.path.join(dirname, 'requirements.txt')]) + install_mock.assert_called_once_with( + os.path.join(dirname, 'requirements.txt')) @all_runners def test_install_requirements_for_file(runner): dirname = get_examples_path('types') - with patch('pip.main') as pip_mock: + with patch('bonobo.commands.run._install_requirements') as install_mock: runner('run', '--install', os.path.join(dirname, 'strings.py')) - pip_mock.assert_called_once_with( - ['install', '-r', os.path.join(dirname, 'requirements.txt')]) + install_mock.assert_called_once_with( + os.path.join(dirname, 'requirements.txt')) @all_runners From 575462ca4cf9f5345939026ce5571bdc7e8277ad Mon Sep 17 00:00:00 2001 From: Vitalii Vokhmin Date: Sat, 15 Jul 2017 15:35:01 +0200 Subject: [PATCH 136/143] Check if PluginExecutionContext was started before shutting it down. If a `PluginExecutionContext().shutdown()` is called _before_ `PluginExecutionContext().start()` was called, this leads to an `AttributeError` exception since finalizer tries to access to attributes which were never defined. --- bonobo/execution/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bonobo/execution/plugin.py b/bonobo/execution/plugin.py index d928f4a..a207f23 100644 --- a/bonobo/execution/plugin.py +++ b/bonobo/execution/plugin.py @@ -16,8 +16,9 @@ class PluginExecutionContext(LoopingExecutionContext): self.wrapped.initialize() def shutdown(self): - with recoverable(self.handle_error): - self.wrapped.finalize() + if self.started: + with recoverable(self.handle_error): + self.wrapped.finalize() self.alive = False def step(self): From abde68108b9beaec07826afb5295e5b0a52c90d5 Mon Sep 17 00:00:00 2001 From: Parthiv20 Date: Sat, 15 Jul 2017 17:27:34 +0200 Subject: [PATCH 137/143] better windows console output --- bonobo/_api.py | 14 ++++++++------ bonobo/ext/console.py | 20 +++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/bonobo/_api.py b/bonobo/_api.py index ab890c6..adeaf8b 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -4,6 +4,8 @@ from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, J from bonobo.strategies import create_strategy from bonobo.util.objects import get_name + + __all__ = [] @@ -21,17 +23,17 @@ def register_api_group(*args): def run(graph, strategy=None, plugins=None, services=None): """ Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it. - + The only necessary argument is a :class:`Graph` instance, containing the logic you actually want to execute. - + By default, this graph will be executed using the "threadpool" strategy: each graph node will be wrapped in a thread, and executed in a loop until there is no more input to this node. - + You can provide plugins factory objects in the plugins list, this function will add the necessary plugins for interactive console execution and jupyter notebook execution if it detects correctly that it runs in this context. - + You'll probably want to provide a services dictionary mapping service names to service instances. - + :param Graph graph: The :class:`Graph` to execute. :param str strategy: The :class:`bonobo.strategies.base.Strategy` to use. :param list plugins: The list of plugins to enhance execution. @@ -71,7 +73,7 @@ register_api(create_strategy) def open_fs(fs_url=None, *args, **kwargs): """ Wraps :func:`fs.open_fs` function with a few candies. - + :param str fs_url: A filesystem URL :param parse_result: A parsed filesystem URL. :type parse_result: :class:`ParseResult` diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index acf464b..146b991 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -2,7 +2,9 @@ import io import sys from contextlib import redirect_stdout -from colorama import Style, Fore +from colorama import Style, Fore, init +init(wrap=True) + from bonobo import settings from bonobo.plugins import Plugin @@ -23,7 +25,6 @@ class IOBuffer(): finally: previous.close() - class ConsoleOutputPlugin(Plugin): """ Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor @@ -43,11 +44,11 @@ class ConsoleOutputPlugin(Plugin): self._stdout = sys.stdout self.stdout = IOBuffer() - self.redirect_stdout = redirect_stdout(self.stdout) + self.redirect_stdout = redirect_stdout(self.stdout if sys.platform != 'win32' else self._stdout) self.redirect_stdout.__enter__() def run(self): - if self.isatty: + if self.isatty and sys.platform != 'win32': self._write(self.context.parent, rewind=True) else: pass # not a tty @@ -60,8 +61,13 @@ class ConsoleOutputPlugin(Plugin): t_cnt = len(context) buffered = self.stdout.switch() - for line in buffered.split('\n')[:-1]: - print(line + CLEAR_EOL, file=sys.stderr) + + if sys.platform == 'win32': + for line in buffered.split('\n')[:-1]: + print(line, file=sys.stderr) + else: + for line in buffered.split('\n')[:-1]: + print(line + CLEAR_EOL, file=sys.stderr) for i in context.graph.topologically_sorted_indexes: node = context[i] @@ -76,7 +82,7 @@ class ConsoleOutputPlugin(Plugin): else: _line = ''.join( ( - ' ', Fore.BLACK, '-', ' ', node.name, name_suffix, ' ', node.get_statistics_as_string(), + ' ', Style.BRIGHT+Fore.BLACK, '-', ' ', node.name, name_suffix, ' ', node.get_statistics_as_string(), Style.RESET_ALL, ' ', ) ) From 0bde35888e76389390bb6d705779bef65be2a11b Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 15 Jul 2017 17:49:22 +0200 Subject: [PATCH 138/143] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ed931de --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bonobo@rdc.li. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ From def0a525e7a1178c3fd70078402a085c11e7fe23 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sat, 15 Jul 2017 17:50:16 +0200 Subject: [PATCH 139/143] Create CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e88ab53 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +See http://docs.bonobo-project.org/en/latest/contribute/index.html From 937c61bd762d0e3828e735f591966017f015d27a Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 16 Jul 2017 10:20:25 +0200 Subject: [PATCH 140/143] Update dependencies --- Makefile | 2 +- requirements-dev.txt | 4 ++-- requirements-docker.txt | 4 ++-- requirements-jupyter.txt | 4 ++-- requirements.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 10094af..f5d0f8b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-07-04 10:50:55.775681 +# Updated at 2017-07-16 10:20:05.825842 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/requirements-dev.txt b/requirements-dev.txt index 55ba71c..69d64d8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,8 +21,8 @@ pygments==2.2.0 pytest-cov==2.5.1 pytest-sugar==0.8.0 pytest-timeout==1.2.0 -pytest==3.1.2 -python-dateutil==2.6.0 +pytest==3.1.3 +python-dateutil==2.6.1 pytz==2017.2 requests==2.18.1 six==1.10.0 diff --git a/requirements-docker.txt b/requirements-docker.txt index 0a39353..77ea242 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,6 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.9 +bonobo-docker==0.2.10 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 @@ -15,6 +15,6 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.18.1 six==1.10.0 -stevedore==1.23.0 +stevedore==1.24.0 urllib3==1.21.1 websocket-client==0.44.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index d6a6fdb..2542040 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,7 +1,7 @@ -e .[jupyter] appnope==0.1.0 bleach==2.0.0 -decorator==4.0.11 +decorator==4.1.1 entrypoints==0.2.3 html5lib==0.999999999 ipykernel==4.6.1 @@ -26,7 +26,7 @@ pickleshare==0.7.4 prompt-toolkit==1.0.14 ptyprocess==0.5.2 pygments==2.2.0 -python-dateutil==2.6.0 +python-dateutil==2.6.1 pyzmq==16.0.2 qtconsole==4.3.0 simplegeneric==0.8.1 diff --git a/requirements.txt b/requirements.txt index 093a6a1..5ddbb01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.18.1 six==1.10.0 -stevedore==1.23.0 +stevedore==1.24.0 urllib3==1.21.1 From 0e2e772043ed73a7a3079301cac912c7a7de1c0d Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 16 Jul 2017 10:50:49 +0200 Subject: [PATCH 141/143] release: 0.4.3 --- Makefile | 2 +- docs/changelog.rst | 7 +++++++ requirements-dev.txt | 4 ++-- requirements-docker.txt | 4 ++-- requirements-jupyter.txt | 4 ++-- requirements.txt | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 10094af..0c1f0b4 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file has been auto-generated. # All changes will be lost, see Projectfile. # -# Updated at 2017-07-04 10:50:55.775681 +# Updated at 2017-07-16 10:42:53.988109 PACKAGE ?= bonobo PYTHON ?= $(shell which python) diff --git a/docs/changelog.rst b/docs/changelog.rst index ebd2fac..2f12063 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog ========= +v.0.4.3 - 16 july 2017 +:::::::::::::::::::::: + +* #113 - Add flush() method to IOBuffer (Vitalii Vokhmin) +* Dependencies updated. +* Minor project artifacts updated. + v.0.4.2 - 18 june 2017 :::::::::::::::::::::: diff --git a/requirements-dev.txt b/requirements-dev.txt index 55ba71c..69d64d8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,8 +21,8 @@ pygments==2.2.0 pytest-cov==2.5.1 pytest-sugar==0.8.0 pytest-timeout==1.2.0 -pytest==3.1.2 -python-dateutil==2.6.0 +pytest==3.1.3 +python-dateutil==2.6.1 pytz==2017.2 requests==2.18.1 six==1.10.0 diff --git a/requirements-docker.txt b/requirements-docker.txt index 0a39353..f5e74fc 100644 --- a/requirements-docker.txt +++ b/requirements-docker.txt @@ -1,6 +1,6 @@ -e .[docker] appdirs==1.4.3 -bonobo-docker==0.2.9 +bonobo-docker==0.2.11 certifi==2017.4.17 chardet==3.0.4 colorama==0.3.9 @@ -15,6 +15,6 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.18.1 six==1.10.0 -stevedore==1.23.0 +stevedore==1.24.0 urllib3==1.21.1 websocket-client==0.44.0 diff --git a/requirements-jupyter.txt b/requirements-jupyter.txt index d6a6fdb..2542040 100644 --- a/requirements-jupyter.txt +++ b/requirements-jupyter.txt @@ -1,7 +1,7 @@ -e .[jupyter] appnope==0.1.0 bleach==2.0.0 -decorator==4.0.11 +decorator==4.1.1 entrypoints==0.2.3 html5lib==0.999999999 ipykernel==4.6.1 @@ -26,7 +26,7 @@ pickleshare==0.7.4 prompt-toolkit==1.0.14 ptyprocess==0.5.2 pygments==2.2.0 -python-dateutil==2.6.0 +python-dateutil==2.6.1 pyzmq==16.0.2 qtconsole==4.3.0 simplegeneric==0.8.1 diff --git a/requirements.txt b/requirements.txt index 093a6a1..5ddbb01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ pyparsing==2.2.0 pytz==2017.2 requests==2.18.1 six==1.10.0 -stevedore==1.23.0 +stevedore==1.24.0 urllib3==1.21.1 From 258bd6235da7618e7bf470a1b116505ead5bda26 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 16 Jul 2017 11:19:06 +0200 Subject: [PATCH 142/143] [logging] Removes logging colors on windows for now as the codes are mis-interpreted. --- bonobo/logging.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bonobo/logging.py b/bonobo/logging.py index 3784600..1884511 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -8,6 +8,8 @@ from colorama import Fore, Style from bonobo import settings from bonobo.util.term import CLEAR_EOL +iswindows = (sys.platform == 'win32') + def get_format(): yield '{b}[%(fg)s%(levelname)s{b}][{w}' @@ -18,9 +20,9 @@ def get_format(): colors = { - 'b': Fore.BLACK, - 'w': Fore.LIGHTBLACK_EX, - 'r': Style.RESET_ALL, + 'b': '' if iswindows else Fore.BLACK, + 'w': '' if iswindows else Fore.LIGHTBLACK_EX, + 'r': '' if iswindows else Style.RESET_ALL, } format = (''.join(get_format())).format(**colors) @@ -28,7 +30,9 @@ format = (''.join(get_format())).format(**colors) class Filter(logging.Filter): def filter(self, record): record.spent = record.relativeCreated // 1000 - if record.levelname == 'DEBG': + if iswindows: + record.fg = '' + elif record.levelname == 'DEBG': record.fg = Fore.LIGHTBLACK_EX elif record.levelname == 'INFO': record.fg = Fore.LIGHTWHITE_EX @@ -46,7 +50,10 @@ class Filter(logging.Filter): class Formatter(logging.Formatter): def formatException(self, ei): tb = super().formatException(ei) - return textwrap.indent(tb, Fore.BLACK + ' | ' + Fore.WHITE) + if iswindows: + return textwrap.indent(tb, ' | ') + else: + return textwrap.indent(tb, Fore.BLACK + ' | ' + Fore.WHITE) def setup(level): From 423a75d554396512ac58ee37989a91835d8720f3 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Sun, 16 Jul 2017 11:26:48 +0200 Subject: [PATCH 143/143] [logging] Removes kill-until-eol character on windows platform. --- bonobo/logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bonobo/logging.py b/bonobo/logging.py index 1884511..071fcd3 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -16,7 +16,8 @@ def get_format(): yield '{b}][{w}'.join(('%(spent)04d', '%(name)s')) yield '{b}]' yield ' %(fg)s%(message)s{r}' - yield CLEAR_EOL + if not iswindows: + yield CLEAR_EOL colors = {