Merge pull request #132 from hartym/develop

Refactoring of configurables.
This commit is contained in:
Romain Dorgueil
2017-07-05 13:13:39 +02:00
committed by GitHub
54 changed files with 1305 additions and 689 deletions

9
.gitignore vendored
View File

@ -2,6 +2,7 @@
*,cover *,cover
*.egg *.egg
*.egg-info/ *.egg-info/
*.iml
*.log *.log
*.manifest *.manifest
*.mo *.mo
@ -20,25 +21,17 @@
.installed.cfg .installed.cfg
.ipynb_checkpoints .ipynb_checkpoints
.python-version .python-version
.tox/
.webassets-cache
/.idea /.idea
/.release /.release
/bonobo.iml
/bonobo/examples/work_in_progress/ /bonobo/examples/work_in_progress/
/bonobo/ext/jupyter/js/node_modules/ /bonobo/ext/jupyter/js/node_modules/
/build/ /build/
/coverage.xml /coverage.xml
/develop-eggs/
/dist/ /dist/
/docs/_build/ /docs/_build/
/downloads/
/eggs/ /eggs/
/examples/private /examples/private
/htmlcov/
/sdist/ /sdist/
/tags /tags
celerybeat-schedule
parts/
pip-delete-this-directory.txt pip-delete-this-directory.txt
pip-log.txt pip-log.txt

View File

@ -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-06-10 15:15:34.093885 # Updated at 2017-07-04 10:50:55.775681
PACKAGE ?= bonobo PACKAGE ?= bonobo
PYTHON ?= $(shell which python) PYTHON ?= $(shell which python)
@ -18,7 +18,7 @@ SPHINX_BUILD ?= $(PYTHON_DIRNAME)/sphinx-build
SPHINX_OPTIONS ?= SPHINX_OPTIONS ?=
SPHINX_SOURCEDIR ?= docs SPHINX_SOURCEDIR ?= docs
SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build
YAPF ?= $(PYTHON_DIRNAME)/yapf YAPF ?= $(PYTHON) -m yapf
YAPF_OPTIONS ?= -rip YAPF_OPTIONS ?= -rip
VERSION ?= $(shell git describe 2>/dev/null || echo dev) VERSION ?= $(shell git describe 2>/dev/null || echo dev)

View File

@ -1,6 +1,6 @@
from bonobo.structs import Bag, Graph, Token from bonobo.structs import Bag, Graph, Token
from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \ from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \
PrettyPrinter, PickleWriter, PickleReader, Tee, count, identity, noop, pprint PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop
from bonobo.strategies import create_strategy from bonobo.strategies import create_strategy
from bonobo.util.objects import get_name from bonobo.util.objects import get_name
@ -45,7 +45,7 @@ def run(graph, strategy=None, plugins=None, services=None):
from bonobo import settings from bonobo import settings
settings.check() settings.check()
if not settings.QUIET: # pragma: no cover if not settings.QUIET.get(): # pragma: no cover
if _is_interactive_console(): if _is_interactive_console():
from bonobo.ext.console import ConsoleOutputPlugin from bonobo.ext.console import ConsoleOutputPlugin
if ConsoleOutputPlugin not in plugins: if ConsoleOutputPlugin not in plugins:
@ -68,7 +68,7 @@ register_api(create_strategy)
# Shortcut to filesystem2's open_fs, that we make available there for convenience. # Shortcut to filesystem2's open_fs, that we make available there for convenience.
@register_api @register_api
def open_fs(fs_url, *args, **kwargs): def open_fs(fs_url=None, *args, **kwargs):
""" """
Wraps :func:`fs.open_fs` function with a few candies. Wraps :func:`fs.open_fs` function with a few candies.
@ -83,6 +83,10 @@ def open_fs(fs_url, *args, **kwargs):
""" """
from fs import open_fs as _open_fs from fs import open_fs as _open_fs
from os.path import expanduser from os.path import expanduser
from os import getcwd
if fs_url is None:
fs_url = getcwd()
return _open_fs(expanduser(str(fs_url)), *args, **kwargs) return _open_fs(expanduser(str(fs_url)), *args, **kwargs)
@ -100,11 +104,11 @@ register_api_group(
PrettyPrinter, PrettyPrinter,
PickleReader, PickleReader,
PickleWriter, PickleWriter,
RateLimited,
Tee, Tee,
count, count,
identity, identity,
noop, noop,
pprint,
) )

View File

@ -1 +1 @@
__version__ = '0.4.0' __version__ = '0.4.3'

View File

@ -27,9 +27,9 @@ def entrypoint(args=None):
args = parser.parse_args(args).__dict__ args = parser.parse_args(args).__dict__
if args.pop('debug', False): if args.pop('debug', False):
settings.DEBUG = True settings.DEBUG.set(True)
settings.LOGGING_LEVEL = logging.DEBUG settings.LOGGING_LEVEL.set(logging.DEBUG)
logging.set_level(settings.LOGGING_LEVEL) logging.set_level(settings.LOGGING_LEVEL.get())
logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args))
commands[args.pop('command')](**args) commands[args.pop('command')](**args)

View File

@ -31,10 +31,10 @@ def execute(filename, module, install=False, quiet=False, verbose=False):
from bonobo import Graph, run, settings from bonobo import Graph, run, settings
if quiet: if quiet:
settings.QUIET = True settings.QUIET.set(True)
if verbose: if verbose:
settings.DEBUG = True settings.DEBUG.set(True)
if filename: if filename:
if os.path.isdir(filename): if os.path.isdir(filename):

View File

@ -1,9 +1,9 @@
from bonobo.config.configurables import Configurable from bonobo.config.configurables import Configurable
from bonobo.config.options import Option, Method from bonobo.config.options import Method, Option
from bonobo.config.processors import ContextProcessor from bonobo.config.processors import ContextProcessor
from bonobo.config.services import Container, Service, Exclusive from bonobo.config.services import Container, Exclusive, Service, requires
# bonobo.config public programming interface # Bonobo's Config API
__all__ = [ __all__ = [
'Configurable', 'Configurable',
'Container', 'Container',
@ -12,4 +12,5 @@ __all__ = [
'Method', 'Method',
'Option', 'Option',
'Service', 'Service',
'requires',
] ]

View File

@ -1,12 +1,14 @@
from bonobo.config.options import Method, Option from bonobo.util.inspect import isoption, iscontextprocessor
from bonobo.config.processors import ContextProcessor from bonobo.errors import AbstractError
from bonobo.errors import ConfigurationError, AbstractError from bonobo.util.collections import sortedlist
__all__ = [ __all__ = [
'Configurable', 'Configurable',
'Option', 'Option',
] ]
get_creation_counter = lambda v: v._creation_counter
class ConfigurableMeta(type): class ConfigurableMeta(type):
""" """
@ -15,36 +17,78 @@ class ConfigurableMeta(type):
def __init__(cls, what, bases=None, dict=None): def __init__(cls, what, bases=None, dict=None):
super().__init__(what, bases, dict) super().__init__(what, bases, dict)
cls.__options__ = {}
cls.__positional_options__ = [] cls.__processors = sortedlist()
cls.__processors__ = [] cls.__methods = sortedlist()
cls.__wrappable__ = None cls.__options = sortedlist()
cls.__names = set()
# cls.__kwoptions = []
for typ in cls.__mro__: for typ in cls.__mro__:
for name, value in typ.__dict__.items(): for name, value in filter(lambda x: isoption(x[1]), typ.__dict__.items()):
if isinstance(value, Option): if iscontextprocessor(value):
if isinstance(value, ContextProcessor): cls.__processors.insort((value._creation_counter, value))
cls.__processors__.append(value) continue
else:
if not value.name:
value.name = name
if isinstance(value, Method): if not value.name:
if cls.__wrappable__: value.name = name
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.__names:
cls.__options__[name] = value cls.__names.add(name)
cls.__options.insort((not value.positional, value._creation_counter, name, value))
if value.positional: @property
cls.__positional_options__.append(name) def __options__(cls):
return ((name, option) for _, _, name, option in cls.__options)
# This can be done before, more efficiently. Not so bad neither as this is only done at type() creation time @property
# (aka class Xxx(...) time) and there should not be hundreds of processors. Still not very elegant. def __options_dict__(cls):
cls.__processors__ = sorted(cls.__processors__, key=lambda v: v._creation_counter) return dict(cls.__options__)
@property
def __processors__(cls):
return (processor for _, processor in cls.__processors)
def __repr__(self):
return ' '.join(('<Configurable', super(ConfigurableMeta, self).__repr__().split(' ', 1)[1], ))
try:
import _functools
except:
import functools
PartiallyConfigured = functools.partial
else:
class PartiallyConfigured(_functools.partial):
@property # TODO XXX cache this shit
def _options_values(self):
""" Simulate option values for partially configured objects. """
try:
return self.__options_values
except AttributeError:
self.__options_values = {**self.keywords}
position = 0
for name, option in self.func.__options__:
if not option.positional:
break # no positional left
if name in self.keywords:
continue # already fulfilled
self.__options_values[name] = self.args[position] if len(self.args) >= position + 1 else None
position += 1
return self.__options_values
def __getattr__(self, item):
_dict = self.func.__options_dict__
if item in _dict:
return _dict[item].__get__(self, self.func)
return getattr(self.func, item)
class Configurable(metaclass=ConfigurableMeta): class Configurable(metaclass=ConfigurableMeta):
@ -54,61 +98,106 @@ class Configurable(metaclass=ConfigurableMeta):
""" """
def __new__(cls, *args, **kwargs): def __new__(cls, *args, _final=False, **kwargs):
if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'): """
return type(args[0].__name__, (cls, ), {cls.__wrappable__: args[0]}) Custom instance builder. If not all options are fulfilled, will return a :class:`PartiallyConfigured` instance
which is just a :class:`functools.partial` object that behaves like a :class:`Configurable` instance.
return super(Configurable, cls).__new__(cls) The special `_final` argument can be used to force final instance to be created, or an error raised if options
are missing.
def __init__(self, *args, **kwargs):
super().__init__()
# initialize option's value dictionary, used by descriptor implementation (see Option).
self.__options_values__ = {}
:param args:
:param _final: bool
:param kwargs:
:return: Configurable or PartiallyConfigured
"""
options = tuple(cls.__options__)
# compute missing options, given the kwargs. # compute missing options, given the kwargs.
missing = set() missing = set()
for name, option in type(self).__options__.items(): for name, option in options:
if option.required and not option.name in kwargs: if option.required and not option.name in kwargs:
missing.add(name) missing.add(name)
# 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 name, option in options:
if len(args) <= position: if not option.positional:
break break # option orders make all positional options first, job done.
kwargs[positional_option] = args[position]
position += 1
if positional_option in missing:
missing.remove(positional_option)
# complain if there are still missing options. if not isoption(getattr(cls, name)):
if len(missing): missing.remove(name)
raise TypeError( continue
'{}() missing {} required option{}: {}.'.format(
type(self).__name__, if len(args) <= position:
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing))) break # no more positional arguments given.
)
) position += 1
if name in missing:
missing.remove(name)
# complain if there is more options than possible. # complain if there is more options than possible.
extraneous = set(kwargs.keys()) - set(type(self).__options__.keys()) extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set())
if len(extraneous): if len(extraneous):
raise TypeError( raise TypeError(
'{}() got {} unexpected option{}: {}.'.format( '{}() got {} unexpected option{}: {}.'.format(
type(self).__name__, cls.__name__,
len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous))) len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
) )
) )
# missing options? we'll return a partial instance to finish the work later, unless we're required to be
# "final".
if len(missing):
if _final:
raise TypeError(
'{}() missing {} required option{}: {}.'.format(
cls.__name__,
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
)
)
return PartiallyConfigured(cls, *args, **kwargs)
return super(Configurable, cls).__new__(cls)
def __init__(self, *args, **kwargs):
# initialize option's value dictionary, used by descriptor implementation (see Option).
self._options_values = {**kwargs}
# set option values. # set option values.
for name, value in kwargs.items(): for name, value in kwargs.items():
setattr(self, name, value) setattr(self, name, value)
position = 0
for name, option in self.__options__:
if not option.positional:
break # option orders make all positional options first
# value was overriden? Skip.
maybe_value = getattr(type(self), name)
if not isoption(maybe_value):
continue
if len(args) <= position:
break
if name in self._options_values:
raise ValueError('Already got a value for option {}'.format(name))
setattr(self, name, args[position])
position += 1
def __call__(self, *args, **kwargs): 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. """ 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) return self.call(*args, **kwargs)
@property
def __options__(self):
return type(self).__options__
@property
def __processors__(self):
return type(self).__processors__
def call(self, *args, **kwargs): def call(self, *args, **kwargs):
raise AbstractError('Not implemented.') raise AbstractError('Not implemented.')

View File

@ -1,3 +1,6 @@
from bonobo.util.inspect import istype
class Option: class Option:
""" """
An Option is a descriptor for Configurable's parameters. An Option is a descriptor for Configurable's parameters.
@ -14,7 +17,9 @@ class Option:
If an option is required, an error will be raised if no value is provided (at runtime). If it is not, option 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. will have the default value if user does not override it at runtime.
(default: False) Ignored if a default is provided, meaning that the option cannot be required.
(default: True)
.. attribute:: positional .. attribute:: positional
@ -48,10 +53,10 @@ class Option:
_creation_counter = 0 _creation_counter = 0
def __init__(self, type=None, *, required=False, positional=False, default=None): def __init__(self, type=None, *, required=True, positional=False, default=None):
self.name = None self.name = None
self.type = type self.type = type
self.required = required self.required = required if default is None else False
self.positional = positional self.positional = positional
self.default = default self.default = default
@ -60,12 +65,27 @@ class Option:
Option._creation_counter += 1 Option._creation_counter += 1
def __get__(self, inst, typ): def __get__(self, inst, typ):
if not self.name in inst.__options_values__: # XXX If we call this on the type, then either return overriden value or ... ???
inst.__options_values__[self.name] = self.get_default() if inst is None:
return inst.__options_values__[self.name] return vars(type).get(self.name, self)
if not self.name in inst._options_values:
inst._options_values[self.name] = self.get_default()
return inst._options_values[self.name]
def __set__(self, inst, value): def __set__(self, inst, value):
inst.__options_values__[self.name] = self.clean(value) inst._options_values[self.name] = self.clean(value)
def __repr__(self):
return '<{positional}{typename} {type}{name} default={default!r}{required}>'.format(
typename=type(self).__name__,
type='({})'.format(self.type) if istype(self.type) else '',
name=self.name,
positional='*' if self.positional else '**',
default=self.default,
required=' (required)' if self.required else '',
)
def clean(self, value): def clean(self, value):
return self.type(value) if self.type else value return self.type(value) if self.type else value
@ -105,20 +125,17 @@ class Method(Option):
""" """
def __init__(self): def __init__(self, *, required=True, positional=True):
super().__init__(None, required=False) super().__init__(None, required=required, positional=positional)
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): def __set__(self, inst, value):
if isinstance(value, str):
raise ValueError('should be callable')
inst.__options_values__[self.name] = self.type(value) if self.type else value
def clean(self, value):
if not hasattr(value, '__call__'): if not hasattr(value, '__call__'):
raise ValueError('{} value must be callable.'.format(type(self).__name__)) raise TypeError(
return value 'Option of type {!r} is expecting a callable value, got {!r} object (which is not).'.
format(type(self).__name__, type(value).__name__)
)
inst._options_values[self.name] = self.type(value) if self.type else value
def __call__(self, *args, **kwargs):
# only here to trick IDEs into thinking this is callable.
raise NotImplementedError('You cannot call the descriptor')

View File

@ -74,8 +74,7 @@ class ContextCurrifier:
def __init__(self, wrapped, *initial_context): def __init__(self, wrapped, *initial_context):
self.wrapped = wrapped self.wrapped = wrapped
self.context = tuple(initial_context) self.context = tuple(initial_context)
self._stack = [] self._stack, self._stack_values = None, None
self._stack_values = []
def __iter__(self): def __iter__(self):
yield from self.wrapped yield from self.wrapped
@ -86,8 +85,10 @@ class ContextCurrifier:
return self.wrapped(*self.context, *args, **kwargs) return self.wrapped(*self.context, *args, **kwargs)
def setup(self, *context): def setup(self, *context):
if len(self._stack): if self._stack is not None:
raise RuntimeError('Cannot setup context currification twice.') raise RuntimeError('Cannot setup context currification twice.')
self._stack, self._stack_values = list(), list()
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)
@ -97,7 +98,7 @@ class ContextCurrifier:
self._stack.append(_processed) self._stack.append(_processed)
def teardown(self): def teardown(self):
while len(self._stack): while self._stack:
processor = self._stack.pop() processor = self._stack.pop()
try: try:
# todo yield from ? how to ? # todo yield from ? how to ?
@ -108,6 +109,7 @@ class ContextCurrifier:
else: else:
# 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.')
self._stack, self._stack_values = None, None
@contextmanager @contextmanager
def as_contextmanager(self, *context): def as_contextmanager(self, *context):

View File

@ -53,10 +53,14 @@ class Service(Option):
super().__init__(str, required=False, default=name) super().__init__(str, required=False, default=name)
def __set__(self, inst, value): def __set__(self, inst, value):
inst.__options_values__[self.name] = validate_service_name(value) inst._options_values[self.name] = validate_service_name(value)
def resolve(self, inst, services): def resolve(self, inst, services):
return services.get(getattr(inst, self.name)) try:
name = getattr(inst, self.name)
except AttributeError:
name = self.name
return services.get(name)
class Container(dict): class Container(dict):
@ -71,7 +75,7 @@ class Container(dict):
def args_for(self, mixed): def args_for(self, mixed):
try: try:
options = mixed.__options__ options = dict(mixed.__options__)
except AttributeError: except AttributeError:
options = {} options = {}
@ -126,3 +130,19 @@ class Exclusive(ContextDecorator):
def __exit__(self, *exc): def __exit__(self, *exc):
self.get_lock().release() self.get_lock().release()
def requires(*service_names):
def decorate(mixed):
try:
options = mixed.__options__
except AttributeError:
mixed.__options__ = options = {}
for service_name in service_names:
service = Service(service_name)
service.name = service_name
options[service_name] = service
return mixed
return decorate

View File

@ -58,5 +58,22 @@ class ConfigurationError(Exception):
pass pass
class MissingServiceImplementationError(KeyError): class UnrecoverableError(Exception):
"""Flag for errors that must interrupt the workflow, either because they will happen for sure on each node run, or
because you know that your transformation has no point continuing runnning after a bad event."""
class UnrecoverableValueError(UnrecoverableError, ValueError):
pass
class UnrecoverableRuntimeError(UnrecoverableError, RuntimeError):
pass
class UnrecoverableNotImplementedError(UnrecoverableError, NotImplementedError):
pass
class MissingServiceImplementationError(UnrecoverableError, KeyError):
pass pass

View File

@ -1,182 +1,182 @@
{"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France", {"les montparnos": "65 boulevard Pasteur, 75015 Paris, France",
"les montparnos": "65 boulevard Pasteur, 75015 Paris, France", "Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France",
"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France", "Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France",
"Le Felteu": "1 rue Pecquay, 75004 Paris, France", "Le Bellerive": "71 quai de Seine, 75019 Paris, France",
"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France",
"O q de poule": "53 rue du ruisseau, 75018 Paris, France", "O q de poule": "53 rue du ruisseau, 75018 Paris, France",
"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France",
"Le chantereine": "51 Rue Victoire, 75009 Paris, France", "Le chantereine": "51 Rue Victoire, 75009 Paris, France",
"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France", "Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France",
"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France",
"Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France",
"La Renaissance": "112 Rue Championnet, 75018 Paris, France",
"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France", "Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France",
"Le Sully": "6 Bd henri IV, 75004 Paris, France", "La Bauloise": "36 rue du hameau, 75015 Paris, France",
"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France", "Le Dellac": "14 rue Rougemont, 75009 Paris, France",
"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France",
"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France",
"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France",
"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France",
"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France",
"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France",
"Le Square": "31 rue Saint-Dominique, 75007 Paris, France",
"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France",
"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France", "Le Bosquet": "46 avenue Bosquet, 75007 Paris, France",
"Le Sully": "6 Bd henri IV, 75004 Paris, France",
"Le Felteu": "1 rue Pecquay, 75004 Paris, France",
"Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France", "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", "D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France",
"Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France", "Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France",
"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France",
"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France",
"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France",
"La Renaissance": "112 Rue Championnet, 75018 Paris, France",
"Le Square": "31 rue Saint-Dominique, 75007 Paris, France",
"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France",
"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France",
"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France",
"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France",
"Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France", "Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France",
"Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France", "Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France",
"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France", "La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France",
"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France", "Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France",
"Le Bellerive": "71 quai de Seine, 75019 Paris, France", "Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France",
"La Bauloise": "36 rue du hameau, 75015 Paris, France", "Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France",
"Le Dellac": "14 rue Rougemont, 75009 Paris, France", "Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France",
"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France",
"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France",
"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France",
"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France",
"l'Usine": "1 rue d'Avron, 75020 Paris, France",
"La Bricole": "52 rue Liebniz, 75018 Paris, France",
"le ronsard": "place maubert, 75005 Paris, France",
"Face Bar": "82 rue des archives, 75003 Paris, France",
"American Kitchen": "49 rue bichat, 75010 Paris, France",
"La Marine": "55 bis quai de valmy, 75010 Paris, France",
"Le Bloc": "21 avenue Brochant, 75017 Paris, France",
"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France",
"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France",
"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France",
"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France",
"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France",
"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France",
"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France",
"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France",
"Le Supercoin": "3, rue Baudelique, 75018 Paris, France",
"Populettes": "86 bis rue Riquet, 75018 Paris, France",
"Au bon coin": "49 rue des Cloys, 75018 Paris, France",
"Le Couvent": "69 rue Broca, 75013 Paris, France",
"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France",
"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France",
"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France",
"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France",
"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France",
"La Perle": "78 rue vieille du temple, 75003 Paris, France",
"Le Descartes": "1 rue Thouin, 75005 Paris, France",
"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France",
"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France",
"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France",
"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France",
"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France",
"Au panini de la place": "47 rue Belgrand, 75020 Paris, France",
"Le Village": "182 rue de Courcelles, 75017 Paris, France",
"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France",
"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France",
"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France",
"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France",
"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France",
"Br\u00fblerie San Jos\u00e9": "30 rue des Petits-Champs, 75002 Paris, France", "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",
"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France",
"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France",
"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France",
"Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France", "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",
"Le Biz": "18 rue Favart, 75002 Paris, France",
"L'Olive": "8 rue L'Olive, 75018 Paris, France", "L'Olive": "8 rue L'Olive, 75018 Paris, France",
"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France", "Le Biz": "18 rue Favart, 75002 Paris, France",
"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France", "Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France",
"La Marine": "55 bis quai de valmy, 75010 Paris, France", "Le General Beuret": "9 Place du General Beuret, 75015 Paris, France",
"American Kitchen": "49 rue bichat, 75010 Paris, France",
"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France",
"Face Bar": "82 rue des archives, 75003 Paris, France",
"Le Bloc": "21 avenue Brochant, 75017 Paris, France",
"La Bricole": "52 rue Liebniz, 75018 Paris, France",
"le ronsard": "place maubert, 75005 Paris, France",
"l'Usine": "1 rue d'Avron, 75020 Paris, France",
"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France",
"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France",
"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France",
"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France",
"Le Germinal": "95 avenue Emile Zola, 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",
"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France", "Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France",
"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France",
"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France",
"Le Supercoin": "3, rue Baudelique, 75018 Paris, France",
"Populettes": "86 bis rue Riquet, 75018 Paris, France",
"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France",
"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France",
"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France",
"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France",
"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France",
"Au bon coin": "49 rue des Cloys, 75018 Paris, France",
"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France",
"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France",
"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France",
"Le Couvent": "69 rue Broca, 75013 Paris, France",
"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France",
"La Perle": "78 rue vieille du temple, 75003 Paris, France",
"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France",
"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France",
"Le Descartes": "1 rue Thouin, 75005 Paris, France",
"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France",
"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France",
"Au panini de la place": "47 rue Belgrand, 75020 Paris, France",
"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France",
"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France",
"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France",
"Le Village": "182 rue de Courcelles, 75017 Paris, France",
"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France",
"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France",
"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France",
"Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France",
"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France",
"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France",
"Caves populaires": "22 rue des Dames, 75017 Paris, France",
"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France",
"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France",
"L'anjou": "1 rue de Montholon, 75009 Paris, France",
"Le Brio": "216, rue Marcadet, 75018 Paris, France",
"Tamm Bara": "7 rue Clisson, 75013 Paris, France",
"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly",
"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France",
"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France",
"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France",
"Le Centenaire": "104 rue amelot, 75011 Paris, France",
"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France",
"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France",
"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France",
"Le Dunois": "77 rue Dunois, 75013 Paris, France", "Le Dunois": "77 rue Dunois, 75013 Paris, France",
"La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France", "La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France",
"Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France", "Le 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", "Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France",
"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly",
"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France",
"Le bal du pirate": "60 rue des bergers, 75015 Paris, France", "Le bal du pirate": "60 rue des bergers, 75015 Paris, France",
"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France",
"L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France", "L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France",
"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France", "l'orillon bar": "35 rue de l'orillon, 75011 Paris, France",
"zic zinc": "95 rue claude decaen, 75012 Paris, France", "zic zinc": "95 rue claude decaen, 75012 Paris, France",
"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France",
"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France",
"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France",
"Le Centenaire": "104 rue amelot, 75011 Paris, France",
"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France",
"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France",
"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France", "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",
"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",
"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France", "Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France",
"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France", "le 1 cinq": "172 rue de vaugirard, 75015 Paris, France",
"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France",
"Les Artisans": "106 rue Lecourbe, 75015 Paris, France", "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 BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France", "Le Brio": "216, rue Marcadet, 75018 Paris, France",
"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France", "Tamm Bara": "7 rue Clisson, 75013 Paris, France",
"Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France", "Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France",
"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France", "bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France",
"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France", "Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France",
"La Brocante": "10 rue Rossini, 75009 Paris, France", "Caves populaires": "22 rue des Dames, 75017 Paris, France",
"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France", "Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France",
"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France", "Chez Prune": "36 rue Beaurepaire, 75010 Paris, France",
"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France", "L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France",
"O'Paris": "1 Rue des Envierges, 75020 Paris, France", "L'anjou": "1 rue de Montholon, 75009 Paris, France",
"Botak cafe": "1 rue Paul albert, 75018 Paris, France", "Botak cafe": "1 rue Paul albert, 75018 Paris, France",
"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France",
"Les caves populaires": "22 rue des Dames, 75017 Paris, France",
"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France",
"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France",
"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France",
"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France",
"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France",
"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France",
"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France",
"Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France", "Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France",
"Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France", "Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France",
"Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France", "Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France",
"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France",
"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France", "le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France",
"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France",
"maison du vin": "52 rue des plantes, 75014 Paris, France", "maison du vin": "52 rue des plantes, 75014 Paris, France",
"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France", "Le Tournebride": "104 rue Mouffetard, 75005 Paris, France",
"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France", "Le Fronton": "63 rue de Ponthieu, 75008 Paris, France",
"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France",
"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France",
"Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France",
"Les caves populaires": "22 rue des Dames, 75017 Paris, France",
"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France",
"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France",
"La Brocante": "10 rue Rossini, 75009 Paris, France",
"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France",
"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France",
"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France",
"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France",
"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France",
"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France",
"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France",
"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France",
"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France",
"le lutece": "380 rue de vaugirard, 75015 Paris, France", "le lutece": "380 rue de vaugirard, 75015 Paris, France",
"O'Paris": "1 Rue des Envierges, 75020 Paris, France",
"Rivolux": "16 rue de Rivoli, 75004 Paris, France", "Rivolux": "16 rue de Rivoli, 75004 Paris, France",
"Brasiloja": "16 rue Ganneron, 75018 Paris, France", "Brasiloja": "16 rue Ganneron, 75018 Paris, France",
"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France",
"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France",
"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France",
"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France",
"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France",
"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France",
"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France",
"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France",
"Melting Pot": "3 rue de Lagny, 75020 Paris, France",
"le Zango": "58 rue Daguerre, 75014 Paris, France",
"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France",
"Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France", "Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France",
"Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France", "Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France",
"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France",
"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France", "Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France",
"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France", "Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France",
"L'Angle": "28 rue de Ponthieu, 75008 Paris, France", "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", "Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France",
"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France",
"L'Entracte": "place de l'opera, 75002 Paris, France", "L'Entracte": "place de l'opera, 75002 Paris, France",
"Panem": "18 rue de Crussol, 75011 Paris, France", "Panem": "18 rue de Crussol, 75011 Paris, France",
"Au pays de Vannes": "34 bis rue de Wattignies, 75012 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'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France",
"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France",
"L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France", "L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France",
"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France", "Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France",
"L'horizon": "93, rue de la Roquette, 75011 Paris, France", "L'horizon": "93, rue de la Roquette, 75011 Paris, France",
"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France", "L'empreinte": "54, avenue Daumesnil, 75012 Paris, France",
"Caf\u00e9 Victor": "10 boulevard Victor, 75015 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", "Le Brigadier": "12 rue Blanche, 75009 Paris, France",
"Waikiki": "10 rue d\"Ulm, 75005 Paris, France"} "Waikiki": "10 rue d\"Ulm, 75005 Paris, France",
"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France",
"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France",
"Melting Pot": "3 rue de Lagny, 75020 Paris, France",
"le Zango": "58 rue Daguerre, 75014 Paris, France",
"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France",
"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France",
"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France",
"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France",
"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France"}

View File

@ -1,182 +1,182 @@
Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France
les montparnos, 65 boulevard Pasteur, 75015 Paris, France les montparnos, 65 boulevard Pasteur, 75015 Paris, France
Le Saint Jean, 23 rue des abbesses, 75018 Paris, France Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France
Le Felteu, 1 rue Pecquay, 75004 Paris, France Café Lea, 5 rue Claude Bernard, 75005 Paris, France
Le Bellerive, 71 quai de Seine, 75019 Paris, France
Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France
O q de poule, 53 rue du ruisseau, 75018 Paris, France O q de poule, 53 rue du ruisseau, 75018 Paris, France
Le café des amis, 125 rue Blomet, 75015 Paris, France
Le chantereine, 51 Rue Victoire, 75009 Paris, France Le chantereine, 51 Rue Victoire, 75009 Paris, France
Le Müller, 11 rue Feutrier, 75018 Paris, France Le Müller, 11 rue Feutrier, 75018 Paris, France
La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France
Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France
La Renaissance, 112 Rue Championnet, 75018 Paris, France
Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France
Le Sully, 6 Bd henri IV, 75004 Paris, France La Bauloise, 36 rue du hameau, 75015 Paris, France
Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France Le Dellac, 14 rue Rougemont, 75009 Paris, France
Le café des amis, 125 rue Blomet, 75015 Paris, France
Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France
Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France
Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France
Le Café Livres, 10 rue Saint Martin, 75004 Paris, France
Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France
Le Square, 31 rue Saint-Dominique, 75007 Paris, France
Les Arcades, 61 rue de Ponthieu, 75008 Paris, France
Le Bosquet, 46 avenue Bosquet, 75007 Paris, France Le Bosquet, 46 avenue Bosquet, 75007 Paris, France
Le Sully, 6 Bd henri IV, 75004 Paris, France
Le Felteu, 1 rue Pecquay, 75004 Paris, France
Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France 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 Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France
Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France
Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France
Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France
Le Saint Jean, 23 rue des abbesses, 75018 Paris, France
La Renaissance, 112 Rue Championnet, 75018 Paris, France
Le Square, 31 rue Saint-Dominique, 75007 Paris, France
Les Arcades, 61 rue de Ponthieu, 75008 Paris, France
Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France
Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France
Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France
Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France
Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France
Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France
Café Lea, 5 rue Claude Bernard, 75005 Paris, France Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France
Le Bellerive, 71 quai de Seine, 75019 Paris, France Le Café Livres, 10 rue Saint Martin, 75004 Paris, France
La Bauloise, 36 rue du hameau, 75015 Paris, France Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France
Le Dellac, 14 rue Rougemont, 75009 Paris, France Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France
Le pari's café, 104 rue caulaincourt, 75018 Paris, France
Le Poulailler, 60 rue saint-sabin, 75011 Paris, France
Chai 33, 33 Cour Saint Emilion, 75012 Paris, France
L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France
l'Usine, 1 rue d'Avron, 75020 Paris, France
La Bricole, 52 rue Liebniz, 75018 Paris, France
le ronsard, place maubert, 75005 Paris, France
Face Bar, 82 rue des archives, 75003 Paris, France
American Kitchen, 49 rue bichat, 75010 Paris, France
La Marine, 55 bis quai de valmy, 75010 Paris, France
Le Bloc, 21 avenue Brochant, 75017 Paris, France
La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France
Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France
La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France
Café Zen, 46 rue Victoire, 75009 Paris, France
O'Breizh, 27 rue de Penthièvre, 75008 Paris, France
Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France
Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France
La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France
Le Supercoin, 3, rue Baudelique, 75018 Paris, France
Populettes, 86 bis rue Riquet, 75018 Paris, France
Au bon coin, 49 rue des Cloys, 75018 Paris, France
Le Couvent, 69 rue Broca, 75013 Paris, France
La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France
L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France
Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France
Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France
Le Café frappé, 95 rue Montmartre, 75002 Paris, France
La Perle, 78 rue vieille du temple, 75003 Paris, France
Le Descartes, 1 rue Thouin, 75005 Paris, France
Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France
Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France
Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France
Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France
Le Malar, 88 rue Saint-Dominique, 75007 Paris, France
Au panini de la place, 47 rue Belgrand, 75020 Paris, France
Le Village, 182 rue de Courcelles, 75017 Paris, France
Pause Café, 41 rue de Charonne, 75011 Paris, France
Le Pure café, 14 rue Jean Macé, 75011 Paris, France
Extra old café, 307 fg saint Antoine, 75011 Paris, France
Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France
En attendant l'or, 3 rue Faidherbe, 75011 Paris, France
Brûlerie San José, 30 rue des Petits-Champs, 75002 Paris, France 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
Le General Beuret, 9 Place du General Beuret, 75015 Paris, France
Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France
En attendant l'or, 3 rue Faidherbe, 75011 Paris, France
Café Martin, 2 place Martin Nadaud, 75001 Paris, France 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
Le Biz, 18 rue Favart, 75002 Paris, France
L'Olive, 8 rue L'Olive, 75018 Paris, France L'Olive, 8 rue L'Olive, 75018 Paris, France
Le pari's café, 104 rue caulaincourt, 75018 Paris, France Le Biz, 18 rue Favart, 75002 Paris, France
Le Poulailler, 60 rue saint-sabin, 75011 Paris, France Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France
La Marine, 55 bis quai de valmy, 75010 Paris, France Le General Beuret, 9 Place du General Beuret, 75015 Paris, France
American Kitchen, 49 rue bichat, 75010 Paris, France
Chai 33, 33 Cour Saint Emilion, 75012 Paris, France
Face Bar, 82 rue des archives, 75003 Paris, France
Le Bloc, 21 avenue Brochant, 75017 Paris, France
La Bricole, 52 rue Liebniz, 75018 Paris, France
le ronsard, place maubert, 75005 Paris, France
l'Usine, 1 rue d'Avron, 75020 Paris, France
La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France
Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France
Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France
Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France
Le Germinal, 95 avenue Emile Zola, 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
Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France
Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France
O'Breizh, 27 rue de Penthièvre, 75008 Paris, France
Le Supercoin, 3, rue Baudelique, 75018 Paris, France
Populettes, 86 bis rue Riquet, 75018 Paris, France
La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France
L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France
Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France
Café Zen, 46 rue Victoire, 75009 Paris, France
La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France
Au bon coin, 49 rue des Cloys, 75018 Paris, France
La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France
Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France
Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France
Le Couvent, 69 rue Broca, 75013 Paris, France
Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France
La Perle, 78 rue vieille du temple, 75003 Paris, France
Le Café frappé, 95 rue Montmartre, 75002 Paris, France
L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France
Le Descartes, 1 rue Thouin, 75005 Paris, France
Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France
Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France
Au panini de la place, 47 rue Belgrand, 75020 Paris, France
Extra old café, 307 fg saint Antoine, 75011 Paris, France
Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France
Le Pure café, 14 rue Jean Macé, 75011 Paris, France
Le Village, 182 rue de Courcelles, 75017 Paris, France
Le Malar, 88 rue Saint-Dominique, 75007 Paris, France
Pause Café, 41 rue de Charonne, 75011 Paris, France
Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France
Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France
Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France
Caprice café, 12 avenue Jean Moulin, 75014 Paris, France
Caves populaires, 22 rue des Dames, 75017 Paris, France
Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France
Chez Prune, 36 rue Beaurepaire, 75010 Paris, France
L'anjou, 1 rue de Montholon, 75009 Paris, France
Le Brio, 216, rue Marcadet, 75018 Paris, France
Tamm Bara, 7 rue Clisson, 75013 Paris, France
La chaumière gourmande, Route de la Muette à Neuilly
Club hippique du Jardin dAcclimatation, 75016 Paris, France
Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France
Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France
Le Centenaire, 104 rue amelot, 75011 Paris, France
Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France
Ragueneau, 202 rue Saint Honoré, 75001 Paris, France
L'Inévitable, 22 rue Linné, 75005 Paris, France
Le Dunois, 77 rue Dunois, 75013 Paris, France Le Dunois, 77 rue Dunois, 75013 Paris, France
La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France
Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France Le 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 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 dAcclimatation, 75016 Paris, France
Le bal du pirate, 60 rue des bergers, 75015 Paris, France Le bal du pirate, 60 rue des bergers, 75015 Paris, France
Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France
L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France
l'orillon bar, 35 rue de l'orillon, 75011 Paris, France l'orillon bar, 35 rue de l'orillon, 75011 Paris, France
zic zinc, 95 rue claude decaen, 75012 Paris, France zic zinc, 95 rue claude decaen, 75012 Paris, France
Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France
Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France
Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France
Le Centenaire, 104 rue amelot, 75011 Paris, France
Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France
Ragueneau, 202 rue Saint Honoré, 75001 Paris, France
Café Pistache, 9 rue des petits champs, 75001 Paris, France 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
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
Café beauveau, 9 rue de Miromesnil, 75008 Paris, France Café beauveau, 9 rue de Miromesnil, 75008 Paris, France
le 1 cinq, 172 rue de vaugirard, 75015 Paris, France le 1 cinq, 172 rue de vaugirard, 75015 Paris, France
Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France
Les Artisans, 106 rue Lecourbe, 75015 Paris, France 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 BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France Le Brio, 216, rue Marcadet, 75018 Paris, France
La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France Tamm Bara, 7 rue Clisson, 75013 Paris, France
Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France
La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France
Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France Caprice café, 12 avenue Jean Moulin, 75014 Paris, France
La Brocante, 10 rue Rossini, 75009 Paris, France Caves populaires, 22 rue des Dames, 75017 Paris, France
Café Clochette, 16 avenue Richerand, 75010 Paris, France Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France
L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France Chez Prune, 36 rue Beaurepaire, 75010 Paris, France
NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France L'Inévitable, 22 rue Linné, 75005 Paris, France
O'Paris, 1 Rue des Envierges, 75020 Paris, France L'anjou, 1 rue de Montholon, 75009 Paris, France
Botak cafe, 1 rue Paul albert, 75018 Paris, France Botak cafe, 1 rue Paul albert, 75018 Paris, France
La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France
Les caves populaires, 22 rue des Dames, 75017 Paris, France
Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France
Trois pièces cuisine, 101 rue des dames, 75017 Paris, France
Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France
L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France
Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France
Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France
Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France
Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France
Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France
Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France
L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France
le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France
Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France
maison du vin, 52 rue des plantes, 75014 Paris, France maison du vin, 52 rue des plantes, 75014 Paris, France
Le Tournebride, 104 rue Mouffetard, 75005 Paris, France Le Tournebride, 104 rue Mouffetard, 75005 Paris, France
Le Fronton, 63 rue de Ponthieu, 75008 Paris, France Le Fronton, 63 rue de Ponthieu, 75008 Paris, France
Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France
La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France
Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France
Les caves populaires, 22 rue des Dames, 75017 Paris, France
Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France
Trois pièces cuisine, 101 rue des dames, 75017 Paris, France
La Brocante, 10 rue Rossini, 75009 Paris, France
Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France
Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France
Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France
La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France
La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France
Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France
Café Clochette, 16 avenue Richerand, 75010 Paris, France
L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France
NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France
le lutece, 380 rue de vaugirard, 75015 Paris, France le lutece, 380 rue de vaugirard, 75015 Paris, France
O'Paris, 1 Rue des Envierges, 75020 Paris, France
Rivolux, 16 rue de Rivoli, 75004 Paris, France Rivolux, 16 rue de Rivoli, 75004 Paris, France
Brasiloja, 16 rue Ganneron, 75018 Paris, France Brasiloja, 16 rue Ganneron, 75018 Paris, France
Le café Monde et Médias, Place de la République, 75003 Paris, France
L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France
Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France
l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France
Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France
Pari's Café, 174 avenue de Clichy, 75017 Paris, France
Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France
Café Varenne, 36 rue de Varenne, 75007 Paris, France
Melting Pot, 3 rue de Lagny, 75020 Paris, France
le Zango, 58 rue Daguerre, 75014 Paris, France
Chez Miamophile, 6 rue Mélingue, 75019 Paris, France
Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France
Canopy Café associatif, 19 rue Pajol, 75018 Paris, France Canopy Café associatif, 19 rue Pajol, 75018 Paris, France
Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France
Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France
Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France
L'Angle, 28 rue de Ponthieu, 75008 Paris, France 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 Café Dupont, 198 rue de la Convention, 75015 Paris, France
Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France
L'Entracte, place de l'opera, 75002 Paris, France L'Entracte, place de l'opera, 75002 Paris, France
Panem, 18 rue de Crussol, 75011 Paris, France Panem, 18 rue de Crussol, 75011 Paris, France
Au pays de Vannes, 34 bis rue de Wattignies, 75012 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'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France
Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France
L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France
Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France
L'horizon, 93, rue de la Roquette, 75011 Paris, France L'horizon, 93, rue de la Roquette, 75011 Paris, France
L'empreinte, 54, avenue Daumesnil, 75012 Paris, France L'empreinte, 54, avenue Daumesnil, 75012 Paris, France
Café Victor, 10 boulevard Victor, 75015 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 Le Brigadier, 12 rue Blanche, 75009 Paris, France
Waikiki, 10 rue d"Ulm, 75005 Paris, France Waikiki, 10 rue d"Ulm, 75005 Paris, France
Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France
Pari's Café, 174 avenue de Clichy, 75017 Paris, France
Melting Pot, 3 rue de Lagny, 75020 Paris, France
le Zango, 58 rue Daguerre, 75014 Paris, France
Chez Miamophile, 6 rue Mélingue, 75019 Paris, France
Le café Monde et Médias, Place de la République, 75003 Paris, France
Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France
Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France
L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France

View File

@ -96,8 +96,8 @@ graph = bonobo.Graph(
), ),
normalize, normalize,
filter_france, filter_france,
bonobo.JsonWriter(path='fablabs.txt', ioformat='arg0'),
bonobo.Tee(display), bonobo.Tee(display),
bonobo.JsonWriter(path='fablabs.txt'),
) )
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -9,13 +9,16 @@ class OddOnlyFilter(Filter):
@Filter @Filter
def MultiplesOfThreeOnlyFilter(self, i): def multiples_of_three(i):
return not (i % 3) return not (i % 3)
graph = bonobo.Graph( graph = bonobo.Graph(
lambda: tuple(range(50)), lambda: tuple(range(50)),
OddOnlyFilter(), OddOnlyFilter(),
MultiplesOfThreeOnlyFilter(), multiples_of_three,
print, print,
) )
if __name__ == '__main__':
bonobo.run(graph)

View File

@ -14,3 +14,6 @@ graph = bonobo.Graph(
pause, pause,
print, print,
) )
if __name__ == '__main__':
bonobo.run(graph)

View File

@ -8,7 +8,7 @@ def split_one(line):
graph = bonobo.Graph( graph = bonobo.Graph(
bonobo.FileReader('coffeeshops.txt'), bonobo.FileReader('coffeeshops.txt'),
split_one, split_one,
bonobo.JsonWriter('coffeeshops.json'), bonobo.JsonWriter('coffeeshops.json', ioformat='arg0'),
) )
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -58,6 +58,13 @@ class LoopingExecutionContext(Wrapper):
# XXX enhancers # XXX enhancers
self._enhancers = get_enhancers(self.wrapped) self._enhancers = get_enhancers(self.wrapped)
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
self.stop()
def start(self): def start(self):
if self.started: if self.started:
raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self))) raise RuntimeError('Cannot start a node twice ({}).'.format(get_name(self)))

View File

@ -3,7 +3,7 @@ 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.errors import InactiveReadableError from bonobo.errors import InactiveReadableError, UnrecoverableError
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.structs.inputs import Input
@ -93,6 +93,10 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
except Empty: except Empty:
sleep(self.PERIOD) sleep(self.PERIOD)
continue continue
except UnrecoverableError as exc:
self.handle_error(exc, traceback.format_exc())
self.input.shutdown()
break
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc()) self.handle_error(exc, traceback.format_exc())

View File

@ -65,7 +65,7 @@ class ConsoleOutputPlugin(Plugin):
for i in context.graph.topologically_sorted_indexes: for i in context.graph.topologically_sorted_indexes:
node = context[i] node = context[i]
name_suffix = '({})'.format(i) if settings.DEBUG else '' name_suffix = '({})'.format(i) if settings.DEBUG.get() else ''
if node.alive: if node.alive:
_line = ''.join( _line = ''.join(
( (
@ -100,7 +100,7 @@ class ConsoleOutputPlugin(Plugin):
print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr) print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr)
def _write(self, graph_context, rewind): def _write(self, graph_context, rewind):
if settings.PROFILE: if settings.PROFILE.get():
if self.counter % 10 and self._append_cache: if self.counter % 10 and self._append_cache:
append = self._append_cache append = self._append_cache
else: else:

View File

@ -13,13 +13,13 @@ def path_str(path):
class OpenDataSoftAPI(Configurable): class OpenDataSoftAPI(Configurable):
dataset = Option(str, required=True) dataset = Option(str, positional=True)
endpoint = Option(str, default='{scheme}://{netloc}{path}') endpoint = Option(str, default='{scheme}://{netloc}{path}')
scheme = Option(str, default='https') scheme = Option(str, default='https')
netloc = Option(str, default='data.opendatasoft.com') netloc = Option(str, default='data.opendatasoft.com')
path = Option(path_str, default='/api/records/1.0/search/') path = Option(path_str, default='/api/records/1.0/search/')
rows = Option(int, default=500) rows = Option(int, default=500)
limit = Option(int, default=None) limit = Option(int, required=False)
timezone = Option(str, default='Europe/Paris') timezone = Option(str, default='Europe/Paris')
kwargs = Option(dict, default=dict) kwargs = Option(dict, default=dict)

View File

@ -71,5 +71,8 @@ def get_logger(name='bonobo'):
return logging.getLogger(name) return logging.getLogger(name)
# Compatibility with python logging
getLogger = get_logger
# Setup formating and level. # Setup formating and level.
setup(level=settings.LOGGING_LEVEL) setup(level=settings.LOGGING_LEVEL.get())

View File

@ -1,9 +1,8 @@
from bonobo.nodes.io import __all__ as _all_io
from bonobo.nodes.io import *
from bonobo.nodes.basics import __all__ as _all_basics
from bonobo.nodes.basics import * from bonobo.nodes.basics import *
from bonobo.nodes.basics import __all__ as _all_basics
from bonobo.nodes.filter import Filter from bonobo.nodes.filter import Filter
from bonobo.nodes.io import *
from bonobo.nodes.io import __all__ as _all_io
from bonobo.nodes.throttle import RateLimited
__all__ = _all_basics + _all_io + ['Filter'] __all__ = _all_basics + _all_io + ['Filter', 'RateLimited']

View File

@ -1,23 +1,19 @@
import functools import functools
from pprint import pprint as _pprint
import itertools import itertools
from colorama import Fore, Style
from bonobo import settings from bonobo import settings
from bonobo.config import Configurable, Option from bonobo.config import Configurable, Option
from bonobo.config.processors import ContextProcessor from bonobo.config.processors import ContextProcessor
from bonobo.constants import NOT_MODIFIED
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',
'Limit', 'Limit',
'Tee', 'Tee',
'count', 'count',
'pprint',
'PrettyPrinter', 'PrettyPrinter',
'noop', 'noop',
] ]
@ -73,7 +69,7 @@ def _count_counter(self, context):
class PrettyPrinter(Configurable): class PrettyPrinter(Configurable):
def call(self, *args, **kwargs): def call(self, *args, **kwargs):
formater = self._format_quiet if settings.QUIET else self._format_console formater = self._format_quiet if settings.QUIET.get() else self._format_console
for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())):
print(formater(i, item, value)) print(formater(i, item, value))
@ -87,47 +83,6 @@ class PrettyPrinter(Configurable):
) )
pprint = PrettyPrinter()
pprint.__name__ = 'pprint'
def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True):
from bonobo.constants import NOT_MODIFIED
def _pprint(*args, **kwargs):
nonlocal title_keys, sort, print_values
row = args[0]
for key in title_keys:
if key in row:
print(Style.BRIGHT, row.get(key), Style.RESET_ALL, sep='')
break
if print_values:
for k in sorted(row) if sort else row:
print(
'',
Fore.BLUE,
k,
Style.RESET_ALL,
' : ',
Fore.BLACK,
'(',
type(row[k]).__name__,
')',
Style.RESET_ALL,
' ',
repr(row[k]),
CLEAR_EOL,
)
yield NOT_MODIFIED
_pprint.__name__ = 'pprint'
return _pprint
def noop(*args, **kwargs): # pylint: disable=unused-argument def noop(*args, **kwargs): # pylint: disable=unused-argument
from bonobo.constants import NOT_MODIFIED from bonobo.constants import NOT_MODIFIED
return NOT_MODIFIED return NOT_MODIFIED

83
bonobo/nodes/io/base.py Normal file
View File

@ -0,0 +1,83 @@
from bonobo import settings
from bonobo.config import Configurable, ContextProcessor, Option, Service
from bonobo.errors import UnrecoverableValueError, UnrecoverableNotImplementedError
from bonobo.structs.bags import Bag
class IOFormatEnabled(Configurable):
ioformat = Option(default=settings.IOFORMAT.get)
def get_input(self, *args, **kwargs):
if self.ioformat == settings.IOFORMAT_ARG0:
if len(args) != 1 or len(kwargs):
raise UnrecoverableValueError(
'Wrong input formating: IOFORMAT=ARG0 implies one arg and no kwargs, got args={!r} and kwargs={!r}.'.
format(args, kwargs)
)
return args[0]
if self.ioformat == settings.IOFORMAT_KWARGS:
if len(args) or not len(kwargs):
raise UnrecoverableValueError(
'Wrong input formating: IOFORMAT=KWARGS ioformat implies no arg, got args={!r} and kwargs={!r}.'.
format(args, kwargs)
)
return kwargs
raise UnrecoverableNotImplementedError('Unsupported format.')
def get_output(self, row):
if self.ioformat == settings.IOFORMAT_ARG0:
return row
if self.ioformat == settings.IOFORMAT_KWARGS:
return Bag(**row)
raise UnrecoverableNotImplementedError('Unsupported format.')
class FileHandler(Configurable):
"""Abstract component factory for file-related components.
Args:
path (str): which path to use within the provided filesystem.
eol (str): which character to use to separate lines.
mode (str): which mode to use when opening the file.
fs (str): service name to use for filesystem.
"""
path = Option(str, required=True, positional=True) # type: str
eol = Option(str, default='\n') # type: str
mode = Option(str) # type: str
encoding = Option(str, default='utf-8') # type: str
fs = Service('fs') # type: str
@ContextProcessor
def file(self, context, fs):
with self.open(fs) as file:
yield file
def open(self, fs):
return fs.open(self.path, self.mode, encoding=self.encoding)
class Reader:
"""Abstract component factory for readers.
"""
def __call__(self, *args, **kwargs):
yield from self.read(*args, **kwargs)
def read(self, *args, **kwargs):
raise NotImplementedError('Abstract.')
class Writer:
"""Abstract component factory for writers.
"""
def __call__(self, *args, **kwargs):
return self.write(*args, **kwargs)
def write(self, *args, **kwargs):
raise NotImplementedError('Abstract.')

View File

@ -3,7 +3,8 @@ import csv
from bonobo.config import Option from bonobo.config import Option
from bonobo.config.processors import ContextProcessor from bonobo.config.processors import ContextProcessor
from bonobo.constants import NOT_MODIFIED from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.file import FileHandler, FileReader, FileWriter from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.nodes.io.base import FileHandler, IOFormatEnabled
from bonobo.util.objects import ValueHolder from bonobo.util.objects import ValueHolder
@ -25,10 +26,10 @@ class CsvHandler(FileHandler):
""" """
delimiter = Option(str, default=';') delimiter = Option(str, default=';')
quotechar = Option(str, default='"') quotechar = Option(str, default='"')
headers = Option(tuple) headers = Option(tuple, required=False)
class CsvReader(CsvHandler, FileReader): class CsvReader(IOFormatEnabled, FileReader, CsvHandler):
""" """
Reads a CSV and yield the values as dicts. Reads a CSV and yield the values as dicts.
@ -64,7 +65,7 @@ class CsvReader(CsvHandler, FileReader):
yield self.get_output(dict(zip(_headers, row))) yield self.get_output(dict(zip(_headers, row)))
class CsvWriter(CsvHandler, FileWriter): class CsvWriter(IOFormatEnabled, FileWriter, CsvHandler):
@ContextProcessor @ContextProcessor
def writer(self, context, fs, file, lineno): def writer(self, context, fs, file, lineno):
writer = csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol) writer = csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol)

View File

@ -1,81 +1,11 @@
from bonobo import settings from bonobo.config import Option
from bonobo.config import Option, Service
from bonobo.config.configurables import Configurable
from bonobo.config.processors import ContextProcessor from bonobo.config.processors import ContextProcessor
from bonobo.constants import NOT_MODIFIED from bonobo.constants import NOT_MODIFIED
from bonobo.structs.bags import Bag from bonobo.nodes.io.base import FileHandler, Reader, Writer
from bonobo.util.objects import ValueHolder from bonobo.util.objects import ValueHolder
class FileHandler(Configurable): class FileReader(Reader, FileHandler):
"""Abstract component factory for file-related components.
Args:
path (str): which path to use within the provided filesystem.
eol (str): which character to use to separate lines.
mode (str): which mode to use when opening the file.
fs (str): service name to use for filesystem.
"""
path = Option(str, required=True, positional=True) # type: str
eol = Option(str, default='\n') # type: str
mode = Option(str) # type: str
encoding = Option(str, default='utf-8') # type: str
fs = Service('fs') # type: str
ioformat = Option(default=settings.IOFORMAT.get)
@ContextProcessor
def file(self, context, fs):
with self.open(fs) as file:
yield file
def open(self, fs):
return fs.open(self.path, self.mode, encoding=self.encoding)
def get_input(self, *args, **kwargs):
if self.ioformat == settings.IOFORMAT_ARG0:
assert len(args) == 1 and not len(kwargs), 'ARG0 format implies one arg and no kwargs.'
return args[0]
if self.ioformat == settings.IOFORMAT_KWARGS:
assert len(args) == 0 and len(kwargs), 'KWARGS format implies no arg.'
return kwargs
raise NotImplementedError('Unsupported format.')
def get_output(self, row):
if self.ioformat == settings.IOFORMAT_ARG0:
return row
if self.ioformat == settings.IOFORMAT_KWARGS:
return Bag(**row)
raise NotImplementedError('Unsupported format.')
class Reader(FileHandler):
"""Abstract component factory for readers.
"""
def __call__(self, *args, **kwargs):
yield from self.read(*args, **kwargs)
def read(self, *args, **kwargs):
raise NotImplementedError('Abstract.')
class Writer(FileHandler):
"""Abstract component factory for writers.
"""
def __call__(self, *args, **kwargs):
return self.write(*args)
def write(self, *args, **kwargs):
raise NotImplementedError('Abstract.')
class FileReader(Reader):
"""Component factory for file-like readers. """Component factory for file-like readers.
On its own, it can be used to read a file and yield one row per line, trimming the "eol" character at the end if On its own, it can be used to read a file and yield one row per line, trimming the "eol" character at the end if
@ -93,7 +23,7 @@ class FileReader(Reader):
yield line.rstrip(self.eol) yield line.rstrip(self.eol)
class FileWriter(Writer): class FileWriter(Writer, FileHandler):
"""Component factory for file or file-like writers. """Component factory for file or file-like writers.
On its own, it can be used to write in a file one line per row that comes into this component. Extending it is On its own, it can be used to write in a file one line per row that comes into this component. Extending it is
@ -107,11 +37,11 @@ class FileWriter(Writer):
lineno = ValueHolder(0) lineno = ValueHolder(0)
yield lineno yield lineno
def write(self, fs, file, lineno, row): def write(self, fs, file, lineno, line):
""" """
Write a row on the next line of opened file in context. Write a row on the next line of opened file in context.
""" """
self._write_line(file, (self.eol if lineno.value else '') + row) self._write_line(file, (self.eol if lineno.value else '') + line)
lineno += 1 lineno += 1
return NOT_MODIFIED return NOT_MODIFIED

View File

@ -1,15 +1,17 @@
import json import json
from bonobo.config.processors import ContextProcessor from bonobo.config.processors import ContextProcessor
from bonobo.nodes.io.file import FileWriter, FileReader from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler, IOFormatEnabled
from bonobo.nodes.io.file import FileReader, FileWriter
class JsonHandler(): class JsonHandler(FileHandler):
eol = ',\n' eol = ',\n'
prefix, suffix = '[', ']' prefix, suffix = '[', ']'
class JsonReader(JsonHandler, FileReader): class JsonReader(IOFormatEnabled, FileReader, JsonHandler):
loader = staticmethod(json.load) loader = staticmethod(json.load)
def read(self, fs, file): def read(self, fs, file):
@ -17,18 +19,21 @@ class JsonReader(JsonHandler, FileReader):
yield self.get_output(line) yield self.get_output(line)
class JsonWriter(JsonHandler, FileWriter): class JsonWriter(IOFormatEnabled, FileWriter, JsonHandler):
@ContextProcessor @ContextProcessor
def envelope(self, context, fs, file, lineno): def envelope(self, context, fs, file, lineno):
file.write(self.prefix) file.write(self.prefix)
yield yield
file.write(self.suffix) file.write(self.suffix)
def write(self, fs, file, lineno, row): def write(self, fs, file, lineno, *args, **kwargs):
""" """
Write a json row on the next line of file pointed by ctx.file. Write a json row on the next line of file pointed by ctx.file.
:param ctx: :param ctx:
:param row: :param row:
""" """
return super().write(fs, file, lineno, json.dumps(row)) row = self.get_input(*args, **kwargs)
self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row))
lineno += 1
return NOT_MODIFIED

View File

@ -1,10 +1,11 @@
import pickle import pickle
from bonobo.config.processors import ContextProcessor
from bonobo.config import Option from bonobo.config import Option
from bonobo.config.processors import ContextProcessor
from bonobo.constants import NOT_MODIFIED from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler, IOFormatEnabled
from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.util.objects import ValueHolder from bonobo.util.objects import ValueHolder
from .file import FileReader, FileWriter, FileHandler
class PickleHandler(FileHandler): class PickleHandler(FileHandler):
@ -16,10 +17,10 @@ class PickleHandler(FileHandler):
""" """
item_names = Option(tuple) item_names = Option(tuple, required=False)
class PickleReader(PickleHandler, FileReader): class PickleReader(IOFormatEnabled, FileReader, PickleHandler):
""" """
Reads a Python pickle object and yields the items in dicts. Reads a Python pickle object and yields the items in dicts.
""" """
@ -56,8 +57,7 @@ class PickleReader(PickleHandler, FileReader):
yield self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))) yield self.get_output(dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i)))
class PickleWriter(PickleHandler, FileWriter): class PickleWriter(IOFormatEnabled, FileWriter, PickleHandler):
mode = Option(str, default='wb') mode = Option(str, default='wb')
def write(self, fs, file, lineno, item): def write(self, fs, file, lineno, item):

55
bonobo/nodes/throttle.py Normal file
View File

@ -0,0 +1,55 @@
import threading
import time
from bonobo.config import Configurable, ContextProcessor, Method, Option
class RateLimitBucket(threading.Thread):
daemon = True
@property
def stopped(self):
return self._stop_event.is_set()
def __init__(self, initial=1, period=1, amount=1):
super(RateLimitBucket, self).__init__()
self.semaphore = threading.BoundedSemaphore(initial)
self.amount = amount
self.period = period
self._stop_event = threading.Event()
def stop(self):
self._stop_event.set()
def run(self):
while not self.stopped:
time.sleep(self.period)
for _ in range(self.amount):
self.semaphore.release()
def wait(self):
return self.semaphore.acquire()
class RateLimited(Configurable):
handler = Method()
initial = Option(int, positional=True, default=1)
period = Option(int, positional=True, default=1)
amount = Option(int, positional=True, default=1)
@ContextProcessor
def bucket(self, context):
print(context)
bucket = RateLimitBucket(self.initial, self.amount, self.period)
bucket.start()
print(bucket)
yield bucket
bucket.stop()
bucket.join()
def call(self, bucket, *args, **kwargs):
print(bucket, args, kwargs)
bucket.wait()
return self.handler(*args, **kwargs)

View File

@ -5,6 +5,10 @@ from bonobo.errors import ValidationError
def to_bool(s): def to_bool(s):
if s is None:
return False
if type(s) is bool:
return s
if len(s): if len(s):
if s.lower() in ('f', 'false', 'n', 'no', '0'): if s.lower() in ('f', 'false', 'n', 'no', '0'):
return False return False
@ -13,7 +17,18 @@ def to_bool(s):
class Setting: class Setting:
def __init__(self, name, default=None, validator=None): __all__ = {}
@classmethod
def clear_all(cls):
for setting in Setting.__all__.values():
setting.clear()
def __new__(cls, name, *args, **kwargs):
Setting.__all__[name] = super().__new__(cls)
return Setting.__all__[name]
def __init__(self, name, default=None, validator=None, formatter=None):
self.name = name self.name = name
if default: if default:
@ -21,15 +36,14 @@ class Setting:
else: else:
self.default = lambda: None self.default = lambda: None
if validator: self.validator = validator
self.validator = validator self.formatter = formatter
else:
self.validator = None
def __repr__(self): def __repr__(self):
return '<Setting {}={!r}>'.format(self.name, self.value) return '<Setting {}={!r}>'.format(self.name, self.get())
def set(self, value): def set(self, value):
value = self.formatter(value) if self.formatter else value
if self.validator and not self.validator(value): if self.validator and not self.validator(value):
raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name))
self.value = value self.value = value
@ -38,21 +52,35 @@ class Setting:
try: try:
return self.value return self.value
except AttributeError: except AttributeError:
self.value = self.default() value = os.environ.get(self.name, None)
if value is None:
value = self.default()
self.set(value)
return self.value return self.value
def clear(self):
try:
del self.value
except AttributeError:
pass
# Debug/verbose mode. # Debug/verbose mode.
DEBUG = to_bool(os.environ.get('DEBUG', 'f')) DEBUG = Setting('DEBUG', formatter=to_bool, default=False)
# Profile mode. # Profile mode.
PROFILE = to_bool(os.environ.get('PROFILE', 'f')) PROFILE = Setting('PROFILE', formatter=to_bool, default=False)
# Quiet mode. # Quiet mode.
QUIET = to_bool(os.environ.get('QUIET', 'f')) QUIET = Setting('QUIET', formatter=to_bool, default=False)
# Logging level. # Logging level.
LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO LOGGING_LEVEL = Setting(
'LOGGING_LEVEL',
formatter=logging._checkLevel,
validator=logging._checkLevel,
default=lambda: logging.DEBUG if DEBUG.get() else logging.INFO
)
# Input/Output format for transformations # Input/Output format for transformations
IOFORMAT_ARG0 = 'arg0' IOFORMAT_ARG0 = 'arg0'
@ -67,5 +95,8 @@ IOFORMAT = Setting('IOFORMAT', default=IOFORMAT_KWARGS, validator=IOFORMATS.__co
def check(): def check():
if DEBUG and QUIET: if DEBUG.get() and QUIET.get():
raise RuntimeError('I cannot be verbose and quiet at the same time.') raise RuntimeError('I cannot be verbose and quiet at the same time.')
clear_all = Setting.clear_all

View File

@ -1,6 +1,5 @@
import time import time
import traceback import traceback
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
@ -31,12 +30,11 @@ class ExecutorStrategy(Strategy):
for plugin_context in context.plugins: for plugin_context in context.plugins:
def _runner(plugin_context=plugin_context): def _runner(plugin_context=plugin_context):
try: with plugin_context:
plugin_context.start() try:
plugin_context.loop() plugin_context.loop()
plugin_context.stop() except Exception as exc:
except Exception as exc: print_error(exc, traceback.format_exc(), context=plugin_context)
print_error(exc, traceback.format_exc(), context=plugin_context)
futures.append(executor.submit(_runner)) futures.append(executor.submit(_runner))

View File

@ -77,6 +77,12 @@ class Input(Queue, Readable, Writable):
return Queue.put(self, data, block, timeout) return Queue.put(self, data, block, timeout)
def _decrement_runlevel(self):
if self._runlevel == 1:
self.on_finalize()
self._runlevel -= 1
self.on_end()
def get(self, block=True, timeout=None): def get(self, block=True, timeout=None):
if not self.alive: if not self.alive:
raise InactiveReadableError('Cannot get() on an inactive {}.'.format(Readable.__name__)) raise InactiveReadableError('Cannot get() on an inactive {}.'.format(Readable.__name__))
@ -84,13 +90,7 @@ class Input(Queue, Readable, Writable):
data = Queue.get(self, block, timeout) data = Queue.get(self, block, timeout)
if data == END: if data == END:
if self._runlevel == 1: self._decrement_runlevel()
self.on_finalize()
self._runlevel -= 1
# callback
self.on_end()
if not self.alive: if not self.alive:
raise InactiveReadableError( raise InactiveReadableError(
@ -100,6 +100,10 @@ class Input(Queue, Readable, Writable):
return data return data
def shutdown(self):
while self._runlevel >= 1:
self._decrement_runlevel()
def empty(self): def empty(self):
self.mutex.acquire() self.mutex.acquire()
while self._qsize() and self.queue[0] == END: while self._qsize() and self.queue[0] == END:

View File

@ -0,0 +1,6 @@
import bisect
class sortedlist(list):
def insort(self, x):
bisect.insort(self, x)

116
bonobo/util/inspect.py Normal file
View File

@ -0,0 +1,116 @@
from collections import namedtuple
def isconfigurabletype(mixed):
"""
Check if the given argument is an instance of :class:`bonobo.config.ConfigurableMeta`, meaning it has all the
plumbery necessary to build :class:`bonobo.config.Configurable`-like instances.
:param mixed:
:return: bool
"""
from bonobo.config.configurables import ConfigurableMeta
return isinstance(mixed, ConfigurableMeta)
def isconfigurable(mixed):
"""
Check if the given argument is an instance of :class:`bonobo.config.Configurable`.
:param mixed:
:return: bool
"""
from bonobo.config.configurables import Configurable
return isinstance(mixed, Configurable)
def isoption(mixed):
"""
Check if the given argument is an instance of :class:`bonobo.config.Option`.
:param mixed:
:return: bool
"""
from bonobo.config.options import Option
return isinstance(mixed, Option)
def ismethod(mixed):
"""
Check if the given argument is an instance of :class:`bonobo.config.Method`.
:param mixed:
:return: bool
"""
from bonobo.config.options import Method
return isinstance(mixed, Method)
def iscontextprocessor(x):
"""
Check if the given argument is an instance of :class:`bonobo.config.ContextProcessor`.
:param mixed:
:return: bool
"""
from bonobo.config.processors import ContextProcessor
return isinstance(x, ContextProcessor)
def istype(mixed):
"""
Check if the given argument is a type object.
:param mixed:
:return: bool
"""
return isinstance(mixed, type)
ConfigurableInspection = namedtuple(
'ConfigurableInspection', [
'type',
'instance',
'options',
'processors',
'partial',
]
)
ConfigurableInspection.__enter__ = lambda self: self
ConfigurableInspection.__exit__ = lambda *exc_details: None
def inspect_node(mixed, *, _partial=None):
"""
If the given argument is somehow a :class:`bonobo.config.Configurable` object (either a subclass, an instance, or
a partially configured instance), then it will return a :class:`ConfigurableInspection` namedtuple, used to inspect
the configurable metadata (options). If you want to get the option values, you don't need this, it is only usefull
to perform introspection on a configurable.
If it's not looking like a configurable, it will raise a :class:`TypeError`.
:param mixed:
:return: ConfigurableInspection
:raise: TypeError
"""
if isconfigurabletype(mixed):
inst, typ = None, mixed
elif isconfigurable(mixed):
inst, typ = mixed, type(mixed)
elif hasattr(mixed, 'func'):
return inspect_node(mixed.func, _partial=(mixed.args, mixed.keywords))
else:
raise TypeError(
'Not a Configurable, nor a Configurable instance and not even a partially configured Configurable. Check your inputs.'
)
return ConfigurableInspection(
typ,
inst,
list(typ.__options__),
list(typ.__processors__),
_partial,
)

View File

@ -1,6 +1,7 @@
from contextlib import contextmanager from contextlib import contextmanager
from unittest.mock import MagicMock from unittest.mock import MagicMock
from bonobo import open_fs
from bonobo.execution.node import NodeExecutionContext from bonobo.execution.node import NodeExecutionContext
@ -17,3 +18,20 @@ def optional_contextmanager(cm, *, ignore=False):
else: else:
with cm: with cm:
yield yield
class FilesystemTester:
def __init__(self, extension='txt', mode='w'):
self.extension = extension
self.input_data = ''
self.mode = mode
def get_services_for_reader(self, tmpdir):
fs, filename = open_fs(tmpdir), 'input.' + self.extension
with fs.open(filename, self.mode) as fp:
fp.write(self.input_data)
return fs, filename, {'fs': fs}
def get_services_for_writer(self, tmpdir):
fs, filename = open_fs(tmpdir), 'output.' + self.extension
return fs, filename, {'fs': fs}

View File

View File

@ -49,7 +49,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<p class="biglink"><a class="biglink" href="{{ pathto("changes") }}">{% trans %} <p class="biglink"><a class="biglink" target="_blank" href="https://github.com/python-bonobo/bonobo/tree/master/bonobo/examples">{% trans %}
Cookbook{% endtrans %}</a><br/> Cookbook{% endtrans %}</a><br/>
<span class="linkdescr">{% trans %}examples and recipes{% endtrans %}</span></p> <span class="linkdescr">{% trans %}examples and recipes{% endtrans %}</span></p>
</td> </td>

View File

@ -1,6 +1,23 @@
Changelog Changelog
========= =========
v.0.4.2 - 18 june 2017
::::::::::::::::::::::
* [config] Implements a "requires()" service injection decorator for functions (api may change).
* [core] Execution contexts are now context managers.
* [fs] adds a defaut to current working directory in open_fs(...).
* [logging] Adds logging alias for easier imports.
* [stdlib] Fix I/O related nodes (especially json), there were bad bugs with ioformat.
Dependency updates
------------------
* Update bonobo-docker from 0.2.6 to 0.2.8
* Update dependencies.
* Update fs from 2.0.3 to 2.0.4
* Update requests from 2.17.3 to 2.18.1
v.0.4.0 - 10 june 2017 v.0.4.0 - 10 june 2017
:::::::::::::::::::::: ::::::::::::::::::::::

View File

@ -24,10 +24,10 @@ pytest-timeout==1.2.0
pytest==3.1.2 pytest==3.1.2
python-dateutil==2.6.0 python-dateutil==2.6.0
pytz==2017.2 pytz==2017.2
requests==2.17.3 requests==2.18.1
six==1.10.0 six==1.10.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
sphinx==1.6.2 sphinx==1.6.3
sphinxcontrib-websupport==1.0.1 sphinxcontrib-websupport==1.0.1
termcolor==1.1.0 termcolor==1.1.0
urllib3==1.21.1 urllib3==1.21.1

View File

@ -1,19 +1,20 @@
-e .[docker] -e .[docker]
appdirs==1.4.3 appdirs==1.4.3
bonobo-docker==0.2.6 bonobo-docker==0.2.9
certifi==2017.4.17 certifi==2017.4.17
chardet==3.0.4 chardet==3.0.4
colorama==0.3.9 colorama==0.3.9
docker-pycreds==0.2.1 docker-pycreds==0.2.1
docker==2.3.0 docker==2.3.0
enum34==1.1.6 fs==2.0.4
fs==2.0.3
idna==2.5 idna==2.5
pbr==3.0.1 packaging==16.8
pbr==3.1.1
psutil==5.2.2 psutil==5.2.2
pyparsing==2.2.0
pytz==2017.2 pytz==2017.2
requests==2.17.3 requests==2.18.1
six==1.10.0 six==1.10.0
stevedore==1.23.0 stevedore==1.23.0
urllib3==1.21.1 urllib3==1.21.1
websocket-client==0.40.0 websocket-client==0.44.0

View File

@ -11,7 +11,7 @@ ipywidgets==6.0.0
jedi==0.10.2 jedi==0.10.2
jinja2==2.9.6 jinja2==2.9.6
jsonschema==2.6.0 jsonschema==2.6.0
jupyter-client==5.0.1 jupyter-client==5.1.0
jupyter-console==5.1.0 jupyter-console==5.1.0
jupyter-core==4.3.0 jupyter-core==4.3.0
jupyter==1.0.0 jupyter==1.0.0
@ -24,7 +24,7 @@ pandocfilters==1.4.1
pexpect==4.2.1 pexpect==4.2.1
pickleshare==0.7.4 pickleshare==0.7.4
prompt-toolkit==1.0.14 prompt-toolkit==1.0.14
ptyprocess==0.5.1 ptyprocess==0.5.2
pygments==2.2.0 pygments==2.2.0
python-dateutil==2.6.0 python-dateutil==2.6.0
pyzmq==16.0.2 pyzmq==16.0.2

View File

@ -3,15 +3,14 @@ appdirs==1.4.3
certifi==2017.4.17 certifi==2017.4.17
chardet==3.0.4 chardet==3.0.4
colorama==0.3.9 colorama==0.3.9
enum34==1.1.6 fs==2.0.4
fs==2.0.3
idna==2.5 idna==2.5
packaging==16.8 packaging==16.8
pbr==3.0.1 pbr==3.1.1
psutil==5.2.2 psutil==5.2.2
pyparsing==2.2.0 pyparsing==2.2.0
pytz==2017.2 pytz==2017.2
requests==2.17.3 requests==2.18.1
six==1.10.0 six==1.10.0
stevedore==1.23.0 stevedore==1.23.0
urllib3==1.21.1 urllib3==1.21.1

View File

@ -2,12 +2,17 @@ import pytest
from bonobo.config.configurables import Configurable from bonobo.config.configurables import Configurable
from bonobo.config.options import Option from bonobo.config.options import Option
from bonobo.util.inspect import inspect_node
class NoOptConfigurable(Configurable):
pass
class MyConfigurable(Configurable): class MyConfigurable(Configurable):
required_str = Option(str, required=True) required_str = Option(str)
default_str = Option(str, default='foo') default_str = Option(str, default='foo')
integer = Option(int) integer = Option(int, required=False)
class MyHarderConfigurable(MyConfigurable): class MyHarderConfigurable(MyConfigurable):
@ -25,14 +30,20 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable):
def test_missing_required_option_error(): def test_missing_required_option_error():
with inspect_node(MyConfigurable()) as ni:
assert ni.partial
with pytest.raises(TypeError) as exc: with pytest.raises(TypeError) as exc:
MyConfigurable() MyConfigurable(_final=True)
assert exc.match('missing 1 required option:') assert exc.match('missing 1 required option:')
def test_missing_required_options_error(): def test_missing_required_options_error():
with inspect_node(MyHarderConfigurable()) as ni:
assert ni.partial
with pytest.raises(TypeError) as exc: with pytest.raises(TypeError) as exc:
MyHarderConfigurable() MyHarderConfigurable(_final=True)
assert exc.match('missing 2 required options:') assert exc.match('missing 2 required options:')
@ -50,6 +61,10 @@ def test_extraneous_options_error():
def test_defaults(): def test_defaults():
o = MyConfigurable(required_str='hello') o = MyConfigurable(required_str='hello')
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'hello' assert o.required_str == 'hello'
assert o.default_str == 'foo' assert o.default_str == 'foo'
assert o.integer == None assert o.integer == None
@ -57,6 +72,10 @@ def test_defaults():
def test_str_type_factory(): def test_str_type_factory():
o = MyConfigurable(required_str=42) o = MyConfigurable(required_str=42)
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == '42' assert o.required_str == '42'
assert o.default_str == 'foo' assert o.default_str == 'foo'
assert o.integer == None assert o.integer == None
@ -64,6 +83,10 @@ def test_str_type_factory():
def test_int_type_factory(): def test_int_type_factory():
o = MyConfigurable(required_str='yo', default_str='bar', integer='42') o = MyConfigurable(required_str='yo', default_str='bar', integer='42')
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'yo' assert o.required_str == 'yo'
assert o.default_str == 'bar' assert o.default_str == 'bar'
assert o.integer == 42 assert o.integer == 42
@ -71,6 +94,10 @@ def test_int_type_factory():
def test_bool_type_factory(): def test_bool_type_factory():
o = MyHarderConfigurable(required_str='yes', also_required='True') o = MyHarderConfigurable(required_str='yes', also_required='True')
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'yes' assert o.required_str == 'yes'
assert o.default_str == 'foo' assert o.default_str == 'foo'
assert o.integer == None assert o.integer == None
@ -79,6 +106,10 @@ def test_bool_type_factory():
def test_option_resolution_order(): def test_option_resolution_order():
o = MyBetterConfigurable() o = MyBetterConfigurable()
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'kaboom' assert o.required_str == 'kaboom'
assert o.default_str == 'foo' assert o.default_str == 'foo'
assert o.integer == None assert o.integer == None
@ -86,3 +117,20 @@ def test_option_resolution_order():
def test_option_positional(): def test_option_positional():
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello') o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
with inspect_node(o) as ni:
assert not ni.partial
assert o.first == '1'
assert o.second == '2'
assert o.third == '3'
assert o.required_str == 'hello'
assert o.default_str == 'foo'
assert o.integer is None
def test_no_opt_configurable():
o = NoOptConfigurable()
with inspect_node(o) as ni:
assert not ni.partial

View File

@ -1,7 +1,5 @@
import pytest
from bonobo.config import Configurable, Method, Option from bonobo.config import Configurable, Method, Option
from bonobo.errors import ConfigurationError from bonobo.util.inspect import inspect_node
class MethodBasedConfigurable(Configurable): class MethodBasedConfigurable(Configurable):
@ -13,23 +11,73 @@ class MethodBasedConfigurable(Configurable):
self.handler(*args, **kwargs) self.handler(*args, **kwargs)
def test_one_wrapper_only(): def test_multiple_wrapper_suppored():
with pytest.raises(ConfigurationError): class TwoMethods(Configurable):
h1 = Method(required=True)
h2 = Method(required=True)
class TwoMethods(Configurable): with inspect_node(TwoMethods) as ci:
h1 = Method() assert ci.type == TwoMethods
h2 = Method() assert not ci.instance
assert len(ci.options) == 2
assert not len(ci.processors)
assert not ci.partial
@TwoMethods
def OneMethod():
pass
with inspect_node(OneMethod) as ci:
assert ci.type == TwoMethods
assert not ci.instance
assert len(ci.options) == 2
assert not len(ci.processors)
assert ci.partial
@OneMethod
def transformation():
pass
with inspect_node(transformation) as ci:
assert ci.type == TwoMethods
assert ci.instance
assert len(ci.options) == 2
assert not len(ci.processors)
assert not ci.partial
def test_define_with_decorator(): def test_define_with_decorator():
calls = [] calls = []
@MethodBasedConfigurable def my_handler(*args, **kwargs):
def Concrete(self, *args, **kwargs): calls.append((args, kwargs, ))
Concrete = MethodBasedConfigurable(my_handler)
assert callable(Concrete.handler)
assert Concrete.handler == my_handler
with inspect_node(Concrete) as ci:
assert ci.type == MethodBasedConfigurable
assert ci.partial
t = Concrete('foo', bar='baz')
assert callable(t.handler)
assert len(calls) == 0
t()
assert len(calls) == 1
def test_late_binding_method_decoration():
calls = []
@MethodBasedConfigurable(foo='foo')
def Concrete(*args, **kwargs):
calls.append((args, kwargs, )) calls.append((args, kwargs, ))
assert callable(Concrete.handler) assert callable(Concrete.handler)
t = Concrete('foo', bar='baz') t = Concrete(bar='baz')
assert callable(t.handler) assert callable(t.handler)
assert len(calls) == 0 assert len(calls) == 0
@ -43,7 +91,7 @@ def test_define_with_argument():
def concrete_handler(*args, **kwargs): def concrete_handler(*args, **kwargs):
calls.append((args, kwargs, )) calls.append((args, kwargs, ))
t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler) t = MethodBasedConfigurable(concrete_handler, 'foo', bar='baz')
assert callable(t.handler) assert callable(t.handler)
assert len(calls) == 0 assert len(calls) == 0
t() t()
@ -71,7 +119,7 @@ def test_inheritance_then_decorate():
pass pass
@Inheriting @Inheriting
def Concrete(self, *args, **kwargs): def Concrete(*args, **kwargs):
calls.append((args, kwargs, )) calls.append((args, kwargs, ))
assert callable(Concrete.handler) assert callable(Concrete.handler)

View File

@ -0,0 +1,66 @@
from unittest.mock import MagicMock
from bonobo.config import Configurable, ContextProcessor, Method, Option
from bonobo.util.inspect import inspect_node
class Bobby(Configurable):
handler = Method()
handler2 = Method()
foo = Option(positional=True)
bar = Option(required=False)
@ContextProcessor
def think(self, context):
yield 'different'
def call(self, think, *args, **kwargs):
self.handler('1', *args, **kwargs)
self.handler2('2', *args, **kwargs)
def test_partial():
C = Bobby
# inspect the configurable class
with inspect_node(C) as ci:
assert ci.type == Bobby
assert not ci.instance
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert not ci.partial
# instanciate a partial instance ...
f1 = MagicMock()
C = C(f1)
with inspect_node(C) as ci:
assert ci.type == Bobby
assert not ci.instance
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert ci.partial
assert ci.partial[0] == (f1, )
assert not len(ci.partial[1])
# instanciate a more complete partial instance ...
f2 = MagicMock()
C = C(f2)
with inspect_node(C) as ci:
assert ci.type == Bobby
assert not ci.instance
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert ci.partial
assert ci.partial[0] == (f1, f2, )
assert not len(ci.partial[1])
c = C('foo')
with inspect_node(c) as ci:
assert ci.type == Bobby
assert ci.instance
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert not ci.partial

View File

@ -3,7 +3,7 @@ import time
import pytest import pytest
from bonobo.config import Configurable, Container, Exclusive, Service from bonobo.config import Configurable, Container, Exclusive, Service, requires
from bonobo.config.services import validate_service_name from bonobo.config.services import validate_service_name
@ -94,3 +94,17 @@ def test_exclusive():
'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3', 'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3',
'2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4' '2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4'
] ]
def test_requires():
vcr = VCR()
services = Container(output=vcr.append)
@requires('output')
def append(out, x):
out(x)
svcargs = services.args_for(append)
assert len(svcargs) == 1
assert svcargs[0] == vcr.append

View File

@ -1,23 +1,21 @@
import pytest import pytest
from bonobo import Bag, CsvReader, CsvWriter, open_fs, settings from bonobo import Bag, CsvReader, CsvWriter, settings
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution.node import NodeExecutionContext from bonobo.execution.node import NodeExecutionContext
from bonobo.util.testing import CapturingNodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester
csv_tester = FilesystemTester('csv')
csv_tester.input_data = 'a,b,c\na foo,b foo,c foo\na bar,b bar,c bar'
def test_write_csv_to_file(tmpdir): def test_write_csv_to_file_arg0(tmpdir):
fs, filename = open_fs(tmpdir), 'output.csv' fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
writer = CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0) with NodeExecutionContext(CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context:
context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END)
context.step()
context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) context.step()
context.start()
context.step()
context.step()
context.stop()
with fs.open(filename) as fp: with fs.open(filename) as fp:
assert fp.read() == 'foo\nbar\nbaz\n' assert fp.read() == 'foo\nbar\nbaz\n'
@ -26,19 +24,33 @@ def test_write_csv_to_file(tmpdir):
getattr(context, 'file') getattr(context, 'file')
def test_read_csv_from_file(tmpdir): @pytest.mark.parametrize('add_kwargs', ({}, {
fs, filename = open_fs(tmpdir), 'input.csv' 'ioformat': settings.IOFORMAT_KWARGS,
with fs.open(filename, 'w') as fp: }, ))
fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') def test_write_csv_to_file_kwargs(tmpdir, add_kwargs):
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
reader = CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0) with NodeExecutionContext(CsvWriter(path=filename, **add_kwargs), services=services) as context:
context.write(BEGIN, Bag(**{'foo': 'bar'}), Bag(**{'foo': 'baz', 'ignore': 'this'}), END)
context.step()
context.step()
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) with fs.open(filename) as fp:
assert fp.read() == 'foo\nbar\nbaz\n'
context.start() with pytest.raises(AttributeError):
context.write(BEGIN, Bag(), END) getattr(context, 'file')
context.step()
context.stop()
def test_read_csv_from_file_arg0(tmpdir):
fs, filename, services = csv_tester.get_services_for_reader(tmpdir)
with CapturingNodeExecutionContext(
CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0),
services=services,
) as context:
context.write(BEGIN, Bag(), END)
context.step()
assert len(context.send.mock_calls) == 2 assert len(context.send.mock_calls) == 2
@ -59,19 +71,15 @@ def test_read_csv_from_file(tmpdir):
} }
def test_read_csv_kwargs_output_formater(tmpdir): def test_read_csv_from_file_kwargs(tmpdir):
fs, filename = open_fs(tmpdir), 'input.csv' fs, filename, services = csv_tester.get_services_for_reader(tmpdir)
with fs.open(filename, 'w') as fp:
fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar')
reader = CsvReader(path=filename, delimiter=',') with CapturingNodeExecutionContext(
CsvReader(path=filename, delimiter=','),
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) services=services,
) as context:
context.start() context.write(BEGIN, Bag(), END)
context.write(BEGIN, Bag(), END) context.step()
context.step()
context.stop()
assert len(context.send.mock_calls) == 2 assert len(context.send.mock_calls) == 2

View File

@ -1,9 +1,22 @@
import pytest import pytest
from bonobo import Bag, FileReader, FileWriter, open_fs from bonobo import Bag, FileReader, FileWriter
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution.node import NodeExecutionContext from bonobo.execution.node import NodeExecutionContext
from bonobo.util.testing import CapturingNodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester
txt_tester = FilesystemTester('txt')
txt_tester.input_data = 'Hello\nWorld\n'
def test_file_writer_contextless(tmpdir):
fs, filename, services = txt_tester.get_services_for_writer(tmpdir)
with FileWriter(path=filename).open(fs) as fp:
fp.write('Yosh!')
with fs.open(filename) as fp:
assert fp.read() == 'Yosh!'
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -14,46 +27,23 @@ from bonobo.util.testing import CapturingNodeExecutionContext
] ]
) )
def test_file_writer_in_context(tmpdir, lines, output): def test_file_writer_in_context(tmpdir, lines, output):
fs, filename = open_fs(tmpdir), 'output.txt' fs, filename, services = txt_tester.get_services_for_writer(tmpdir)
writer = FileWriter(path=filename) with NodeExecutionContext(FileWriter(path=filename), services=services) as context:
context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, *map(Bag, lines), END)
for _ in range(len(lines)):
context.start() context.step()
context.write(BEGIN, *map(Bag, lines), END)
for _ in range(len(lines)):
context.step()
context.stop()
with fs.open(filename) as fp: with fs.open(filename) as fp:
assert fp.read() == output assert fp.read() == output
def test_file_writer_out_of_context(tmpdir): def test_file_reader(tmpdir):
fs, filename = open_fs(tmpdir), 'output.txt' fs, filename, services = txt_tester.get_services_for_reader(tmpdir)
writer = FileWriter(path=filename) with CapturingNodeExecutionContext(FileReader(path=filename), services=services) as context:
context.write(BEGIN, Bag(), END)
with writer.open(fs) as fp: context.step()
fp.write('Yosh!')
with fs.open(filename) as fp:
assert fp.read() == 'Yosh!'
def test_file_reader_in_context(tmpdir):
fs, filename = open_fs(tmpdir), 'input.txt'
with fs.open(filename, 'w') as fp:
fp.write('Hello\nWorld\n')
reader = FileReader(path=filename)
context = CapturingNodeExecutionContext(reader, services={'fs': fs})
context.start()
context.write(BEGIN, Bag(), END)
context.step()
context.stop()
assert len(context.send.mock_calls) == 2 assert len(context.send.mock_calls) == 2

View File

@ -1,44 +1,48 @@
import pytest import pytest
from bonobo import Bag, JsonReader, JsonWriter, open_fs, settings from bonobo import Bag, JsonReader, JsonWriter, settings
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution.node import NodeExecutionContext from bonobo.execution.node import NodeExecutionContext
from bonobo.util.testing import CapturingNodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester
json_tester = FilesystemTester('json')
json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]'''
def test_write_json_to_file(tmpdir): def test_write_json_arg0(tmpdir):
fs, filename = open_fs(tmpdir), 'output.json' fs, filename, services = json_tester.get_services_for_writer(tmpdir)
writer = JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0) with NodeExecutionContext(JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context:
context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, Bag({'foo': 'bar'}), END)
context.step()
context.start()
context.write(BEGIN, Bag({'foo': 'bar'}), END)
context.step()
context.stop()
with fs.open(filename) as fp: with fs.open(filename) as fp:
assert fp.read() == '[{"foo": "bar"}]' assert fp.read() == '[{"foo": "bar"}]'
with pytest.raises(AttributeError):
getattr(context, 'file')
with pytest.raises(AttributeError): @pytest.mark.parametrize('add_kwargs', ({}, {
getattr(context, 'first') 'ioformat': settings.IOFORMAT_KWARGS,
}, ))
def test_write_json_kwargs(tmpdir, add_kwargs):
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context:
context.write(BEGIN, Bag(**{'foo': 'bar'}), END)
context.step()
with fs.open(filename) as fp:
assert fp.read() == '[{"foo": "bar"}]'
def test_read_json_from_file(tmpdir): def test_read_json_arg0(tmpdir):
fs, filename = open_fs(tmpdir), 'input.json' fs, filename, services = json_tester.get_services_for_reader(tmpdir)
with fs.open(filename, 'w') as fp:
fp.write('[{"x": "foo"},{"x": "bar"}]')
reader = JsonReader(filename, ioformat=settings.IOFORMAT_ARG0)
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) with CapturingNodeExecutionContext(
JsonReader(filename, ioformat=settings.IOFORMAT_ARG0),
context.start() services=services,
context.write(BEGIN, Bag(), END) ) as context:
context.step() context.write(BEGIN, Bag(), END)
context.stop() context.step()
assert len(context.send.mock_calls) == 2 assert len(context.send.mock_calls) == 2

View File

@ -2,24 +2,22 @@ import pickle
import pytest import pytest
from bonobo import Bag, PickleReader, PickleWriter, open_fs, settings from bonobo import Bag, PickleReader, PickleWriter, settings
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution.node import NodeExecutionContext from bonobo.execution.node import NodeExecutionContext
from bonobo.util.testing import CapturingNodeExecutionContext from bonobo.util.testing import CapturingNodeExecutionContext, FilesystemTester
pickle_tester = FilesystemTester('pkl', mode='wb')
pickle_tester.input_data = pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])
def test_write_pickled_dict_to_file(tmpdir): def test_write_pickled_dict_to_file(tmpdir):
fs, filename = open_fs(tmpdir), 'output.pkl' fs, filename, services = pickle_tester.get_services_for_writer(tmpdir)
writer = PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0) with NodeExecutionContext(PickleWriter(filename, ioformat=settings.IOFORMAT_ARG0), services=services) as context:
context = NodeExecutionContext(writer, services={'fs': fs}) context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END)
context.step()
context.write(BEGIN, Bag({'foo': 'bar'}), Bag({'foo': 'baz', 'ignore': 'this'}), END) context.step()
context.start()
context.step()
context.step()
context.stop()
with fs.open(filename, 'rb') as fp: with fs.open(filename, 'rb') as fp:
assert pickle.loads(fp.read()) == {'foo': 'bar'} assert pickle.loads(fp.read()) == {'foo': 'bar'}
@ -29,18 +27,13 @@ def test_write_pickled_dict_to_file(tmpdir):
def test_read_pickled_list_from_file(tmpdir): def test_read_pickled_list_from_file(tmpdir):
fs, filename = open_fs(tmpdir), 'input.pkl' fs, filename, services = pickle_tester.get_services_for_reader(tmpdir)
with fs.open(filename, 'wb') as fp:
fp.write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']]))
reader = PickleReader(filename, ioformat=settings.IOFORMAT_ARG0) with CapturingNodeExecutionContext(
PickleReader(filename, ioformat=settings.IOFORMAT_ARG0), services=services
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) ) as context:
context.write(BEGIN, Bag(), END)
context.start() context.step()
context.write(BEGIN, Bag(), END)
context.step()
context.stop()
assert len(context.send.mock_calls) == 2 assert len(context.send.mock_calls) == 2

View File

@ -5,6 +5,7 @@ import pytest
import bonobo 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
from bonobo.util.inspect import inspect_node
def test_count(): def test_count():

63
tests/test_settings.py Normal file
View File

@ -0,0 +1,63 @@
import logging
from os import environ
from unittest.mock import patch
import pytest
from bonobo import settings
TEST_SETTING = 'TEST_SETTING'
def test_to_bool():
assert not settings.to_bool('')
assert not settings.to_bool('FALSE')
assert not settings.to_bool('NO')
assert not settings.to_bool('0')
assert settings.to_bool('yup')
assert settings.to_bool('True')
assert settings.to_bool('yes')
assert settings.to_bool('1')
def test_setting():
s = settings.Setting(TEST_SETTING)
assert s.get() is None
with patch.dict(environ, {TEST_SETTING: 'hello'}):
assert s.get() is None
s.clear()
assert s.get() == 'hello'
s = settings.Setting(TEST_SETTING, default='nope')
assert s.get() is 'nope'
with patch.dict(environ, {TEST_SETTING: 'hello'}):
assert s.get() == 'nope'
s.clear()
assert s.get() == 'hello'
def test_default_settings():
settings.clear_all()
assert settings.DEBUG.get() == False
assert settings.PROFILE.get() == False
assert settings.QUIET.get() == False
assert settings.LOGGING_LEVEL.get() == logging._checkLevel('INFO')
with patch.dict(environ, {'DEBUG': 't'}):
settings.clear_all()
assert settings.LOGGING_LEVEL.get() == logging._checkLevel('DEBUG')
settings.clear_all()
def test_check():
settings.check()
with patch.dict(environ, {'DEBUG': 't', 'PROFILE': 't', 'QUIET': 't'}):
settings.clear_all()
with pytest.raises(RuntimeError):
settings.check()
settings.clear_all()