1
.gitignore
vendored
1
.gitignore
vendored
@ -37,6 +37,7 @@
|
|||||||
/examples/private
|
/examples/private
|
||||||
/htmlcov/
|
/htmlcov/
|
||||||
/sdist/
|
/sdist/
|
||||||
|
/tags
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
parts/
|
parts/
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -1,7 +1,7 @@
|
|||||||
# This file has been auto-generated.
|
# This file has been auto-generated.
|
||||||
# All changes will be lost, see Projectfile.
|
# All changes will be lost, see Projectfile.
|
||||||
#
|
#
|
||||||
# Updated at 2017-05-02 20:38:38.468986
|
# Updated at 2017-05-03 18:02:59.359160
|
||||||
|
|
||||||
PACKAGE ?= bonobo
|
PACKAGE ?= bonobo
|
||||||
PYTHON ?= $(shell which python)
|
PYTHON ?= $(shell which python)
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import warnings
|
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.strategies import create_strategy
|
||||||
from bonobo.structs import Bag, Graph
|
|
||||||
from bonobo.util.objects import get_name
|
from bonobo.util.objects import get_name
|
||||||
from bonobo.io import CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter
|
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ def register_api_group(*args):
|
|||||||
|
|
||||||
|
|
||||||
@register_api
|
@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.
|
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.
|
:param dict services: The implementations of services this graph will use.
|
||||||
:return bonobo.execution.graph.GraphExecutionContext:
|
: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)
|
strategy = create_strategy(strategy)
|
||||||
|
|
||||||
plugins = plugins or []
|
plugins = plugins or []
|
||||||
|
|
||||||
if _is_interactive_console():
|
if _is_interactive_console(): # pragma: no cover
|
||||||
from bonobo.ext.console import ConsoleOutputPlugin
|
from bonobo.ext.console import ConsoleOutputPlugin
|
||||||
if ConsoleOutputPlugin not in plugins:
|
if ConsoleOutputPlugin not in plugins:
|
||||||
plugins.append(ConsoleOutputPlugin)
|
plugins.append(ConsoleOutputPlugin)
|
||||||
|
|
||||||
if _is_jupyter_notebook():
|
if _is_jupyter_notebook(): # pragma: no cover
|
||||||
from bonobo.ext.jupyter import JupyterOutputPlugin
|
from bonobo.ext.jupyter import JupyterOutputPlugin
|
||||||
if JupyterOutputPlugin not in plugins:
|
if JupyterOutputPlugin not in plugins:
|
||||||
plugins.append(JupyterOutputPlugin)
|
plugins.append(JupyterOutputPlugin)
|
||||||
@ -63,7 +58,7 @@ def run(graph, *chain, strategy=None, plugins=None, services=None):
|
|||||||
|
|
||||||
|
|
||||||
# bonobo.structs
|
# bonobo.structs
|
||||||
register_api_group(Bag, Graph)
|
register_api_group(Bag, Graph, Token)
|
||||||
|
|
||||||
# bonobo.strategies
|
# bonobo.strategies
|
||||||
register_api(create_strategy)
|
register_api(create_strategy)
|
||||||
@ -88,8 +83,15 @@ def open_fs(fs_url, *args, **kwargs):
|
|||||||
return _open_fs(str(fs_url), *args, **kwargs)
|
return _open_fs(str(fs_url), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# bonobo.basics
|
# bonobo.nodes
|
||||||
register_api_group(
|
register_api_group(
|
||||||
|
CsvReader,
|
||||||
|
CsvWriter,
|
||||||
|
FileReader,
|
||||||
|
FileWriter,
|
||||||
|
Filter,
|
||||||
|
JsonReader,
|
||||||
|
JsonWriter,
|
||||||
Limit,
|
Limit,
|
||||||
PrettyPrint,
|
PrettyPrint,
|
||||||
Tee,
|
Tee,
|
||||||
@ -99,9 +101,6 @@ register_api_group(
|
|||||||
pprint,
|
pprint,
|
||||||
)
|
)
|
||||||
|
|
||||||
# bonobo.io
|
|
||||||
register_api_group(CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_interactive_console():
|
def _is_interactive_console():
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from bonobo.config.configurables import Configurable
|
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.processors import ContextProcessor
|
||||||
from bonobo.config.services import Container, Service
|
from bonobo.config.services import Container, Service
|
||||||
|
|
||||||
@ -8,5 +8,6 @@ __all__ = [
|
|||||||
'Container',
|
'Container',
|
||||||
'ContextProcessor',
|
'ContextProcessor',
|
||||||
'Option',
|
'Option',
|
||||||
|
'Method',
|
||||||
'Service',
|
'Service',
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
from bonobo.config.options import Method, Option
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor
|
||||||
from bonobo.config.options import Option
|
from bonobo.errors import ConfigurationError
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Configurable',
|
'Configurable',
|
||||||
@ -17,6 +18,7 @@ class ConfigurableMeta(type):
|
|||||||
cls.__options__ = {}
|
cls.__options__ = {}
|
||||||
cls.__positional_options__ = []
|
cls.__positional_options__ = []
|
||||||
cls.__processors__ = []
|
cls.__processors__ = []
|
||||||
|
cls.__wrappable__ = None
|
||||||
|
|
||||||
for typ in cls.__mro__:
|
for typ in cls.__mro__:
|
||||||
for name, value in typ.__dict__.items():
|
for name, value in typ.__dict__.items():
|
||||||
@ -26,8 +28,17 @@ class ConfigurableMeta(type):
|
|||||||
else:
|
else:
|
||||||
if not value.name:
|
if not value.name:
|
||||||
value.name = 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 name in cls.__options__:
|
if not name in cls.__options__:
|
||||||
cls.__options__[name] = value
|
cls.__options__[name] = value
|
||||||
|
|
||||||
if value.positional:
|
if value.positional:
|
||||||
cls.__positional_options__.append(name)
|
cls.__positional_options__.append(name)
|
||||||
|
|
||||||
@ -43,6 +54,12 @@ 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 super(Configurable, cls).__new__(cls)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@ -58,9 +75,11 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
# transform positional arguments in keyword arguments if possible.
|
# transform positional arguments in keyword arguments if possible.
|
||||||
position = 0
|
position = 0
|
||||||
for positional_option in self.__positional_options__:
|
for positional_option in self.__positional_options__:
|
||||||
|
if len(args) <= position:
|
||||||
|
break
|
||||||
|
kwargs[positional_option] = args[position]
|
||||||
|
position += 1
|
||||||
if positional_option in missing:
|
if positional_option in missing:
|
||||||
kwargs[positional_option] = args[position]
|
|
||||||
position += 1
|
|
||||||
missing.remove(positional_option)
|
missing.remove(positional_option)
|
||||||
|
|
||||||
# complain if there are still missing options.
|
# complain if there are still missing options.
|
||||||
@ -85,3 +104,11 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
# set option values.
|
# set option values.
|
||||||
for name, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
setattr(self, name, value)
|
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)
|
||||||
|
|
||||||
|
def call(self, *args, **kwargs):
|
||||||
|
raise AbstractError('Not implemented.')
|
||||||
|
|||||||
@ -1,8 +1,51 @@
|
|||||||
class Option:
|
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
|
_creation_counter = 0
|
||||||
|
|
||||||
def __init__(self, type=None, *, required=False, positional=False, default=None):
|
def __init__(self, type=None, *, required=False, positional=False, default=None):
|
||||||
@ -16,13 +59,66 @@ class Option:
|
|||||||
self._creation_counter = Option._creation_counter
|
self._creation_counter = Option._creation_counter
|
||||||
Option._creation_counter += 1
|
Option._creation_counter += 1
|
||||||
|
|
||||||
def get_default(self):
|
|
||||||
return self.default() if callable(self.default) else self.default
|
|
||||||
|
|
||||||
def __get__(self, inst, typ):
|
def __get__(self, inst, typ):
|
||||||
if not self.name in inst.__options_values__:
|
if not self.name in inst.__options_values__:
|
||||||
inst.__options_values__[self.name] = self.get_default()
|
inst.__options_values__[self.name] = self.get_default()
|
||||||
return inst.__options_values__[self.name]
|
return inst.__options_values__[self.name]
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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):
|
||||||
|
if isinstance(value, str):
|
||||||
|
raise ValueError('should be callable')
|
||||||
inst.__options_values__[self.name] = self.type(value) if self.type else 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
|
||||||
|
|||||||
@ -1,16 +1,42 @@
|
|||||||
import functools
|
|
||||||
|
|
||||||
import types
|
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.config.options import Option
|
||||||
|
from bonobo.util.compat import deprecated_alias
|
||||||
from bonobo.util.iterators import ensure_tuple
|
from bonobo.util.iterators import ensure_tuple
|
||||||
|
|
||||||
_CONTEXT_PROCESSORS_ATTR = '__processors__'
|
_CONTEXT_PROCESSORS_ATTR = '__processors__'
|
||||||
|
|
||||||
|
|
||||||
class ContextProcessor(Option):
|
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
|
@property
|
||||||
def __name__(self):
|
def __name__(self):
|
||||||
return self.func.__name__
|
return self.func.__name__
|
||||||
@ -49,6 +75,15 @@ class ContextCurrifier:
|
|||||||
self.wrapped = wrapped
|
self.wrapped = wrapped
|
||||||
self.context = tuple(initial_context)
|
self.context = tuple(initial_context)
|
||||||
self._stack = []
|
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):
|
def setup(self, *context):
|
||||||
if len(self._stack):
|
if len(self._stack):
|
||||||
@ -56,19 +91,17 @@ class ContextCurrifier:
|
|||||||
for processor in resolve_processors(self.wrapped):
|
for processor in resolve_processors(self.wrapped):
|
||||||
_processed = processor(self.wrapped, *context, *self.context)
|
_processed = processor(self.wrapped, *context, *self.context)
|
||||||
_append_to_context = next(_processed)
|
_append_to_context = next(_processed)
|
||||||
|
self._stack_values.append(_append_to_context)
|
||||||
if _append_to_context is not None:
|
if _append_to_context is not None:
|
||||||
self.context += ensure_tuple(_append_to_context)
|
self.context += ensure_tuple(_append_to_context)
|
||||||
self._stack.append(_processed)
|
self._stack.append(_processed)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
return self.wrapped(*self.context, *args, **kwargs)
|
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
while len(self._stack):
|
while len(self._stack):
|
||||||
processor = self._stack.pop()
|
processor = self._stack.pop()
|
||||||
try:
|
try:
|
||||||
# todo yield from ? how to ?
|
# todo yield from ? how to ?
|
||||||
next(processor)
|
processor.send(self._stack_values.pop())
|
||||||
except StopIteration as exc:
|
except StopIteration as exc:
|
||||||
# This is normal, and wanted.
|
# This is normal, and wanted.
|
||||||
pass
|
pass
|
||||||
@ -76,40 +109,22 @@ class ContextCurrifier:
|
|||||||
# No error ? We should have had StopIteration ...
|
# No error ? We should have had StopIteration ...
|
||||||
raise RuntimeError('Context processors should not yield more than once.')
|
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.
|
||||||
|
|
||||||
@deprecated
|
Example:
|
||||||
def add_context_processor(cls_or_func, context_processor):
|
|
||||||
getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR).append(context_processor)
|
|
||||||
|
|
||||||
|
>>> with ContextCurrifier(node).as_contextmanager(context) as stack:
|
||||||
|
... stack()
|
||||||
|
|
||||||
@deprecated
|
:param context:
|
||||||
def contextual(cls_or_func):
|
:return:
|
||||||
"""
|
"""
|
||||||
Make sure an element has the context processors collection.
|
self.setup(*context)
|
||||||
|
yield self
|
||||||
:param cls_or_func:
|
self.teardown()
|
||||||
"""
|
|
||||||
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):
|
def resolve_processors(mixed):
|
||||||
|
|||||||
@ -40,6 +40,10 @@ class Service(Option):
|
|||||||
The main goal is not to tie transformations to actual dependencies, so the same can be run in different contexts
|
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).
|
(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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
""" Core required libraries. """
|
|
||||||
@ -52,3 +52,7 @@ class ValidationError(RuntimeError):
|
|||||||
|
|
||||||
class ProhibitedOperationError(RuntimeError):
|
class ProhibitedOperationError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationError(Exception):
|
||||||
|
pass
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
def require(package, requirement=None):
|
||||||
|
requirement = requirement or package
|
||||||
|
|
||||||
|
try:
|
||||||
|
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()
|
||||||
|
print(
|
||||||
|
Fore.YELLOW,
|
||||||
|
' $ pip install {!s}'.format(requirement),
|
||||||
|
Style.RESET_ALL,
|
||||||
|
sep=''
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
raise
|
||||||
|
|||||||
@ -1,182 +1,182 @@
|
|||||||
{"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France",
|
{"O q de poule": "53 rue du ruisseau, 75018 Paris, France",
|
||||||
"Le Sully": "6 Bd henri IV, 75004 Paris, France",
|
"Le chantereine": "51 Rue Victoire, 75009 Paris, France",
|
||||||
"O q de poule": "53 rue du ruisseau, 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",
|
"Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France",
|
||||||
"La Renaissance": "112 Rue Championnet, 75018 Paris, France",
|
"La Renaissance": "112 Rue Championnet, 75018 Paris, France",
|
||||||
"Le chantereine": "51 Rue Victoire, 75009 Paris, France",
|
"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France",
|
||||||
"Le M\u00fcller": "11 rue Feutrier, 75018 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 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 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",
|
"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France",
|
||||||
"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France",
|
"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France",
|
||||||
"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France",
|
"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France",
|
||||||
"Le Square": "31 rue Saint-Dominique, 75007 Paris, France",
|
"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 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",
|
|
||||||
"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 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",
|
||||||
|
"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",
|
"Le Supercoin": "3, rue Baudelique, 75018 Paris, France",
|
||||||
"Populettes": "86 bis rue Riquet, 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",
|
"Le Couvent": "69 rue Broca, 75013 Paris, France",
|
||||||
"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France",
|
"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France",
|
||||||
"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France",
|
|
||||||
"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 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 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 Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France",
|
||||||
"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France",
|
"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France",
|
||||||
"Au panini de la place": "47 rue Belgrand, 75020 Paris, France",
|
"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France",
|
||||||
"Le Village": "182 rue de Courcelles, 75017 Paris, France",
|
"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France",
|
||||||
"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France",
|
"La Perle": "78 rue vieille du temple, 75003 Paris, France",
|
||||||
"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France",
|
"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France",
|
||||||
"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France",
|
"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France",
|
||||||
"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France",
|
"Le Descartes": "1 rue Thouin, 75005 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",
|
"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",
|
"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",
|
"Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France",
|
||||||
"Etienne": "14 rue Turbigo, Paris, 75001 Paris, France",
|
"Etienne": "14 rue Turbigo, Paris, 75001 Paris, France",
|
||||||
"L'ing\u00e9nu": "184 bd Voltaire, 75011 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 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",
|
"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France",
|
||||||
"L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France",
|
"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France",
|
||||||
"Le bal du pirate": "60 rue des bergers, 75015 Paris, France",
|
"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France",
|
||||||
"zic zinc": "95 rue claude decaen, 75012 Paris, France",
|
"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France",
|
||||||
"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France",
|
"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France",
|
||||||
"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France",
|
"Chai 33": "33 Cour Saint Emilion, 75012 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",
|
|
||||||
"Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 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",
|
"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France",
|
||||||
"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France",
|
"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France",
|
||||||
"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France",
|
"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France",
|
||||||
"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France",
|
"Le Centenaire": "104 rue amelot, 75011 Paris, France",
|
||||||
"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France",
|
"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France",
|
||||||
"La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 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 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",
|
"Les Artisans": "106 rue Lecourbe, 75015 Paris, France",
|
||||||
"Peperoni": "83 avenue de Wagram, 75001 Paris, France",
|
"Peperoni": "83 avenue de Wagram, 75001 Paris, France",
|
||||||
"le lutece": "380 rue de vaugirard, 75015 Paris, France",
|
"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France",
|
||||||
"Brasiloja": "16 rue Ganneron, 75018 Paris, France",
|
"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France",
|
||||||
"Rivolux": "16 rue de Rivoli, 75004 Paris, France",
|
"zic zinc": "95 rue claude decaen, 75012 Paris, France",
|
||||||
"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France",
|
"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France",
|
||||||
"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France",
|
"Le Brio": "216, rue Marcadet, 75018 Paris, France",
|
||||||
"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France",
|
"Le Dunois": "77 rue Dunois, 75013 Paris, France",
|
||||||
"O'Paris": "1 Rue des Envierges, 75020 Paris, France",
|
"La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France",
|
||||||
"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 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",
|
"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France",
|
||||||
"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 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",
|
"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France",
|
||||||
"Chez Rutabaga": "16 rue des Petits Champs, 75002 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",
|
"Les caves populaires": "22 rue des Dames, 75017 Paris, France",
|
||||||
"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France",
|
"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 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",
|
|
||||||
"Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 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",
|
"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 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",
|
"maison du vin": "52 rue des plantes, 75014 Paris, France",
|
||||||
"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France",
|
"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France",
|
||||||
"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France",
|
"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France",
|
||||||
"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France",
|
"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France",
|
||||||
"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France",
|
"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France",
|
||||||
"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France",
|
"le lutece": "380 rue de vaugirard, 75015 Paris, France",
|
||||||
"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France",
|
"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France",
|
||||||
"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France",
|
"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France",
|
||||||
"Panem": "18 rue de Crussol, 75011 Paris, France",
|
"O'Paris": "1 Rue des Envierges, 75020 Paris, France",
|
||||||
"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France",
|
"Rivolux": "16 rue de Rivoli, 75004 Paris, France",
|
||||||
"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France",
|
"Brasiloja": "16 rue Ganneron, 75018 Paris, France",
|
||||||
"L'Angle": "28 rue de Ponthieu, 75008 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",
|
"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",
|
"Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France",
|
||||||
"L'Entracte": "place de l'opera, 75002 Paris, France",
|
"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France",
|
||||||
"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France",
|
"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France",
|
||||||
"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France",
|
"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France",
|
||||||
"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France",
|
"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France",
|
||||||
"Le Brigadier": "12 rue Blanche, 75009 Paris, France",
|
"Coffee Chope": "344Vrue Vaugirard, 75015 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",
|
|
||||||
"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France",
|
"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France",
|
||||||
"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France",
|
"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France",
|
||||||
"le Zango": "58 rue Daguerre, 75014 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",
|
"Melting Pot": "3 rue de Lagny, 75020 Paris, France",
|
||||||
"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 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",
|
||||||
|
"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"}
|
||||||
@ -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
|
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
|
Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France
|
||||||
La Renaissance, 112 Rue Championnet, 75018 Paris, France
|
La Renaissance, 112 Rue Championnet, 75018 Paris, France
|
||||||
Le chantereine, 51 Rue Victoire, 75009 Paris, France
|
Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France
|
||||||
Le Müller, 11 rue Feutrier, 75018 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 drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France
|
||||||
Le café des amis, 125 rue Blomet, 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
|
Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France
|
||||||
Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France
|
Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France
|
||||||
Les Arcades, 61 rue de Ponthieu, 75008 Paris, France
|
Le Müller, 11 rue Feutrier, 75018 Paris, France
|
||||||
Le Square, 31 rue Saint-Dominique, 75007 Paris, France
|
Le Café Livres, 10 rue Saint Martin, 75004 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
|
|
||||||
La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 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
|
||||||
|
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
|
Le Supercoin, 3, rue Baudelique, 75018 Paris, France
|
||||||
Populettes, 86 bis rue Riquet, 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
|
Le Couvent, 69 rue Broca, 75013 Paris, France
|
||||||
La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France
|
Café Zen, 46 rue Victoire, 75009 Paris, France
|
||||||
L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France
|
|
||||||
Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 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 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 Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France
|
||||||
Le Malar, 88 rue Saint-Dominique, 75007 Paris, France
|
Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France
|
||||||
Au panini de la place, 47 rue Belgrand, 75020 Paris, France
|
Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France
|
||||||
Le Village, 182 rue de Courcelles, 75017 Paris, France
|
Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France
|
||||||
Pause Café, 41 rue de Charonne, 75011 Paris, France
|
La Perle, 78 rue vieille du temple, 75003 Paris, France
|
||||||
Le Pure café, 14 rue Jean Macé, 75011 Paris, France
|
Le Café frappé, 95 rue Montmartre, 75002 Paris, France
|
||||||
Extra old café, 307 fg saint Antoine, 75011 Paris, France
|
L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France
|
||||||
Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France
|
Le Descartes, 1 rue Thouin, 75005 Paris, France
|
||||||
En attendant l'or, 3 rue Faidherbe, 75011 Paris, France
|
|
||||||
Brûlerie San José, 30 rue des Petits-Champs, 75002 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
|
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
|
Café Martin, 2 place Martin Nadaud, 75001 Paris, France
|
||||||
Etienne, 14 rue Turbigo, Paris, 75001 Paris, France
|
Etienne, 14 rue Turbigo, Paris, 75001 Paris, France
|
||||||
L'ingénu, 184 bd Voltaire, 75011 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 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
|
Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France
|
||||||
L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France
|
Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France
|
||||||
Le bal du pirate, 60 rue des bergers, 75015 Paris, France
|
L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France
|
||||||
zic zinc, 95 rue claude decaen, 75012 Paris, France
|
Le pari's café, 104 rue caulaincourt, 75018 Paris, France
|
||||||
l'orillon bar, 35 rue de l'orillon, 75011 Paris, France
|
Le Poulailler, 60 rue saint-sabin, 75011 Paris, France
|
||||||
Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France
|
Chai 33, 33 Cour Saint Emilion, 75012 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
|
|
||||||
Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 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
|
Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France
|
||||||
Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France
|
Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France
|
||||||
bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France
|
Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France
|
||||||
Café beauveau, 9 rue de Miromesnil, 75008 Paris, France
|
Le Centenaire, 104 rue amelot, 75011 Paris, France
|
||||||
Café Pistache, 9 rue des petits champs, 75001 Paris, France
|
Café Pistache, 9 rue des petits champs, 75001 Paris, France
|
||||||
La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 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 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
|
Les Artisans, 106 rue Lecourbe, 75015 Paris, France
|
||||||
Peperoni, 83 avenue de Wagram, 75001 Paris, France
|
Peperoni, 83 avenue de Wagram, 75001 Paris, France
|
||||||
le lutece, 380 rue de vaugirard, 75015 Paris, France
|
Ragueneau, 202 rue Saint Honoré, 75001 Paris, France
|
||||||
Brasiloja, 16 rue Ganneron, 75018 Paris, France
|
l'orillon bar, 35 rue de l'orillon, 75011 Paris, France
|
||||||
Rivolux, 16 rue de Rivoli, 75004 Paris, France
|
zic zinc, 95 rue claude decaen, 75012 Paris, France
|
||||||
Chai 33, 33 Cour Saint Emilion, 75012 Paris, France
|
L'Inévitable, 22 rue Linné, 75005 Paris, France
|
||||||
L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France
|
Le Brio, 216, rue Marcadet, 75018 Paris, France
|
||||||
NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France
|
Le Dunois, 77 rue Dunois, 75013 Paris, France
|
||||||
O'Paris, 1 Rue des Envierges, 75020 Paris, France
|
La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France
|
||||||
Café Clochette, 16 avenue Richerand, 75010 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
|
La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France
|
||||||
Le Saint René, 148 Boulevard de Charonne, 75020 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
|
La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France
|
||||||
Chez Rutabaga, 16 rue des Petits Champs, 75002 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
|
Les caves populaires, 22 rue des Dames, 75017 Paris, France
|
||||||
Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France
|
Le Plomb du cantal, 3 rue Gaîté, 75014 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
|
|
||||||
Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 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
|
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 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
|
maison du vin, 52 rue des plantes, 75014 Paris, France
|
||||||
Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France
|
Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France
|
||||||
L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France
|
NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France
|
||||||
Le café Monde et Médias, Place de la République, 75003 Paris, France
|
Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France
|
||||||
Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France
|
Le Tournebride, 104 rue Mouffetard, 75005 Paris, France
|
||||||
Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France
|
le lutece, 380 rue de vaugirard, 75015 Paris, France
|
||||||
Chez Miamophile, 6 rue Mélingue, 75019 Paris, France
|
Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France
|
||||||
La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France
|
Le Fronton, 63 rue de Ponthieu, 75008 Paris, France
|
||||||
Panem, 18 rue de Crussol, 75011 Paris, France
|
O'Paris, 1 Rue des Envierges, 75020 Paris, France
|
||||||
Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France
|
Rivolux, 16 rue de Rivoli, 75004 Paris, France
|
||||||
Café Dupont, 198 rue de la Convention, 75015 Paris, France
|
Brasiloja, 16 rue Ganneron, 75018 Paris, France
|
||||||
L'Angle, 28 rue de Ponthieu, 75008 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
|
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
|
Canopy Café associatif, 19 rue Pajol, 75018 Paris, France
|
||||||
L'Entracte, place de l'opera, 75002 Paris, France
|
Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France
|
||||||
Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France
|
Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France
|
||||||
Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France
|
Le café Monde et Médias, Place de la République, 75003 Paris, France
|
||||||
Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France
|
L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France
|
||||||
Le Brigadier, 12 rue Blanche, 75009 Paris, France
|
Coffee Chope, 344Vrue Vaugirard, 75015 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
|
|
||||||
Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France
|
Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France
|
||||||
Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France
|
L'empreinte, 54, avenue Daumesnil, 75012 Paris, France
|
||||||
le Zango, 58 rue Daguerre, 75014 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
|
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
|
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
|
||||||
21
bonobo/examples/nodes/filter.py
Normal file
21
bonobo/examples/nodes/filter.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import bonobo
|
||||||
|
|
||||||
|
from bonobo 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,
|
||||||
|
)
|
||||||
@ -1,21 +1,27 @@
|
|||||||
import traceback
|
import traceback
|
||||||
|
from contextlib import contextmanager
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from bonobo.config import Container
|
from bonobo.config import Container
|
||||||
from bonobo.config.processors import resolve_processors, ContextCurrifier
|
from bonobo.config.processors import ContextCurrifier
|
||||||
|
from bonobo.plugins import get_enhancers
|
||||||
from bonobo.util.errors import print_error
|
from bonobo.util.errors import print_error
|
||||||
from bonobo.util.iterators import ensure_tuple
|
from bonobo.util.objects import Wrapper, get_name
|
||||||
from bonobo.util.objects import Wrapper
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def unrecoverable(error_handler):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
error_handler(exc, traceback.format_exc())
|
||||||
|
raise # raise unrecoverableerror from x ?
|
||||||
|
|
||||||
|
|
||||||
class LoopingExecutionContext(Wrapper):
|
class LoopingExecutionContext(Wrapper):
|
||||||
alive = True
|
alive = True
|
||||||
PERIOD = 0.25
|
PERIOD = 0.25
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
return self._started, self._stopped
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def started(self):
|
def started(self):
|
||||||
return self._started
|
return self._started
|
||||||
@ -26,7 +32,9 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
|
|
||||||
def __init__(self, wrapped, parent, services=None):
|
def __init__(self, wrapped, parent, services=None):
|
||||||
super().__init__(wrapped)
|
super().__init__(wrapped)
|
||||||
|
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
if services:
|
if services:
|
||||||
if parent:
|
if parent:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -36,19 +44,25 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
else:
|
else:
|
||||||
self.services = None
|
self.services = None
|
||||||
|
|
||||||
self._started, self._stopped, self._stack = False, False, None
|
self._started, self._stopped = False, False
|
||||||
|
self._stack = None
|
||||||
|
|
||||||
|
# XXX enhancers
|
||||||
|
self._enhancers = get_enhancers(self.wrapped)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
assert self.state == (False,
|
if self.started:
|
||||||
False), ('{}.start() can only be called on a new node.').format(type(self).__name__)
|
raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self)))
|
||||||
|
|
||||||
self._started = True
|
self._started = True
|
||||||
self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context())
|
self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context())
|
||||||
|
|
||||||
try:
|
with unrecoverable(self.handle_error):
|
||||||
self._stack.setup(self)
|
self._stack.setup(self)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
|
||||||
self.handle_error(exc, traceback.format_exc())
|
for enhancer in self._enhancers:
|
||||||
raise
|
with unrecoverable(self.handle_error):
|
||||||
|
enhancer.start(self)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Generic loop. A bit boring. """
|
"""Generic loop. A bit boring. """
|
||||||
@ -61,16 +75,17 @@ class LoopingExecutionContext(Wrapper):
|
|||||||
raise NotImplementedError('Abstract.')
|
raise NotImplementedError('Abstract.')
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
assert self._started, ('{}.stop() can only be called on a previously started node.').format(type(self).__name__)
|
if not self.started:
|
||||||
|
raise RuntimeError('Cannot stop an unstarted node ({}).'.format(get_name(self)))
|
||||||
|
|
||||||
if self._stopped:
|
if self._stopped:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._stopped = True
|
|
||||||
try:
|
try:
|
||||||
self._stack.teardown()
|
with unrecoverable(self.handle_error):
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
self._stack.teardown()
|
||||||
self.handle_error(exc, traceback.format_exc())
|
finally:
|
||||||
raise
|
self._stopped = True
|
||||||
|
|
||||||
def handle_error(self, exc, trace):
|
def handle_error(self, exc, trace):
|
||||||
return print_error(exc, trace, context=self.wrapped)
|
return print_error(exc, trace, context=self.wrapped)
|
||||||
|
|||||||
@ -21,16 +21,12 @@ class GraphExecutionContext:
|
|||||||
|
|
||||||
def __init__(self, graph, plugins=None, services=None):
|
def __init__(self, graph, plugins=None, services=None):
|
||||||
self.graph = graph
|
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.plugins = [PluginExecutionContext(plugin, parent=self) for plugin in plugins or ()]
|
||||||
self.services = Container(services) if services else Container()
|
self.services = Container(services) if services else Container()
|
||||||
|
|
||||||
for i, node_context in enumerate(self):
|
for i, node_context in enumerate(self):
|
||||||
try:
|
node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
|
||||||
node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
node_context.input.on_begin = partial(node_context.send, BEGIN, _control=True)
|
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_end = partial(node_context.send, END, _control=True)
|
||||||
node_context.input.on_finalize = partial(node_context.stop)
|
node_context.input.on_finalize = partial(node_context.stop)
|
||||||
@ -50,7 +46,7 @@ class GraphExecutionContext:
|
|||||||
|
|
||||||
for i in self.graph.outputs_of(BEGIN):
|
for i in self.graph.outputs_of(BEGIN):
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self[i].recv(message)
|
self[i].write(message)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# todo use strategy
|
# todo use strategy
|
||||||
|
|||||||
@ -3,13 +3,15 @@ from queue import Empty
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from bonobo.constants import INHERIT_INPUT, NOT_MODIFIED
|
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.errors import InactiveReadableError
|
||||||
from bonobo.execution.base import LoopingExecutionContext
|
from bonobo.execution.base import LoopingExecutionContext
|
||||||
from bonobo.structs.bags import Bag
|
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.errors import is_error
|
||||||
from bonobo.util.iterators import iter_if_not_sequence
|
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):
|
class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
||||||
@ -20,7 +22,11 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
@property
|
@property
|
||||||
def alive(self):
|
def alive(self):
|
||||||
"""todo check if this is right, and where it is used"""
|
"""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):
|
||||||
|
return '+' if self.alive else '-'
|
||||||
|
|
||||||
def __init__(self, wrapped, parent=None, services=None):
|
def __init__(self, wrapped, parent=None, services=None):
|
||||||
LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services)
|
LoopingExecutionContext.__init__(self, wrapped, parent=parent, services=services)
|
||||||
@ -30,18 +36,13 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
self.outputs = []
|
self.outputs = []
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (('+' if self.alive else '-') + ' ' + self.__name__ + ' ' + self.get_statistics_as_string()).strip()
|
return self.alive_str + ' ' + self.__name__ + self.get_statistics_as_string(prefix=' ')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
stats = self.get_statistics_as_string().strip()
|
name, type_name = get_name(self), get_name(type(self))
|
||||||
return '<{}({}{}){}>'.format(
|
return '<{}({}{}){}>'.format(type_name, self.alive_str, name, self.get_statistics_as_string(prefix=' '))
|
||||||
type(self).__name__,
|
|
||||||
'+' if self.alive else '',
|
|
||||||
self.__name__,
|
|
||||||
(' ' + stats) if stats else '',
|
|
||||||
)
|
|
||||||
|
|
||||||
def recv(self, *messages):
|
def write(self, *messages):
|
||||||
"""
|
"""
|
||||||
Push a message list to this context's input queue.
|
Push a message list to this context's input queue.
|
||||||
|
|
||||||
@ -50,6 +51,9 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
for message in messages:
|
for message in messages:
|
||||||
self.input.put(message)
|
self.input.put(message)
|
||||||
|
|
||||||
|
# XXX deprecated alias
|
||||||
|
recv = deprecated_alias('recv', write)
|
||||||
|
|
||||||
def send(self, value, _control=False):
|
def send(self, value, _control=False):
|
||||||
"""
|
"""
|
||||||
Sends a message to all of this context's outputs.
|
Sends a message to all of this context's outputs.
|
||||||
@ -57,12 +61,19 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
:param mixed value: message
|
:param mixed value: message
|
||||||
:param _control: if true, won't count in statistics.
|
:param _control: if true, won't count in statistics.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not _control:
|
if not _control:
|
||||||
self.increment('out')
|
self.increment('out')
|
||||||
for output in self.outputs:
|
|
||||||
output.put(value)
|
|
||||||
|
|
||||||
def get(self):
|
if is_error(value):
|
||||||
|
value.apply(self.handle_error)
|
||||||
|
else:
|
||||||
|
for output in self.outputs:
|
||||||
|
output.put(value)
|
||||||
|
|
||||||
|
push = deprecated_alias('push', send)
|
||||||
|
|
||||||
|
def get(self): # recv() ? input_data = self.receive()
|
||||||
"""
|
"""
|
||||||
Get from the queue first, then increment stats, so if Queue raise Timeout or Empty, stat won't be changed.
|
Get from the queue first, then increment stats, so if Queue raise Timeout or Empty, stat won't be changed.
|
||||||
|
|
||||||
@ -95,12 +106,6 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
# todo add timer
|
# todo add timer
|
||||||
self.handle_results(input_bag, input_bag.apply(self._stack))
|
self.handle_results(input_bag, input_bag.apply(self._stack))
|
||||||
|
|
||||||
def push(self, bag):
|
|
||||||
# MAKE THIS PUBLIC API FOR CONTEXT PROCESSORS !!!
|
|
||||||
# xxx handle error or send in first call to apply(...)?
|
|
||||||
# xxx return value ?
|
|
||||||
bag.apply(self.handle_error) if is_error(bag) else self.send(bag)
|
|
||||||
|
|
||||||
def handle_results(self, input_bag, results):
|
def handle_results(self, input_bag, results):
|
||||||
# self._exec_time += timer.duration
|
# self._exec_time += timer.duration
|
||||||
# Put data onto output channels
|
# Put data onto output channels
|
||||||
@ -108,7 +113,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
results = iter_if_not_sequence(results)
|
results = iter_if_not_sequence(results)
|
||||||
except TypeError: # not an iterator
|
except TypeError: # not an iterator
|
||||||
if results:
|
if results:
|
||||||
self.push(_resolve(input_bag, results))
|
self.send(_resolve(input_bag, results))
|
||||||
else:
|
else:
|
||||||
# case with no result, an execution went through anyway, use for stats.
|
# case with no result, an execution went through anyway, use for stats.
|
||||||
# self._exec_count += 1
|
# self._exec_count += 1
|
||||||
@ -120,7 +125,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.push(_resolve(input_bag, result))
|
self.send(_resolve(input_bag, result))
|
||||||
|
|
||||||
|
|
||||||
def _resolve(input_bag, output):
|
def _resolve(input_bag, output):
|
||||||
|
|||||||
@ -1,40 +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 functools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from colorama import Fore, Style
|
from colorama import Style, Fore
|
||||||
|
|
||||||
|
from bonobo import settings
|
||||||
from bonobo.plugins import Plugin
|
from bonobo.plugins import Plugin
|
||||||
from bonobo.util.term import CLEAR_EOL, MOVE_CURSOR_UP
|
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.get_memory_info()[0] / float(2**20)
|
|
||||||
|
|
||||||
|
|
||||||
# @lru_cache(64)
|
|
||||||
# def execution_time(harness):
|
|
||||||
# return datetime.datetime.now() - harness._started_at
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleOutputPlugin(Plugin):
|
class ConsoleOutputPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor
|
Outputs status information to the connected stdout. Can be a TTY, with or without support for colors/cursor
|
||||||
@ -50,15 +23,14 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
self.prefix = ''
|
self.prefix = ''
|
||||||
|
|
||||||
def _write(self, graph_context, rewind):
|
def _write(self, graph_context, rewind):
|
||||||
profile, debug = False, False
|
if settings.PROFILE:
|
||||||
if profile:
|
|
||||||
append = (
|
append = (
|
||||||
('Memory', '{0:.2f} Mb'.format(memory_usage())),
|
('Memory', '{0:.2f} Mb'.format(memory_usage())),
|
||||||
# ('Total time', '{0} s'.format(execution_time(harness))),
|
# ('Total time', '{0} s'.format(execution_time(harness))),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
append = ()
|
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):
|
def run(self):
|
||||||
if sys.stdout.isatty():
|
if sys.stdout.isatty():
|
||||||
@ -70,23 +42,24 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
self._write(self.context.parent, rewind=False)
|
self._write(self.context.parent, rewind=False)
|
||||||
|
|
||||||
@staticmethod
|
@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)
|
t_cnt = len(context)
|
||||||
|
|
||||||
for i, component in enumerate(context):
|
for i in context.graph.topologically_sorted_indexes:
|
||||||
if component.alive:
|
node = context[i]
|
||||||
|
name_suffix = '({})'.format(i) if settings.DEBUG else ''
|
||||||
|
if node.alive:
|
||||||
_line = ''.join(
|
_line = ''.join(
|
||||||
(
|
(
|
||||||
Fore.BLACK, '({})'.format(i + 1), Style.RESET_ALL, ' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ',
|
' ', Style.BRIGHT, '+', Style.RESET_ALL, ' ', node.name, name_suffix, ' ',
|
||||||
component.name, ' ', component.get_statistics_as_string(debug=debug,
|
node.get_statistics_as_string(), Style.RESET_ALL, ' ',
|
||||||
profile=profile), Style.RESET_ALL, ' ',
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_line = ''.join(
|
_line = ''.join(
|
||||||
(
|
(
|
||||||
Fore.BLACK, '({})'.format(i + 1), ' - ', component.name, ' ',
|
' ', Fore.BLACK, '-', ' ', node.name, name_suffix, ' ', node.get_statistics_as_string(),
|
||||||
component.get_statistics_as_string(debug=debug, profile=profile), Style.RESET_ALL, ' ',
|
Style.RESET_ALL, ' ',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(prefix + _line + '\033[0K')
|
print(prefix + _line + '\033[0K')
|
||||||
@ -106,3 +79,10 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
if rewind:
|
if rewind:
|
||||||
print(CLEAR_EOL)
|
print(CLEAR_EOL)
|
||||||
print(MOVE_CURSOR_UP(t_cnt + 2))
|
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)
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from .plugin import ConsoleOutputPlugin
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ConsoleOutputPlugin',
|
|
||||||
]
|
|
||||||
@ -3,9 +3,8 @@ from urllib.parse import urlencode
|
|||||||
import requests # todo: make this a service so we can substitute it ?
|
import requests # todo: make this a service so we can substitute it ?
|
||||||
|
|
||||||
from bonobo.config import Option
|
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.config.configurables import Configurable
|
||||||
from bonobo.util.compat import deprecated
|
|
||||||
from bonobo.util.objects import ValueHolder
|
from bonobo.util.objects import ValueHolder
|
||||||
|
|
||||||
|
|
||||||
@ -47,12 +46,7 @@ class OpenDataSoftAPI(Configurable):
|
|||||||
for row in records:
|
for row in records:
|
||||||
yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})}
|
yield {**row.get('fields', {}), 'geometry': row.get('geometry', {})}
|
||||||
|
|
||||||
start.value += self.rows
|
start += self.rows
|
||||||
|
|
||||||
|
|
||||||
@deprecated
|
|
||||||
def from_opendatasoft_api(dataset, **kwargs):
|
|
||||||
return OpenDataSoftAPI(dataset=dataset, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
9
bonobo/nodes/__init__.py
Normal file
9
bonobo/nodes/__init__.py
Normal file
@ -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']
|
||||||
@ -3,10 +3,12 @@ from pprint import pprint as _pprint
|
|||||||
|
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
|
from bonobo.config import Configurable, Option
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor
|
||||||
from bonobo.structs.bags import Bag
|
from bonobo.structs.bags import Bag
|
||||||
from bonobo.util.objects import ValueHolder
|
from bonobo.util.objects import ValueHolder
|
||||||
from bonobo.util.term import CLEAR_EOL
|
from bonobo.util.term import CLEAR_EOL
|
||||||
|
from bonobo.constants import NOT_MODIFIED
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'identity',
|
'identity',
|
||||||
@ -23,19 +25,26 @@ def identity(x):
|
|||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
def Limit(n=10):
|
class Limit(Configurable):
|
||||||
from bonobo.constants import NOT_MODIFIED
|
"""
|
||||||
i = 0
|
Creates a Limit() node, that will only let go through the first n rows (defined by the `limit` option), unmodified.
|
||||||
|
|
||||||
def _limit(*args, **kwargs):
|
.. attribute:: limit
|
||||||
nonlocal i, n
|
|
||||||
i += 1
|
Number of rows to let go through.
|
||||||
if i <= n:
|
|
||||||
|
"""
|
||||||
|
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
|
yield NOT_MODIFIED
|
||||||
|
|
||||||
_limit.__name__ = 'Limit({})'.format(n)
|
|
||||||
return _limit
|
|
||||||
|
|
||||||
|
|
||||||
def Tee(f):
|
def Tee(f):
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.constants import NOT_MODIFIED
|
||||||
@ -57,7 +66,7 @@ def count(counter, *args, **kwargs):
|
|||||||
def _count_counter(self, context):
|
def _count_counter(self, context):
|
||||||
counter = ValueHolder(0)
|
counter = ValueHolder(0)
|
||||||
yield counter
|
yield counter
|
||||||
context.send(Bag(counter.value))
|
context.send(Bag(counter._value))
|
||||||
|
|
||||||
|
|
||||||
pprint = Tee(_pprint)
|
pprint = Tee(_pprint)
|
||||||
26
bonobo/nodes/filter.py
Normal file
26
bonobo/nodes/filter.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
from bonobo.config import Option
|
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 bonobo.util.objects import ValueHolder
|
||||||
from .file import FileHandler, FileReader, FileWriter
|
from .file import FileHandler, FileReader, FileWriter
|
||||||
|
|
||||||
@ -45,8 +46,11 @@ class CsvReader(CsvHandler, FileReader):
|
|||||||
|
|
||||||
def read(self, fs, file, headers):
|
def read(self, fs, file, headers):
|
||||||
reader = csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar)
|
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:
|
if self.skip and self.skip > 0:
|
||||||
for _ in range(0, self.skip):
|
for _ in range(0, self.skip):
|
||||||
@ -67,8 +71,9 @@ class CsvWriter(CsvHandler, FileWriter):
|
|||||||
yield writer, headers
|
yield writer, headers
|
||||||
|
|
||||||
def write(self, fs, file, lineno, writer, headers, row):
|
def write(self, fs, file, lineno, writer, headers, row):
|
||||||
if not lineno.value:
|
if not lineno:
|
||||||
headers.value = headers.value or row.keys()
|
headers.set(headers.value or row.keys())
|
||||||
writer.writerow(headers.value)
|
writer.writerow(headers.get())
|
||||||
writer.writerow(row[header] for header in headers.value)
|
writer.writerow(row[header] for header in headers.get())
|
||||||
lineno.value += 1
|
lineno += 1
|
||||||
|
return NOT_MODIFIED
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from bonobo.config import Option, Service
|
from bonobo.config import Option, Service
|
||||||
from bonobo.config.configurables import Configurable
|
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
|
from bonobo.util.objects import ValueHolder
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -85,7 +86,7 @@ class FileWriter(Writer):
|
|||||||
|
|
||||||
@ContextProcessor
|
@ContextProcessor
|
||||||
def lineno(self, context, fs, file):
|
def lineno(self, context, fs, file):
|
||||||
lineno = ValueHolder(0, type=int)
|
lineno = ValueHolder(0)
|
||||||
yield lineno
|
yield lineno
|
||||||
|
|
||||||
def write(self, fs, file, lineno, row):
|
def write(self, fs, file, lineno, row):
|
||||||
@ -93,7 +94,8 @@ class FileWriter(Writer):
|
|||||||
Write a row on the next line of opened file in context.
|
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 '') + row)
|
||||||
lineno.value += 1
|
lineno += 1
|
||||||
|
return NOT_MODIFIED
|
||||||
|
|
||||||
def _write_line(self, file, line):
|
def _write_line(self, file, line):
|
||||||
return file.write(line)
|
return file.write(line)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from bonobo.config.processors import ContextProcessor, contextual
|
from bonobo.config.processors import ContextProcessor
|
||||||
from .file import FileWriter, FileReader
|
from .file import FileWriter, FileReader
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
from bonobo.config import Configurable
|
||||||
|
from bonobo.util.objects import get_attribute_or_create
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""
|
"""
|
||||||
A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need
|
A plugin is an extension to the core behavior of bonobo. If you're writing transformations, you should not need
|
||||||
@ -21,3 +25,16 @@ class Plugin:
|
|||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_enhancers(obj):
|
||||||
|
try:
|
||||||
|
return get_attribute_or_create(obj, '__enhancers__', list())
|
||||||
|
except AttributeError:
|
||||||
|
return list()
|
||||||
|
|
||||||
|
|
||||||
|
class NodeEnhancer(Configurable):
|
||||||
|
def __matmul__(self, other):
|
||||||
|
get_enhancers(other).append(self)
|
||||||
|
return other
|
||||||
|
|||||||
16
bonobo/settings.py
Normal file
16
bonobo/settings.py
Normal file
@ -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'))
|
||||||
@ -67,9 +67,7 @@ class Bag:
|
|||||||
iter(func_or_iter)
|
iter(func_or_iter)
|
||||||
|
|
||||||
def generator():
|
def generator():
|
||||||
nonlocal func_or_iter
|
yield from func_or_iter
|
||||||
for x in func_or_iter:
|
|
||||||
yield x
|
|
||||||
|
|
||||||
return generator()
|
return generator()
|
||||||
except TypeError as exc:
|
except TypeError as exc:
|
||||||
@ -87,6 +85,9 @@ class Bag:
|
|||||||
def inherit(cls, *args, **kwargs):
|
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
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<{} ({})>'.format(
|
return '<{} ({})>'.format(
|
||||||
type(self).__name__, ', '.
|
type(self).__name__, ', '.
|
||||||
|
|||||||
@ -3,29 +3,115 @@ from bonobo.constants import BEGIN
|
|||||||
|
|
||||||
class Graph:
|
class Graph:
|
||||||
"""
|
"""
|
||||||
Represents a coherent directed acyclic graph of components.
|
Represents a directed graph of nodes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *chain):
|
def __init__(self, *chain):
|
||||||
|
self.edges = {BEGIN: set()}
|
||||||
|
self.named = {}
|
||||||
self.nodes = []
|
self.nodes = []
|
||||||
self.graph = {BEGIN: set()}
|
|
||||||
self.add_chain(*chain)
|
self.add_chain(*chain)
|
||||||
|
|
||||||
def outputs_of(self, idx, create=False):
|
def __iter__(self):
|
||||||
if create and not idx in self.graph:
|
yield from self.nodes
|
||||||
self.graph[idx] = set()
|
|
||||||
return self.graph[idx]
|
|
||||||
|
|
||||||
def add_node(self, c):
|
|
||||||
i = len(self.nodes)
|
|
||||||
self.nodes.append(c)
|
|
||||||
return i
|
|
||||||
|
|
||||||
def add_chain(self, *nodes, _input=BEGIN):
|
|
||||||
for node in nodes:
|
|
||||||
_next = self.add_node(node)
|
|
||||||
self.outputs_of(_input, create=True).add(_next)
|
|
||||||
_input = _next
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
|
""" Node count.
|
||||||
|
"""
|
||||||
return len(self.nodes)
|
return len(self.nodes)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.nodes[key]
|
||||||
|
|
||||||
|
def outputs_of(self, idx, create=False):
|
||||||
|
""" 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):
|
||||||
|
""" 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 idx
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@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))
|
||||||
|
|||||||
@ -15,11 +15,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
from bonobo.errors import AbstractError, InactiveWritableError, InactiveReadableError
|
|
||||||
from bonobo.constants import BEGIN, END
|
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
|
BUFFER_SIZE = 8192
|
||||||
|
|
||||||
@ -1,31 +1,7 @@
|
|||||||
import functools
|
import functools
|
||||||
import struct
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import warnings
|
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):
|
def deprecated_alias(alias, func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def new_func(*args, **kwargs):
|
def new_func(*args, **kwargs):
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
import functools
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
def get_name(mixed):
|
def get_name(mixed):
|
||||||
try:
|
try:
|
||||||
return mixed.__name__
|
return mixed.__name__
|
||||||
@ -27,175 +31,199 @@ class ValueHolder:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, value, *, type=None):
|
def __init__(self, value):
|
||||||
self.value = value
|
self._value = value
|
||||||
self.type = type
|
|
||||||
|
|
||||||
def __repr__(self):
|
@property
|
||||||
return repr(self.value)
|
def value(self):
|
||||||
|
# XXX deprecated
|
||||||
|
return self._value
|
||||||
|
|
||||||
def __lt__(self, other):
|
def get(self):
|
||||||
return self.value < other
|
return self._value
|
||||||
|
|
||||||
def __le__(self, other):
|
def set(self, new_value):
|
||||||
return self.value <= other
|
self._value = new_value
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self._value)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.value == other
|
return self._value == other
|
||||||
|
|
||||||
def __ne__(self, 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):
|
def __gt__(self, other):
|
||||||
return self.value > other
|
return self._value > other
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
return self.value >= other
|
return self._value >= other
|
||||||
|
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
return self.value + other
|
return self._value + other
|
||||||
|
|
||||||
def __radd__(self, other):
|
def __radd__(self, other):
|
||||||
return other + self.value
|
return other + self._value
|
||||||
|
|
||||||
def __iadd__(self, other):
|
def __iadd__(self, other):
|
||||||
self.value += other
|
self._value += other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __sub__(self, other):
|
def __sub__(self, other):
|
||||||
return self.value - other
|
return self._value - other
|
||||||
|
|
||||||
def __rsub__(self, other):
|
def __rsub__(self, other):
|
||||||
return other - self.value
|
return other - self._value
|
||||||
|
|
||||||
def __isub__(self, other):
|
def __isub__(self, other):
|
||||||
self.value -= other
|
self._value -= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __mul__(self, other):
|
def __mul__(self, other):
|
||||||
return self.value * other
|
return self._value * other
|
||||||
|
|
||||||
def __rmul__(self, other):
|
def __rmul__(self, other):
|
||||||
return other * self.value
|
return other * self._value
|
||||||
|
|
||||||
def __imul__(self, other):
|
def __imul__(self, other):
|
||||||
self.value *= other
|
self._value *= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __matmul__(self, other):
|
def __matmul__(self, other):
|
||||||
return self.value @ other
|
return self._value @ other
|
||||||
|
|
||||||
def __rmatmul__(self, other):
|
def __rmatmul__(self, other):
|
||||||
return other @ self.value
|
return other @ self._value
|
||||||
|
|
||||||
def __imatmul__(self, other):
|
def __imatmul__(self, other):
|
||||||
self.value @= other
|
self._value @= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __truediv__(self, other):
|
def __truediv__(self, other):
|
||||||
return self.value / other
|
return self._value / other
|
||||||
|
|
||||||
def __rtruediv__(self, other):
|
def __rtruediv__(self, other):
|
||||||
return other / self.value
|
return other / self._value
|
||||||
|
|
||||||
def __itruediv__(self, other):
|
def __itruediv__(self, other):
|
||||||
self.value /= other
|
self._value /= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __floordiv__(self, other):
|
def __floordiv__(self, other):
|
||||||
return self.value // other
|
return self._value // other
|
||||||
|
|
||||||
def __rfloordiv__(self, other):
|
def __rfloordiv__(self, other):
|
||||||
return other // self.value
|
return other // self._value
|
||||||
|
|
||||||
def __ifloordiv__(self, other):
|
def __ifloordiv__(self, other):
|
||||||
self.value //= other
|
self._value //= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __mod__(self, other):
|
def __mod__(self, other):
|
||||||
return self.value % other
|
return self._value % other
|
||||||
|
|
||||||
def __rmod__(self, other):
|
def __rmod__(self, other):
|
||||||
return other % self.value
|
return other % self._value
|
||||||
|
|
||||||
def __imod__(self, other):
|
def __imod__(self, other):
|
||||||
self.value %= other
|
self._value %= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __divmod__(self, other):
|
def __divmod__(self, other):
|
||||||
return divmod(self.value, other)
|
return divmod(self._value, other)
|
||||||
|
|
||||||
def __rdivmod__(self, other):
|
def __rdivmod__(self, other):
|
||||||
return divmod(other, self.value)
|
return divmod(other, self._value)
|
||||||
|
|
||||||
def __pow__(self, other):
|
def __pow__(self, other):
|
||||||
return self.value**other
|
return self._value**other
|
||||||
|
|
||||||
def __rpow__(self, other):
|
def __rpow__(self, other):
|
||||||
return other**self.value
|
return other**self._value
|
||||||
|
|
||||||
def __ipow__(self, other):
|
def __ipow__(self, other):
|
||||||
self.value **= other
|
self._value **= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __lshift__(self, other):
|
def __lshift__(self, other):
|
||||||
return self.value << other
|
return self._value << other
|
||||||
|
|
||||||
def __rlshift__(self, other):
|
def __rlshift__(self, other):
|
||||||
return other << self.value
|
return other << self._value
|
||||||
|
|
||||||
def __ilshift__(self, other):
|
def __ilshift__(self, other):
|
||||||
self.value <<= other
|
self._value <<= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __rshift__(self, other):
|
def __rshift__(self, other):
|
||||||
return self.value >> other
|
return self._value >> other
|
||||||
|
|
||||||
def __rrshift__(self, other):
|
def __rrshift__(self, other):
|
||||||
return other >> self.value
|
return other >> self._value
|
||||||
|
|
||||||
def __irshift__(self, other):
|
def __irshift__(self, other):
|
||||||
self.value >>= other
|
self._value >>= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
return self.value & other
|
return self._value & other
|
||||||
|
|
||||||
def __rand__(self, other):
|
def __rand__(self, other):
|
||||||
return other & self.value
|
return other & self._value
|
||||||
|
|
||||||
def __iand__(self, other):
|
def __iand__(self, other):
|
||||||
self.value &= other
|
self._value &= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __xor__(self, other):
|
def __xor__(self, other):
|
||||||
return self.value ^ other
|
return self._value ^ other
|
||||||
|
|
||||||
def __rxor__(self, other):
|
def __rxor__(self, other):
|
||||||
return other ^ self.value
|
return other ^ self._value
|
||||||
|
|
||||||
def __ixor__(self, other):
|
def __ixor__(self, other):
|
||||||
self.value ^= other
|
self._value ^= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return self.value | other
|
return self._value | other
|
||||||
|
|
||||||
def __ror__(self, other):
|
def __ror__(self, other):
|
||||||
return other | self.value
|
return other | self._value
|
||||||
|
|
||||||
def __ior__(self, other):
|
def __ior__(self, other):
|
||||||
self.value |= other
|
self._value |= other
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return -self.value
|
return -self._value
|
||||||
|
|
||||||
def __pos__(self):
|
def __pos__(self):
|
||||||
return +self.value
|
return +self._value
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return abs(self.value)
|
return abs(self._value)
|
||||||
|
|
||||||
def __invert__(self):
|
def __invert__(self):
|
||||||
return ~self.value
|
return ~self._value
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_attribute_or_create(obj, attr, default):
|
||||||
|
try:
|
||||||
|
return getattr(obj, attr)
|
||||||
|
except AttributeError:
|
||||||
|
setattr(obj, attr, default)
|
||||||
|
return getattr(obj, attr)
|
||||||
|
|||||||
22
bonobo/util/python.py
Normal file
22
bonobo/util/python.py
Normal file
@ -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
|
||||||
@ -24,7 +24,8 @@ class WithStatistics:
|
|||||||
return ((name, self.statistics[name]) for name in self.statistics_names)
|
return ((name, self.statistics[name]) for name in self.statistics_names)
|
||||||
|
|
||||||
def get_statistics_as_string(self, *args, **kwargs):
|
def get_statistics_as_string(self, *args, **kwargs):
|
||||||
return ' '.join(('{0}={1}'.format(name, cnt) for name, cnt in self.get_statistics(*args, **kwargs) if cnt > 0))
|
stats = tuple('{0}={1}'.format(name, cnt) for name, cnt in self.get_statistics(*args, **kwargs) if cnt > 0)
|
||||||
|
return (kwargs.get('prefix', '') + ' '.join(stats)) if len(stats) else ''
|
||||||
|
|
||||||
def increment(self, name):
|
def increment(self, name):
|
||||||
self.statistics[name] += 1
|
self.statistics[name] += 1
|
||||||
32
docs/_templates/index.html
vendored
32
docs/_templates/index.html
vendored
@ -3,10 +3,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<div style="border: 2px solid red; font-weight: bold; margin: 1em; padding: 1em">
|
<div style="border: 2px solid red; font-weight: bold; margin: 1em; padding: 1em">
|
||||||
Bonobo is currently <strong>ALPHA</strong> software. That means that the doc is not finished, and that
|
Bonobo is <strong>ALPHA</strong> software. Some APIs will change.
|
||||||
some APIs will change.<br>
|
|
||||||
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.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 style="text-align: center">
|
<h1 style="text-align: center">
|
||||||
@ -16,26 +13,12 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% trans %}
|
{% trans %}
|
||||||
<strong>Bonobo</strong> is a line-by-line data-processing toolkit for python 3.5+ emphasizing simple and
|
<strong>Bonobo</strong> is a line-by-line data-processing toolkit for python 3.5+ (extract-transform-load
|
||||||
atomic data transformations defined using a directed graph of plain old python callables (functions and
|
framework) emphasizing simple and atomic data transformations defined using a directed graph of plain old
|
||||||
generators).
|
python objects (functions, iterables, generators, ...).
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
|
||||||
{% trans %}
|
|
||||||
<strong>Bonobo</strong> is a extract-transform-load framework that uses python code to define transformations.
|
|
||||||
{% endtrans %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{% trans %}
|
|
||||||
<strong>Bonobo</strong> is your own data-monkey army. Tedious and repetitive data-processing incoming? Give
|
|
||||||
it a try!
|
|
||||||
{% endtrans %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<h2 style="margin-bottom: 0">{% trans %}Documentation{% endtrans %}</h2>
|
<h2 style="margin-bottom: 0">{% trans %}Documentation{% endtrans %}</h2>
|
||||||
|
|
||||||
<table class="contentstable">
|
<table class="contentstable">
|
||||||
@ -95,8 +78,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% trans %}
|
{% trans %}
|
||||||
<b>Dependency injection:</b> Abstract the transformation dependencies to easily switch data sources and
|
<b>Service injection:</b> Abstract the transformation dependencies to easily switch data sources and
|
||||||
used libraries, allowing to easily test your transformations.
|
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 %}
|
{% endtrans %}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -107,7 +91,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% trans %}
|
{% trans %}
|
||||||
Work in progress: read the <a href="https://www.bonobo-project.org/roadmap">roadmap</a>.
|
Bonobo is young, and the todo-list is huge. Read the <a href="https://www.bonobo-project.org/roadmap">roadmap</a>.
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ There are a few things that you should know while writing transformations graphs
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
purity
|
purity
|
||||||
|
transformations
|
||||||
services
|
services
|
||||||
|
|
||||||
Third party integrations
|
Third party integrations
|
||||||
|
|||||||
17
docs/guide/plugins.rst
Normal file
17
docs/guide/plugins.rst
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Plugins
|
||||||
|
=======
|
||||||
|
|
||||||
|
|
||||||
|
Graph level plugins
|
||||||
|
:::::::::::::::::::
|
||||||
|
|
||||||
|
|
||||||
|
Node level plugins
|
||||||
|
::::::::::::::::::
|
||||||
|
|
||||||
|
enhancers
|
||||||
|
|
||||||
|
|
||||||
|
node
|
||||||
|
-
|
||||||
|
|
||||||
@ -1,20 +1,18 @@
|
|||||||
Services and dependencies (draft implementation)
|
Services and dependencies
|
||||||
================================================
|
=========================
|
||||||
|
|
||||||
:Status: Draft implementation
|
:Last-Modified: 20 may 2017
|
||||||
:Stability: Alpha
|
|
||||||
:Last-Modified: 28 apr 2017
|
|
||||||
|
|
||||||
Most probably, you'll want to use external systems within your transformations. Those systems may include databases,
|
You'll probably want to use external systems within your transformations. Those systems may include databases, apis
|
||||||
apis (using http, for example), filesystems, etc.
|
(using http, for example), filesystems, etc.
|
||||||
|
|
||||||
You can start by hardcoding those services. That does the job, at first.
|
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:
|
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.
|
* 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
|
* 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 probably want to contigure a different database on a staging environment,
|
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
|
preprod environment or production system. Maybe you have silimar systems for different clients and want to select
|
||||||
the system at runtime. Etc.
|
the system at runtime. Etc.
|
||||||
|
|
||||||
@ -52,10 +50,11 @@ injected to your calls under the parameter name "database".
|
|||||||
Function-based transformations
|
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 <https://github.com/python-bonobo/bonobo/issues/70>`_.
|
||||||
|
|
||||||
Execution
|
Provide implementation at run time
|
||||||
---------
|
----------------------------------
|
||||||
|
|
||||||
Let's see how to execute it:
|
Let's see how to execute it:
|
||||||
|
|
||||||
|
|||||||
93
docs/guide/transformations.rst
Normal file
93
docs/guide/transformations.rst
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. autoclass:: bonobo.config.Method
|
||||||
|
|
||||||
|
ContextProcessors
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. autoclass:: bonobo.config.ContextProcessor
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ Utils
|
|||||||
Count
|
Count
|
||||||
-----
|
-----
|
||||||
|
|
||||||
.. automodule:: bonobo.examples.utils.count
|
.. automodule:: bonobo.examples.nodes.count
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|||||||
@ -1,6 +1,27 @@
|
|||||||
Detailed roadmap
|
Detailed roadmap
|
||||||
================
|
================
|
||||||
|
|
||||||
|
initialize / finalize better than start / stop ?
|
||||||
|
|
||||||
|
Graph and node level plugins
|
||||||
|
::::::::::::::::::::::::::::
|
||||||
|
|
||||||
|
* Enhancers or nide-level plugins
|
||||||
|
* Graph level plugins
|
||||||
|
* Documentation
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
Next...
|
Next...
|
||||||
:::::::
|
:::::::
|
||||||
|
|
||||||
@ -10,8 +31,13 @@ Next...
|
|||||||
* Windows break because of readme encoding. Fix in edgy.
|
* Windows break because of readme encoding. Fix in edgy.
|
||||||
* bonobo init --with sqlalchemy,docker
|
* bonobo init --with sqlalchemy,docker
|
||||||
* logger, vebosity level
|
* logger, vebosity level
|
||||||
* Console run should allow console plugin as a command line argument (or silence it).
|
|
||||||
* ContextProcessors not clean
|
|
||||||
|
External libs that looks good
|
||||||
|
:::::::::::::::::::::::::::::
|
||||||
|
|
||||||
|
* dask.distributed
|
||||||
|
* mediator (event dispatcher)
|
||||||
|
|
||||||
Version 0.3
|
Version 0.3
|
||||||
:::::::::::
|
:::::::::::
|
||||||
|
|||||||
@ -1,23 +1,29 @@
|
|||||||
First steps
|
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
|
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::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
tut01
|
tut01
|
||||||
tut02
|
tut02
|
||||||
|
|
||||||
|
|
||||||
What's next?
|
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/jupyter`: run transformations within jupyter notebooks.
|
||||||
* :doc:`../guide/ext/selenium`: run
|
* :doc:`../guide/ext/selenium`: run
|
||||||
* :doc:`../guide/ext/sqlalchemy`: everything you need to interract with SQL databases.
|
* :doc:`../guide/ext/sqlalchemy`: everything you need to interract with SQL databases.
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,27 @@
|
|||||||
-e .[dev]
|
-e .[dev]
|
||||||
|
|
||||||
|
alabaster==0.7.10
|
||||||
|
astroid==1.5.2
|
||||||
|
babel==2.4.0
|
||||||
|
coverage==4.3.4
|
||||||
|
docutils==0.13.1
|
||||||
|
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-timeout==1.2.0
|
||||||
|
pytest==3.0.7
|
||||||
|
pytz==2017.2
|
||||||
|
requests==2.13.0
|
||||||
|
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
|
||||||
|
|||||||
41
requirements-jupyter.txt
Normal file
41
requirements-jupyter.txt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
-e .[jupyter]
|
||||||
|
|
||||||
|
appnope==0.1.0
|
||||||
|
bleach==2.0.0
|
||||||
|
decorator==4.0.11
|
||||||
|
entrypoints==0.2.2
|
||||||
|
html5lib==0.999999999
|
||||||
|
ipykernel==4.6.1
|
||||||
|
ipython-genutils==0.2.0
|
||||||
|
ipython==6.0.0
|
||||||
|
ipywidgets==6.0.0
|
||||||
|
jedi==0.10.2
|
||||||
|
jinja2==2.9.6
|
||||||
|
jsonschema==2.6.0
|
||||||
|
jupyter-client==5.0.1
|
||||||
|
jupyter-console==5.1.0
|
||||||
|
jupyter-core==4.3.0
|
||||||
|
jupyter==1.0.0
|
||||||
|
markupsafe==1.0
|
||||||
|
mistune==0.7.4
|
||||||
|
nbconvert==5.1.1
|
||||||
|
nbformat==4.3.0
|
||||||
|
notebook==5.0.0
|
||||||
|
pandocfilters==1.4.1
|
||||||
|
pexpect==4.2.1
|
||||||
|
pickleshare==0.7.4
|
||||||
|
prompt-toolkit==1.0.14
|
||||||
|
ptyprocess==0.5.1
|
||||||
|
pygments==2.2.0
|
||||||
|
python-dateutil==2.6.0
|
||||||
|
pyzmq==16.0.2
|
||||||
|
qtconsole==4.3.0
|
||||||
|
simplegeneric==0.8.1
|
||||||
|
six==1.10.0
|
||||||
|
terminado==0.6
|
||||||
|
testpath==0.3
|
||||||
|
tornado==4.5.1
|
||||||
|
traitlets==4.3.2
|
||||||
|
wcwidth==0.1.7
|
||||||
|
webencodings==0.5.1
|
||||||
|
widgetsnbextension==2.0.0
|
||||||
@ -1,7 +1,12 @@
|
|||||||
-e .
|
-e .
|
||||||
|
|
||||||
colorama ==0.3.9
|
appdirs==1.4.3
|
||||||
fs ==2.0.3
|
colorama==0.3.9
|
||||||
psutil ==5.2.2
|
enum34==1.1.6
|
||||||
requests ==2.13.0
|
fs==2.0.3
|
||||||
stevedore ==1.21.0
|
pbr==3.0.0
|
||||||
|
psutil==5.2.2
|
||||||
|
pytz==2017.2
|
||||||
|
requests==2.13.0
|
||||||
|
six==1.10.0
|
||||||
|
stevedore==1.21.0
|
||||||
|
|||||||
69
setup.py
69
setup.py
@ -1,22 +1,11 @@
|
|||||||
# This file is autogenerated by edgy.project code generator.
|
# This file is autogenerated by edgy.project code generator.
|
||||||
# All changes will be overwritten.
|
# All changes will be overwritten.
|
||||||
|
|
||||||
import os
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
from codecs import open
|
||||||
|
from os import path
|
||||||
|
|
||||||
root_dir = os.path.dirname(os.path.abspath(__file__))
|
here = path.abspath(path.dirname(__file__))
|
||||||
|
|
||||||
tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n'))))
|
|
||||||
|
|
||||||
|
|
||||||
def read(filename, flt=None):
|
|
||||||
try:
|
|
||||||
with open(filename) as f:
|
|
||||||
content = f.read().strip()
|
|
||||||
return flt(content) if callable(flt) else content
|
|
||||||
except EnvironmentError:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
# Py3 compatibility hacks, borrowed from IPython.
|
# Py3 compatibility hacks, borrowed from IPython.
|
||||||
try:
|
try:
|
||||||
@ -28,9 +17,18 @@ except NameError:
|
|||||||
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
|
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
|
||||||
|
|
||||||
|
|
||||||
|
# Get the long description from the README file
|
||||||
|
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
# 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())
|
||||||
|
|
||||||
version_ns = {}
|
version_ns = {}
|
||||||
try:
|
try:
|
||||||
execfile(os.path.join(root_dir, 'bonobo/_version.py'), version_ns)
|
execfile(path.join(here, 'bonobo/_version.py'), version_ns)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
version = 'dev'
|
version = 'dev'
|
||||||
else:
|
else:
|
||||||
@ -38,44 +36,41 @@ else:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='bonobo',
|
name='bonobo',
|
||||||
description=
|
description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
|
||||||
('Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
|
'python 3.5+.'),
|
||||||
'python 3.5+.'),
|
|
||||||
license='Apache License, Version 2.0',
|
license='Apache License, Version 2.0',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.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'
|
||||||
'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0'
|
|
||||||
],
|
],
|
||||||
version=version,
|
version=version,
|
||||||
long_description=read('README.rst'),
|
long_description=long_description,
|
||||||
classifiers=read('classifiers.txt', tolines),
|
classifiers=classifiers,
|
||||||
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
|
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [
|
data_files=[
|
||||||
'bonobo/ext/jupyter/static/extension.js',
|
(
|
||||||
'bonobo/ext/jupyter/static/index.js',
|
'share/jupyter/nbextensions/bonobo-jupyter', [
|
||||||
'bonobo/ext/jupyter/static/index.js.map'
|
'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js',
|
||||||
])],
|
'bonobo/ext/jupyter/static/index.js.map'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev': [
|
'dev': [
|
||||||
'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4',
|
'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx',
|
||||||
'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx',
|
|
||||||
'sphinx_rtd_theme', 'yapf'
|
'sphinx_rtd_theme', 'yapf'
|
||||||
],
|
],
|
||||||
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']
|
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']
|
||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'bonobo.commands': [
|
'bonobo.commands': [
|
||||||
'init = bonobo.commands.init:register',
|
'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register',
|
||||||
'run = bonobo.commands.run:register',
|
|
||||||
'version = bonobo.commands.version:register'
|
'version = bonobo.commands.version:register'
|
||||||
],
|
],
|
||||||
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
|
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
|
||||||
'edgy.project.features':
|
'edgy.project.features': ['bonobo = '
|
||||||
['bonobo = '
|
'bonobo.ext.edgy.project.feature:BonoboFeature']
|
||||||
'bonobo.ext.edgy.project.feature:BonoboFeature']
|
|
||||||
},
|
},
|
||||||
url='https://www.bonobo-project.org/',
|
url='https://www.bonobo-project.org/',
|
||||||
download_url=
|
download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version),
|
||||||
'https://github.com/python-bonobo/bonobo/tarball/{version}'.format(
|
)
|
||||||
version=version), )
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ def test_write_csv_to_file(tmpdir):
|
|||||||
writer = CsvWriter(path=filename)
|
writer = CsvWriter(path=filename)
|
||||||
context = NodeExecutionContext(writer, services={'fs': fs})
|
context = NodeExecutionContext(writer, services={'fs': fs})
|
||||||
|
|
||||||
context.recv(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END)
|
context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END)
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.step()
|
context.step()
|
||||||
@ -34,7 +34,7 @@ def test_read_csv_from_file(tmpdir):
|
|||||||
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.recv(BEGIN, Bag(), END)
|
context.write(BEGIN, Bag(), END)
|
||||||
context.step()
|
context.step()
|
||||||
context.stop()
|
context.stop()
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ def test_file_writer_in_context(tmpdir, lines, output):
|
|||||||
context = NodeExecutionContext(writer, services={'fs': fs})
|
context = NodeExecutionContext(writer, services={'fs': fs})
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.recv(BEGIN, *map(Bag, lines), END)
|
context.write(BEGIN, *map(Bag, lines), END)
|
||||||
for _ in range(len(lines)):
|
for _ in range(len(lines)):
|
||||||
context.step()
|
context.step()
|
||||||
context.stop()
|
context.stop()
|
||||||
@ -48,7 +48,7 @@ def test_file_reader_in_context(tmpdir):
|
|||||||
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.recv(BEGIN, Bag(), END)
|
context.write(BEGIN, Bag(), END)
|
||||||
context.step()
|
context.step()
|
||||||
context.stop()
|
context.stop()
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def test_write_json_to_file(tmpdir):
|
|||||||
context = NodeExecutionContext(writer, services={'fs': fs})
|
context = NodeExecutionContext(writer, services={'fs': fs})
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.recv(BEGIN, Bag({'foo': 'bar'}), END)
|
context.write(BEGIN, Bag({'foo': 'bar'}), END)
|
||||||
context.step()
|
context.step()
|
||||||
context.stop()
|
context.stop()
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ def test_read_json_from_file(tmpdir):
|
|||||||
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
|
||||||
|
|
||||||
context.start()
|
context.start()
|
||||||
context.recv(BEGIN, Bag(), END)
|
context.write(BEGIN, Bag(), END)
|
||||||
context.step()
|
context.step()
|
||||||
context.stop()
|
context.stop()
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import pickle
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from bonobo import Bag
|
from bonobo import Bag
|
||||||
from bonobo.constants import INHERIT_INPUT
|
from bonobo.constants import INHERIT_INPUT
|
||||||
|
from bonobo.structs import Token
|
||||||
|
|
||||||
args = ('foo', 'bar', )
|
args = ('foo', 'bar', )
|
||||||
kwargs = dict(acme='corp')
|
kwargs = dict(acme='corp')
|
||||||
@ -59,6 +61,37 @@ def test_inherit():
|
|||||||
assert bag4.flags is ()
|
assert bag4.flags is ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pickle():
|
||||||
|
bag1 = Bag('a', a=1)
|
||||||
|
bag2 = Bag.inherit('b', b=2, _parent=bag1)
|
||||||
|
bag3 = bag1.extend('c', c=3)
|
||||||
|
bag4 = Bag('d', d=4)
|
||||||
|
|
||||||
|
# XXX todo this probably won't work with inheriting bags if parent is not there anymore? maybe that's not true
|
||||||
|
# because the parent may be in the serialization output but we need to verify this assertion.
|
||||||
|
|
||||||
|
for bag in bag1, bag2, bag3, bag4:
|
||||||
|
pickled = pickle.dumps(bag)
|
||||||
|
unpickled = pickle.loads(pickled)
|
||||||
|
assert unpickled == bag
|
||||||
|
|
||||||
|
|
||||||
|
def test_eq_operator():
|
||||||
|
assert Bag('foo') == Bag('foo')
|
||||||
|
assert Bag('foo') != Bag('bar')
|
||||||
|
assert Bag('foo') is not Bag('foo')
|
||||||
|
assert Bag('foo') != Token('foo')
|
||||||
|
assert Token('foo') != Bag('foo')
|
||||||
|
|
||||||
|
|
||||||
def test_repr():
|
def test_repr():
|
||||||
bag = Bag('a', a=1)
|
bag = Bag('a', a=1)
|
||||||
assert repr(bag) == "<Bag ('a', a=1)>"
|
assert repr(bag) == "<Bag ('a', a=1)>"
|
||||||
|
|
||||||
|
|
||||||
|
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']
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from unittest.mock import sentinel
|
||||||
|
|
||||||
from bonobo.constants import BEGIN
|
from bonobo.constants import BEGIN
|
||||||
from bonobo.structs import Graph
|
from bonobo.structs import Graph
|
||||||
|
|
||||||
@ -41,3 +43,31 @@ def test_graph_add_chain():
|
|||||||
g.add_chain(identity, identity, identity)
|
g.add_chain(identity, identity, identity)
|
||||||
assert len(g.nodes) == 3
|
assert len(g.nodes) == 3
|
||||||
assert len(g.outputs_of(BEGIN)) == 1
|
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[-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
|
||||||
|
|||||||
@ -19,8 +19,8 @@ from queue import Empty
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import BEGIN, END
|
||||||
from bonobo.core.inputs import Input
|
|
||||||
from bonobo.errors import InactiveWritableError, InactiveReadableError
|
from bonobo.errors import InactiveWritableError, InactiveReadableError
|
||||||
|
from bonobo.structs.inputs import Input
|
||||||
|
|
||||||
|
|
||||||
def test_input_runlevels():
|
def test_input_runlevels():
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import bonobo
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import bonobo
|
||||||
from bonobo.config.processors import ContextCurrifier
|
from bonobo.config.processors import ContextCurrifier
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.constants import NOT_MODIFIED
|
||||||
|
|
||||||
@ -12,12 +13,9 @@ def test_count():
|
|||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
|
|
||||||
currified = ContextCurrifier(bonobo.count)
|
with ContextCurrifier(bonobo.count).as_contextmanager(context) as stack:
|
||||||
currified.setup(context)
|
for i in range(42):
|
||||||
|
stack()
|
||||||
for i in range(42):
|
|
||||||
currified()
|
|
||||||
currified.teardown()
|
|
||||||
|
|
||||||
assert len(context.method_calls) == 1
|
assert len(context.method_calls) == 1
|
||||||
bag = context.send.call_args[0][0]
|
bag = context.send.call_args[0][0]
|
||||||
@ -32,18 +30,32 @@ def test_identity():
|
|||||||
|
|
||||||
|
|
||||||
def test_limit():
|
def test_limit():
|
||||||
limit = bonobo.Limit(2)
|
context, results = MagicMock(), []
|
||||||
results = []
|
|
||||||
for i in range(42):
|
with ContextCurrifier(bonobo.Limit(2)).as_contextmanager(context) as stack:
|
||||||
results += list(limit())
|
for i in range(42):
|
||||||
|
results += list(stack())
|
||||||
|
|
||||||
assert results == [NOT_MODIFIED] * 2
|
assert results == [NOT_MODIFIED] * 2
|
||||||
|
|
||||||
|
|
||||||
def test_limit_not_there():
|
def test_limit_not_there():
|
||||||
limit = bonobo.Limit(42)
|
context, results = MagicMock(), []
|
||||||
results = []
|
|
||||||
for i in range(10):
|
with ContextCurrifier(bonobo.Limit(42)).as_contextmanager(context) as stack:
|
||||||
results += list(limit())
|
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
|
assert results == [NOT_MODIFIED] * 10
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
84
tests/test_config_method.py
Normal file
84
tests/test_config_method.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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, ))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_define_with_argument():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def concrete_handler(*args, **kwargs):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_define_with_inheritance():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class Inheriting(MethodBasedConfigurable):
|
||||||
|
def handler(self, *args, **kwargs):
|
||||||
|
calls.append((args, kwargs, ))
|
||||||
|
|
||||||
|
t = Inheriting('foo', bar='baz')
|
||||||
|
assert callable(t.handler)
|
||||||
|
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, ))
|
||||||
|
|
||||||
|
assert callable(Concrete.handler)
|
||||||
|
t = Concrete('foo', bar='baz')
|
||||||
|
assert callable(t.handler)
|
||||||
|
assert len(calls) == 0
|
||||||
|
t()
|
||||||
|
assert len(calls) == 1
|
||||||
@ -59,11 +59,23 @@ def test_simple_execution_context():
|
|||||||
assert ctx[i].wrapped is node
|
assert ctx[i].wrapped is node
|
||||||
|
|
||||||
assert not ctx.alive
|
assert not ctx.alive
|
||||||
|
assert not ctx.started
|
||||||
|
assert not ctx.stopped
|
||||||
|
|
||||||
ctx.recv(BEGIN, Bag(), END)
|
ctx.recv(BEGIN, Bag(), END)
|
||||||
|
|
||||||
assert not ctx.alive
|
assert not ctx.alive
|
||||||
|
assert not ctx.started
|
||||||
|
assert not ctx.stopped
|
||||||
|
|
||||||
ctx.start()
|
ctx.start()
|
||||||
|
|
||||||
assert ctx.alive
|
assert ctx.alive
|
||||||
|
assert ctx.started
|
||||||
|
assert not ctx.stopped
|
||||||
|
|
||||||
|
ctx.stop()
|
||||||
|
|
||||||
|
assert not ctx.alive
|
||||||
|
assert ctx.started
|
||||||
|
assert ctx.stopped
|
||||||
|
|||||||
1
tests/util/requireable/dummy.py
Normal file
1
tests/util/requireable/dummy.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
foo = 'bar'
|
||||||
23
tests/util/test_compat.py
Normal file
23
tests/util/test_compat.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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()
|
||||||
@ -35,6 +35,7 @@ def test_wrapper_name():
|
|||||||
|
|
||||||
def test_valueholder():
|
def test_valueholder():
|
||||||
x = ValueHolder(42)
|
x = ValueHolder(42)
|
||||||
|
|
||||||
assert x == 42
|
assert x == 42
|
||||||
x += 1
|
x += 1
|
||||||
assert x == 43
|
assert x == 43
|
||||||
|
|||||||
6
tests/util/test_python.py
Normal file
6
tests/util/test_python.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from bonobo.util.python import require
|
||||||
|
|
||||||
|
|
||||||
|
def test_require():
|
||||||
|
dummy = require('requireable.dummy')
|
||||||
|
assert dummy.foo == 'bar'
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from bonobo.core.statistics import WithStatistics
|
from bonobo.util.statistics import WithStatistics
|
||||||
|
|
||||||
|
|
||||||
class MyThingWithStats(WithStatistics):
|
class MyThingWithStats(WithStatistics):
|
||||||
Reference in New Issue
Block a user