Rewritting Bags from scratch using a namedtuple approach, along with other (less major) updates.
New bag implementation improves a lot how bonobo works, even if this is highly backward incompatible (sorry, that's needed, and better sooner than later). * New implementation uses the same approach as python's namedtuple, by dynamically creating the python type's code. This has drawbacks, as it feels like not the right way, but also a lot of benefits that cannot be achieved using a regular approach, especially the constructor parameter order, hardcoded. * Memory usage is now much more efficient. The "keys" memory space will be used only once per "io type", being spent in the underlying type definition instead of in the actual instances. * Transformations now needs to use tuples as output, which will be bound to its "output type". The output type can be infered from the tuple length, or explicitely set by the user using either `context.set_output_type(...)` or `context.set_output_fields(...)` (to build a bag type from a list of field names). Jupyter/Graphviz integration is more tight, allowing to easily display graphs in a notebook, or displaying the live transformation status in an html table instead of a simple <div>. For now, context processors were hacked to stay working as before but the current API is not satisfactory, and should be replaced. This new big change being unreasonable without some time to work on it properly, it is postponed for next versions (0.7, 0.8, ...). Maybe the best idea is to have some kind of "local services", that would use the same dependency injection mechanism as the execution-wide services. Services are now passed by keywoerd arguments only, to avoid confusion with data-arguments.
This commit is contained in:
@ -1,4 +1,11 @@
|
|||||||
[style]
|
[style]
|
||||||
based_on_style = pep8
|
based_on_style = pep8
|
||||||
column_limit = 120
|
column_limit = 120
|
||||||
|
allow_multiline_lambdas = false
|
||||||
|
allow_multiline_dictionary_keys = false
|
||||||
|
coalesce_brackets = true
|
||||||
dedent_closing_brackets = true
|
dedent_closing_brackets = true
|
||||||
|
join_multiple_lines = true
|
||||||
|
spaces_before_comment = 2
|
||||||
|
split_before_first_argument = true
|
||||||
|
split_complex_comprehension = true
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -1,4 +1,4 @@
|
|||||||
# Generated by Medikit 0.4.1 on 2017-11-12.
|
# Generated by Medikit 0.4.2 on 2017-11-26.
|
||||||
# All changes will be overriden.
|
# All changes will be overriden.
|
||||||
|
|
||||||
PACKAGE ?= bonobo
|
PACKAGE ?= bonobo
|
||||||
|
|||||||
@ -43,9 +43,10 @@ python.add_requirements(
|
|||||||
'fs >=2.0,<2.1',
|
'fs >=2.0,<2.1',
|
||||||
'graphviz >=0.8,<0.9',
|
'graphviz >=0.8,<0.9',
|
||||||
'jinja2 >=2.9,<3',
|
'jinja2 >=2.9,<3',
|
||||||
'mondrian >=0.4,<0.5',
|
'mondrian >=0.5,<0.6',
|
||||||
'packaging >=16,<17',
|
'packaging >=16,<17',
|
||||||
'psutil >=5.4,<6',
|
'psutil >=5.4,<6',
|
||||||
|
'python-slugify >=1.2,<1.3',
|
||||||
'requests >=2,<3',
|
'requests >=2,<3',
|
||||||
'stevedore >=1.27,<1.28',
|
'stevedore >=1.27,<1.28',
|
||||||
'whistle >=1.0,<1.1',
|
'whistle >=1.0,<1.1',
|
||||||
|
|||||||
66
RELEASE-0.6.rst
Normal file
66
RELEASE-0.6.rst
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
Problems
|
||||||
|
========
|
||||||
|
|
||||||
|
Failed to display Jupyter Widget of type BonoboWidget.
|
||||||
|
If you're reading this message in Jupyter Notebook or JupyterLab, it may mean that the widgets JavaScript is still loading. If this message persists, it likely means that the widgets JavaScript library is either not installed or not enabled. See the Jupyter Widgets Documentation for setup instructions.
|
||||||
|
If you're reading this message in another notebook frontend (for example, a static rendering on GitHub or NBViewer), it may mean that your frontend doesn't currently support widgets.
|
||||||
|
|
||||||
|
.. code-block:: shell-session
|
||||||
|
|
||||||
|
$ jupyter nbextension enable --py widgetsnbextension
|
||||||
|
$ jupyter nbextension install --py --symlink bonobo.contrib.jupyter
|
||||||
|
$ jupyter nbextension enable --py bonobo.contrib.jupyter
|
||||||
|
|
||||||
|
|
||||||
|
Todo
|
||||||
|
====
|
||||||
|
|
||||||
|
* Pretty printer
|
||||||
|
|
||||||
|
|
||||||
|
Options for Bags
|
||||||
|
================
|
||||||
|
|
||||||
|
tuple only
|
||||||
|
|
||||||
|
pros : simple
|
||||||
|
cons :
|
||||||
|
- how to name columns / store headers ?
|
||||||
|
- how to return a dictionary
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
yield keys('foo', 'bar', 'baz')
|
||||||
|
|
||||||
|
|
||||||
|
yield 'a', 'b', 'c'
|
||||||
|
|
||||||
|
|
||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
* Bags changed to something way closer to namedtuples.
|
||||||
|
* Better at managing memory
|
||||||
|
* Less flexible for kwargs usage, but much more standard and portable from one to another version of python
|
||||||
|
* More future proof for different execution strategies
|
||||||
|
* May lead to changes in your current transformation
|
||||||
|
|
||||||
|
* A given transformation now have an input and a output "type" which is either manually set by the user or
|
||||||
|
detected from the first item sent through a queue. It is a restiction on how bonobo can be used, but
|
||||||
|
will help having better predicatability.
|
||||||
|
|
||||||
|
* No more "graph" instance detection. This was misleading for new users, and not really pythonic. The
|
||||||
|
recommended way to start with bonobo is just to use one python file with a __main__ block, and if the
|
||||||
|
project grows, include this file in a package, either new or existing one. The init cli changed to
|
||||||
|
help you generate files or packages. That also means that we do not generate things with cookiecutter
|
||||||
|
anymore.
|
||||||
|
|
||||||
|
* Jupyter enhancements
|
||||||
|
|
||||||
|
* Graphviz support
|
||||||
|
|
||||||
|
* New nodes in stdlib
|
||||||
|
|
||||||
|
* Registry, used for conversions but also for your own integrations.
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
@ -13,16 +13,14 @@ from bonobo.nodes import (
|
|||||||
PickleWriter,
|
PickleWriter,
|
||||||
PrettyPrinter,
|
PrettyPrinter,
|
||||||
RateLimited,
|
RateLimited,
|
||||||
|
SetFields,
|
||||||
Tee,
|
Tee,
|
||||||
Update,
|
|
||||||
arg0_to_kwargs,
|
|
||||||
count,
|
count,
|
||||||
identity,
|
identity,
|
||||||
kwargs_to_arg0,
|
|
||||||
noop,
|
noop,
|
||||||
)
|
)
|
||||||
from bonobo.nodes import LdjsonReader, LdjsonWriter
|
from bonobo.nodes import LdjsonReader, LdjsonWriter
|
||||||
from bonobo.structs import Bag, ErrorBag, Graph, Token
|
from bonobo.structs import Graph
|
||||||
from bonobo.util import get_name
|
from bonobo.util import get_name
|
||||||
from bonobo.util.environ import parse_args, get_argument_parser
|
from bonobo.util.environ import parse_args, get_argument_parser
|
||||||
|
|
||||||
@ -133,7 +131,7 @@ def inspect(graph, *, plugins=None, services=None, strategy=None, format):
|
|||||||
|
|
||||||
|
|
||||||
# data structures
|
# data structures
|
||||||
register_api_group(Bag, ErrorBag, Graph, Token)
|
register_api_group(Graph)
|
||||||
|
|
||||||
# execution strategies
|
# execution strategies
|
||||||
register_api(create_strategy)
|
register_api(create_strategy)
|
||||||
@ -181,12 +179,10 @@ register_api_group(
|
|||||||
PickleWriter,
|
PickleWriter,
|
||||||
PrettyPrinter,
|
PrettyPrinter,
|
||||||
RateLimited,
|
RateLimited,
|
||||||
|
SetFields,
|
||||||
Tee,
|
Tee,
|
||||||
Update,
|
|
||||||
arg0_to_kwargs,
|
|
||||||
count,
|
count,
|
||||||
identity,
|
identity,
|
||||||
kwargs_to_arg0,
|
|
||||||
noop,
|
noop,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
__version__ = '0.6.dev0'
|
__version__ = '0.6.0a0'
|
||||||
|
|||||||
@ -1,23 +1,29 @@
|
|||||||
from bonobo.commands import BaseCommand
|
from bonobo.commands import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
class VersionCommand(BaseCommand):
|
def get_versions(*, all=False, quiet=None):
|
||||||
def handle(self, *, all=False, quiet=False):
|
|
||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.util.pkgs import bonobo_packages
|
from bonobo.util.pkgs import bonobo_packages
|
||||||
|
|
||||||
print(_format_version(bonobo, quiet=quiet))
|
yield _format_version(bonobo, quiet=quiet)
|
||||||
|
|
||||||
if all:
|
if all:
|
||||||
for name in sorted(bonobo_packages):
|
for name in sorted(bonobo_packages):
|
||||||
if name != 'bonobo':
|
if name != 'bonobo':
|
||||||
try:
|
try:
|
||||||
mod = __import__(name.replace('-', '_'))
|
mod = __import__(name.replace('-', '_'))
|
||||||
try:
|
try:
|
||||||
print(_format_version(mod, name=name, quiet=quiet))
|
yield _format_version(mod, name=name, quiet=quiet)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('{} ({})'.format(name, exc))
|
yield '{} ({})'.format(name, exc)
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
print('{} is not importable ({}).'.format(name, exc))
|
yield '{} is not importable ({}).'.format(name, exc)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionCommand(BaseCommand):
|
||||||
|
def handle(self, *, all=False, quiet=False):
|
||||||
|
for line in get_versions(all=all, quiet=quiet):
|
||||||
|
print(line)
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--all', '-a', action='store_true')
|
parser.add_argument('--all', '-a', action='store_true')
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
from bonobo.config.configurables import Configurable
|
from bonobo.config.configurables import Configurable
|
||||||
|
from bonobo.config.functools import transformation_factory
|
||||||
from bonobo.config.options import Method, Option
|
from bonobo.config.options import Method, Option
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor, use_context, use_context_processor, use_raw_input, use_no_input
|
||||||
from bonobo.config.services import Container, Exclusive, Service, requires, create_container
|
from bonobo.config.services import Container, Exclusive, Service, use, create_container
|
||||||
|
from bonobo.util import deprecated_alias
|
||||||
|
|
||||||
use = requires
|
requires = deprecated_alias('requires', use)
|
||||||
|
|
||||||
# Bonobo's Config API
|
# Bonobo's Config API
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -16,5 +18,10 @@ __all__ = [
|
|||||||
'Service',
|
'Service',
|
||||||
'create_container',
|
'create_container',
|
||||||
'requires',
|
'requires',
|
||||||
|
'transformation_factory',
|
||||||
'use',
|
'use',
|
||||||
|
'use_context',
|
||||||
|
'use_context_processor',
|
||||||
|
'use_no_input',
|
||||||
|
'use_raw_input',
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from bonobo.util import isoption, iscontextprocessor, sortedlist
|
|
||||||
from bonobo.errors import AbstractError
|
from bonobo.errors import AbstractError
|
||||||
|
from bonobo.util import isoption, iscontextprocessor, sortedlist
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Configurable',
|
'Configurable',
|
||||||
@ -18,6 +18,7 @@ class ConfigurableMeta(type):
|
|||||||
super().__init__(what, bases, dict)
|
super().__init__(what, bases, dict)
|
||||||
|
|
||||||
cls.__processors = sortedlist()
|
cls.__processors = sortedlist()
|
||||||
|
cls.__processors_cache = None
|
||||||
cls.__methods = sortedlist()
|
cls.__methods = sortedlist()
|
||||||
cls.__options = sortedlist()
|
cls.__options = sortedlist()
|
||||||
cls.__names = set()
|
cls.__names = set()
|
||||||
@ -47,7 +48,9 @@ class ConfigurableMeta(type):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def __processors__(cls):
|
def __processors__(cls):
|
||||||
return (processor for _, processor in cls.__processors)
|
if cls.__processors_cache is None:
|
||||||
|
cls.__processors_cache = [processor for _, processor in cls.__processors]
|
||||||
|
return cls.__processors_cache
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ' '.join((
|
return ' '.join((
|
||||||
@ -65,7 +68,7 @@ except:
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
class PartiallyConfigured(_functools.partial):
|
class PartiallyConfigured(_functools.partial):
|
||||||
@property # TODO XXX cache this shit
|
@property # TODO XXX cache this
|
||||||
def _options_values(self):
|
def _options_values(self):
|
||||||
""" Simulate option values for partially configured objects. """
|
""" Simulate option values for partially configured objects. """
|
||||||
try:
|
try:
|
||||||
@ -142,8 +145,8 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
if len(extraneous):
|
if len(extraneous):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'{}() got {} unexpected option{}: {}.'.format(
|
'{}() got {} unexpected option{}: {}.'.format(
|
||||||
cls.__name__,
|
cls.__name__, len(extraneous), 's'
|
||||||
len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
|
if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -153,8 +156,8 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
if _final:
|
if _final:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'{}() missing {} required option{}: {}.'.format(
|
'{}() missing {} required option{}: {}.'.format(
|
||||||
cls.__name__,
|
cls.__name__, len(missing), 's'
|
||||||
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
|
if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return PartiallyConfigured(cls, *args, **kwargs)
|
return PartiallyConfigured(cls, *args, **kwargs)
|
||||||
@ -189,9 +192,11 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
position += 1
|
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.
|
raise AbstractError(
|
||||||
"""
|
'You must implement the __call__ method in your configurable class {} to actually use it.'.format(
|
||||||
return self.call(*args, **kwargs)
|
type(self).__name__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __options__(self):
|
def __options__(self):
|
||||||
@ -200,6 +205,3 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
@property
|
@property
|
||||||
def __processors__(self):
|
def __processors__(self):
|
||||||
return type(self).__processors__
|
return type(self).__processors__
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
|
||||||
raise AbstractError('Not implemented.')
|
|
||||||
|
|||||||
15
bonobo/config/functools.py
Normal file
15
bonobo/config/functools.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
def transformation_factory(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def _transformation_factory(*args, **kwargs):
|
||||||
|
retval = f(*args, **kwargs)
|
||||||
|
retval.__name__ = f.__name__ + '({})'.format(
|
||||||
|
', '.join(itertools.chain(map(repr, args), ('{}={!r}'.format(k, v) for k, v in kwargs.items())))
|
||||||
|
)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
return _transformation_factory
|
||||||
@ -46,7 +46,7 @@ class Option:
|
|||||||
title = Option(str, required=True, positional=True)
|
title = Option(str, required=True, positional=True)
|
||||||
keyword = Option(str, default='foo')
|
keyword = Option(str, default='foo')
|
||||||
|
|
||||||
def call(self, s):
|
def __call__(self, s):
|
||||||
return self.title + ': ' + s + ' (' + self.keyword + ')'
|
return self.title + ': ' + s + ' (' + self.keyword + ')'
|
||||||
|
|
||||||
example = Example('hello', keyword='bar')
|
example = Example('hello', keyword='bar')
|
||||||
@ -116,6 +116,22 @@ class RemovedOption(Option):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class RenamedOption(Option):
|
||||||
|
def __init__(self, target, *, positional=False):
|
||||||
|
super(RenamedOption, self).__init__(required=False, positional=False)
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
raise ValueError(
|
||||||
|
'Trying to get value from renamed option {}, try getting {} instead.'.format(self.name, self.target)
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
raise ValueError(
|
||||||
|
'Trying to set value of renamed option {}, try setting {} instead.'.format(self.name, self.target)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Method(Option):
|
class Method(Option):
|
||||||
"""
|
"""
|
||||||
A Method is a special callable-valued option, that can be used in three different ways (but for same purpose).
|
A Method is a special callable-valued option, that can be used in three different ways (but for same purpose).
|
||||||
@ -154,9 +170,15 @@ class Method(Option):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, required=True, positional=True, __doc__=None):
|
def __init__(self, *, default=None, required=True, positional=True, __doc__=None):
|
||||||
super().__init__(None, required=required, positional=positional, __doc__=__doc__)
|
super().__init__(None, required=required, positional=positional, __doc__=__doc__)
|
||||||
|
|
||||||
|
# If a callable is provided as default, then use self as if it was used as a decorator
|
||||||
|
if default is not None:
|
||||||
|
if not callable(default):
|
||||||
|
raise ValueError('Method defaults should be callable, if provided.')
|
||||||
|
self(default)
|
||||||
|
|
||||||
def __get__(self, inst, type_):
|
def __get__(self, inst, type_):
|
||||||
x = super(Method, self).__get__(inst, type_)
|
x = super(Method, self).__get__(inst, type_)
|
||||||
if inst:
|
if inst:
|
||||||
@ -164,10 +186,12 @@ class Method(Option):
|
|||||||
return x
|
return x
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
def __set__(self, inst, value):
|
||||||
if not hasattr(value, '__call__'):
|
if not callable(value):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Option of type {!r} is expecting a callable value, got {!r} object (which is not).'.format(
|
'Option {!r} ({}) is expecting a callable value, got {!r} object: {!r}.'.format(
|
||||||
type(self).__name__, type(value).__name__
|
self.name,
|
||||||
|
type(self).__name__,
|
||||||
|
type(value).__name__, value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
inst._options_values[self.name] = self.type(value) if self.type else value
|
inst._options_values[self.name] = self.type(value) if self.type else value
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from functools import partial
|
||||||
|
from inspect import signature
|
||||||
|
|
||||||
from bonobo.config import Option
|
from bonobo.config import Option
|
||||||
|
from bonobo.errors import UnrecoverableTypeError
|
||||||
from bonobo.util import deprecated_alias, ensure_tuple
|
from bonobo.util import deprecated_alias, ensure_tuple
|
||||||
|
|
||||||
_CONTEXT_PROCESSORS_ATTR = '__processors__'
|
_raw = object()
|
||||||
|
_args = object()
|
||||||
|
_none = object()
|
||||||
|
|
||||||
|
INPUT_FORMATS = {_raw, _args, _none}
|
||||||
|
|
||||||
|
|
||||||
class ContextProcessor(Option):
|
class ContextProcessor(Option):
|
||||||
@ -51,18 +58,11 @@ class ContextProcessor(Option):
|
|||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
return self.func(*args, **kwargs)
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def decorate(cls, cls_or_func):
|
|
||||||
try:
|
|
||||||
cls_or_func.__processors__
|
|
||||||
except AttributeError:
|
|
||||||
cls_or_func.__processors__ = []
|
|
||||||
|
|
||||||
def decorator(processor, cls_or_func=cls_or_func):
|
class bound(partial):
|
||||||
cls_or_func.__processors__.append(cls(processor))
|
@property
|
||||||
return cls_or_func
|
def kwargs(self):
|
||||||
|
return self.keywords
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class ContextCurrifier:
|
class ContextCurrifier:
|
||||||
@ -70,18 +70,47 @@ class ContextCurrifier:
|
|||||||
This is a helper to resolve processors.
|
This is a helper to resolve processors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, wrapped, *initial_context):
|
def __init__(self, wrapped, *args, **kwargs):
|
||||||
self.wrapped = wrapped
|
self.wrapped = wrapped
|
||||||
self.context = tuple(initial_context)
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.format = getattr(wrapped, '__input_format__', _args)
|
||||||
self._stack, self._stack_values = None, None
|
self._stack, self._stack_values = None, None
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self.wrapped
|
yield from self.wrapped
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def _bind(self, _input):
|
||||||
if not callable(self.wrapped) and isinstance(self.wrapped, Iterable):
|
try:
|
||||||
|
bind = signature(self.wrapped).bind
|
||||||
|
except ValueError:
|
||||||
|
bind = partial(bound, self.wrapped)
|
||||||
|
if self.format is _args:
|
||||||
|
return bind(*self.args, *_input, **self.kwargs)
|
||||||
|
if self.format is _raw:
|
||||||
|
return bind(*self.args, _input, **self.kwargs)
|
||||||
|
if self.format is _none:
|
||||||
|
return bind(*self.args, **self.kwargs)
|
||||||
|
raise NotImplementedError('Invalid format {!r}.'.format(self.format))
|
||||||
|
|
||||||
|
def __call__(self, _input):
|
||||||
|
if not callable(self.wrapped):
|
||||||
|
if isinstance(self.wrapped, Iterable):
|
||||||
return self.__iter__()
|
return self.__iter__()
|
||||||
return self.wrapped(*self.context, *args, **kwargs)
|
raise UnrecoverableTypeError('Uncallable node {}'.format(self.wrapped))
|
||||||
|
try:
|
||||||
|
bound = self._bind(_input)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise UnrecoverableTypeError((
|
||||||
|
'Input of {wrapped!r} does not bind to the node signature.\n'
|
||||||
|
'Args: {args}\n'
|
||||||
|
'Input: {input}\n'
|
||||||
|
'Kwargs: {kwargs}\n'
|
||||||
|
'Signature: {sig}'
|
||||||
|
).format(
|
||||||
|
wrapped=self.wrapped, args=self.args, input=_input, kwargs=self.kwargs, sig=signature(self.wrapped)
|
||||||
|
)) from exc
|
||||||
|
return self.wrapped(*bound.args, **bound.kwargs)
|
||||||
|
|
||||||
def setup(self, *context):
|
def setup(self, *context):
|
||||||
if self._stack is not None:
|
if self._stack is not None:
|
||||||
@ -89,14 +118,11 @@ class ContextCurrifier:
|
|||||||
|
|
||||||
self._stack, self._stack_values = list(), list()
|
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.args, **self.kwargs)
|
||||||
try:
|
|
||||||
_append_to_context = next(_processed)
|
_append_to_context = next(_processed)
|
||||||
except TypeError as exc:
|
|
||||||
raise TypeError('Context processor should be generators (using yield).') from exc
|
|
||||||
self._stack_values.append(_append_to_context)
|
self._stack_values.append(_append_to_context)
|
||||||
if _append_to_context is not None:
|
if _append_to_context is not None:
|
||||||
self.context += ensure_tuple(_append_to_context)
|
self.args += ensure_tuple(_append_to_context)
|
||||||
self._stack.append(_processed)
|
self._stack.append(_processed)
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
@ -139,3 +165,42 @@ def resolve_processors(mixed):
|
|||||||
|
|
||||||
|
|
||||||
get_context_processors = deprecated_alias('get_context_processors', resolve_processors)
|
get_context_processors = deprecated_alias('get_context_processors', resolve_processors)
|
||||||
|
|
||||||
|
|
||||||
|
def use_context(f):
|
||||||
|
def context(self, context, *args, **kwargs):
|
||||||
|
yield context
|
||||||
|
|
||||||
|
return use_context_processor(context)(f)
|
||||||
|
|
||||||
|
|
||||||
|
def use_context_processor(context_processor):
|
||||||
|
def using_context_processor(cls_or_func):
|
||||||
|
nonlocal context_processor
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls_or_func.__processors__
|
||||||
|
except AttributeError:
|
||||||
|
cls_or_func.__processors__ = []
|
||||||
|
|
||||||
|
cls_or_func.__processors__.append(ContextProcessor(context_processor))
|
||||||
|
return cls_or_func
|
||||||
|
|
||||||
|
return using_context_processor
|
||||||
|
|
||||||
|
|
||||||
|
def _use_input_format(input_format):
|
||||||
|
if input_format not in INPUT_FORMATS:
|
||||||
|
raise ValueError(
|
||||||
|
'Invalid input format {!r}. Choices: {}'.format(input_format, ', '.join(sorted(INPUT_FORMATS)))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_input_format(f):
|
||||||
|
setattr(f, '__input_format__', input_format)
|
||||||
|
return f
|
||||||
|
|
||||||
|
return _set_input_format
|
||||||
|
|
||||||
|
|
||||||
|
use_no_input = _use_input_format(_none)
|
||||||
|
use_raw_input = _use_input_format(_raw)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import inspect
|
||||||
|
import pprint
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import types
|
import types
|
||||||
@ -73,13 +75,13 @@ class Container(dict):
|
|||||||
return cls
|
return cls
|
||||||
return super().__new__(cls, *args, **kwargs)
|
return super().__new__(cls, *args, **kwargs)
|
||||||
|
|
||||||
def args_for(self, mixed):
|
def kwargs_for(self, mixed):
|
||||||
try:
|
try:
|
||||||
options = dict(mixed.__options__)
|
options = dict(mixed.__options__)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
return tuple(option.resolve(mixed, self) for name, option in options.items() if isinstance(option, Service))
|
return {name: option.resolve(mixed, self) for name, option in options.items() if isinstance(option, Service)}
|
||||||
|
|
||||||
def get(self, name, default=None):
|
def get(self, name, default=None):
|
||||||
if not name in self:
|
if not name in self:
|
||||||
@ -156,7 +158,7 @@ class Exclusive(ContextDecorator):
|
|||||||
self.get_lock().release()
|
self.get_lock().release()
|
||||||
|
|
||||||
|
|
||||||
def requires(*service_names):
|
def use(*service_names):
|
||||||
def decorate(mixed):
|
def decorate(mixed):
|
||||||
try:
|
try:
|
||||||
options = mixed.__options__
|
options = mixed.__options__
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
from bonobo.structs.tokens import Token
|
class Token:
|
||||||
|
"""Factory for signal oriented queue messages or other token types."""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.__name__ = name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{}>'.format(self.__name__)
|
||||||
|
|
||||||
|
|
||||||
BEGIN = Token('Begin')
|
BEGIN = Token('Begin')
|
||||||
END = Token('End')
|
END = Token('End')
|
||||||
|
|
||||||
INHERIT_INPUT = Token('InheritInput')
|
INHERIT_INPUT = Token('InheritInput')
|
||||||
LOOPBACK = Token('Loopback')
|
LOOPBACK = Token('Loopback')
|
||||||
NOT_MODIFIED = Token('NotModified')
|
NOT_MODIFIED = Token('NotModified')
|
||||||
DEFAULT_SERVICES_FILENAME = '_services.py'
|
|
||||||
DEFAULT_SERVICES_ATTR = 'get_services'
|
EMPTY = tuple()
|
||||||
|
|
||||||
TICK_PERIOD = 0.2
|
TICK_PERIOD = 0.2
|
||||||
|
|
||||||
ARGNAMES = '_argnames'
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
# https://developers.google.com/api-client-library/python/guide/aaa_oauth
|
||||||
|
# pip install google-api-python-client (1.6.4)
|
||||||
|
|
||||||
import httplib2
|
import httplib2
|
||||||
from apiclient import discovery
|
from apiclient import discovery
|
||||||
from oauth2client import client, tools
|
from oauth2client import client, tools
|
||||||
@ -7,11 +10,10 @@ from oauth2client.file import Storage
|
|||||||
from oauth2client.tools import argparser
|
from oauth2client.tools import argparser
|
||||||
|
|
||||||
HOME_DIR = os.path.expanduser('~')
|
HOME_DIR = os.path.expanduser('~')
|
||||||
GOOGLE_SCOPES = ('https://www.googleapis.com/auth/spreadsheets', )
|
|
||||||
GOOGLE_SECRETS = os.path.join(HOME_DIR, '.cache/secrets/client_secrets.json')
|
GOOGLE_SECRETS = os.path.join(HOME_DIR, '.cache/secrets/client_secrets.json')
|
||||||
|
|
||||||
|
|
||||||
def get_credentials():
|
def get_credentials(*, scopes):
|
||||||
"""Gets valid user credentials from storage.
|
"""Gets valid user credentials from storage.
|
||||||
|
|
||||||
If nothing has been stored, or if the stored credentials are invalid,
|
If nothing has been stored, or if the stored credentials are invalid,
|
||||||
@ -27,8 +29,11 @@ def get_credentials():
|
|||||||
|
|
||||||
store = Storage(credential_path)
|
store = Storage(credential_path)
|
||||||
credentials = store.get()
|
credentials = store.get()
|
||||||
if not credentials or credentials.invalid:
|
|
||||||
flow = client.flow_from_clientsecrets(GOOGLE_SECRETS, GOOGLE_SCOPES)
|
# see https://developers.google.com/api-client-library/python/auth/web-app
|
||||||
|
# kw: "incremental scopes"
|
||||||
|
if not credentials or credentials.invalid or not credentials.has_scopes(scopes):
|
||||||
|
flow = client.flow_from_clientsecrets(GOOGLE_SECRETS, scopes)
|
||||||
flow.user_agent = 'Bonobo ETL (https://www.bonobo-project.org/)'
|
flow.user_agent = 'Bonobo ETL (https://www.bonobo-project.org/)'
|
||||||
flags = argparser.parse_args(['--noauth_local_webserver'])
|
flags = argparser.parse_args(['--noauth_local_webserver'])
|
||||||
credentials = tools.run_flow(flow, store, flags)
|
credentials = tools.run_flow(flow, store, flags)
|
||||||
@ -36,8 +41,15 @@ def get_credentials():
|
|||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
def get_google_spreadsheets_api_client():
|
def get_google_spreadsheets_api_client(scopes=('https://www.googleapis.com/auth/spreadsheets', )):
|
||||||
credentials = get_credentials()
|
credentials = get_credentials(scopes=scopes)
|
||||||
http = credentials.authorize(httplib2.Http())
|
http = credentials.authorize(httplib2.Http())
|
||||||
discoveryUrl = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
|
discoveryUrl = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
|
||||||
return discovery.build('sheets', 'v4', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
|
return discovery.build('sheets', 'v4', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_google_people_api_client(scopes=('https://www.googleapis.com/auth/contacts', )):
|
||||||
|
credentials = get_credentials(scopes=scopes)
|
||||||
|
http = credentials.authorize(httplib2.Http())
|
||||||
|
discoveryUrl = 'https://people.googleapis.com/$discovery/rest?version=v1'
|
||||||
|
return discovery.build('people', 'v1', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
|
||||||
|
|||||||
@ -63,6 +63,10 @@ class UnrecoverableError(Exception):
|
|||||||
because you know that your transformation has no point continuing runnning after a bad event."""
|
because you know that your transformation has no point continuing runnning after a bad event."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnrecoverableTypeError(UnrecoverableError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UnrecoverableValueError(UnrecoverableError, ValueError):
|
class UnrecoverableValueError(UnrecoverableError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import bonobo
|
||||||
|
|
||||||
|
|
||||||
|
def get_argument_parser(parser=None):
|
||||||
|
parser = bonobo.get_argument_parser(parser=parser)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--limit',
|
||||||
|
'-l',
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help='If set, limits the number of processed lines.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--print',
|
||||||
|
'-p',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='If set, pretty prints before writing to output file.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_options(options):
|
||||||
|
_limit = options.pop('limit', None)
|
||||||
|
_print = options.pop('print', False)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_limit': (bonobo.Limit(_limit), ) if _limit else (),
|
||||||
|
'_print': (bonobo.PrettyPrinter(), ) if _print else (),
|
||||||
|
}
|
||||||
|
|||||||
84
bonobo/examples/datasets/__main__.py
Normal file
84
bonobo/examples/datasets/__main__.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import bonobo
|
||||||
|
from bonobo import examples
|
||||||
|
from bonobo.contrib.opendatasoft import OpenDataSoftAPI as ODSReader
|
||||||
|
from bonobo.nodes.basics import UnpackItems, Rename, Format
|
||||||
|
|
||||||
|
|
||||||
|
def get_coffeeshops_graph(graph=None, *, _limit=(), _print=()):
|
||||||
|
graph = graph or bonobo.Graph()
|
||||||
|
|
||||||
|
producer = graph.add_chain(
|
||||||
|
ODSReader(
|
||||||
|
dataset='liste-des-cafes-a-un-euro',
|
||||||
|
netloc='opendata.paris.fr'
|
||||||
|
),
|
||||||
|
*_limit,
|
||||||
|
UnpackItems(0),
|
||||||
|
Rename(
|
||||||
|
name='nom_du_cafe',
|
||||||
|
address='adresse',
|
||||||
|
zipcode='arrondissement'
|
||||||
|
),
|
||||||
|
Format(city='Paris', country='France'),
|
||||||
|
*_print,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Comma separated values.
|
||||||
|
graph.add_chain(
|
||||||
|
bonobo.CsvWriter(
|
||||||
|
'coffeeshops.csv',
|
||||||
|
fields=['name', 'address', 'zipcode', 'city'],
|
||||||
|
delimiter=','
|
||||||
|
),
|
||||||
|
_input=producer.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Name to address JSON
|
||||||
|
# graph.add_chain(
|
||||||
|
# bonobo.JsonWriter(path='coffeeshops.dict.json'),
|
||||||
|
# _input=producer.output,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# Standard JSON
|
||||||
|
graph.add_chain(
|
||||||
|
bonobo.JsonWriter(path='coffeeshops.json'),
|
||||||
|
_input=producer.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Line-delimited JSON
|
||||||
|
graph.add_chain(
|
||||||
|
bonobo.LdjsonWriter(path='coffeeshops.ldjson'),
|
||||||
|
_input=producer.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
all = 'all'
|
||||||
|
graphs = {
|
||||||
|
'coffeeshops': get_coffeeshops_graph,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_services():
|
||||||
|
return {'fs': bonobo.open_fs(bonobo.get_examples_path('datasets'))}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = examples.get_argument_parser()
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument('--all', '-a', action='store_true', default=False)
|
||||||
|
group.add_argument('--target', '-t', choices=graphs.keys(), nargs='+')
|
||||||
|
|
||||||
|
with bonobo.parse_args(parser) as options:
|
||||||
|
graph_options = examples.get_graph_options(options)
|
||||||
|
graph_names = list(
|
||||||
|
sorted(graphs.keys()) if options['all'] else options['target']
|
||||||
|
)
|
||||||
|
|
||||||
|
graph = bonobo.Graph()
|
||||||
|
for name in graph_names:
|
||||||
|
graph = graphs[name](graph, **graph_options)
|
||||||
|
|
||||||
|
bonobo.run(graph, services=get_services())
|
||||||
@ -1,7 +0,0 @@
|
|||||||
from os.path import dirname
|
|
||||||
|
|
||||||
import bonobo
|
|
||||||
|
|
||||||
|
|
||||||
def get_services():
|
|
||||||
return {'fs': bonobo.open_fs(dirname(__file__))}
|
|
||||||
1
bonobo/examples/datasets/coffeeshops.csv
Normal file
1
bonobo/examples/datasets/coffeeshops.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
"['name', 'address', 'zipcode', 'city']"
|
||||||
|
@ -1,182 +1,181 @@
|
|||||||
{"les montparnos": "65 boulevard Pasteur, 75015 Paris, France",
|
[{"zipcode": 75015, "address": "344Vrue Vaugirard", "prix_salle": "-", "geoloc": [48.839512, 2.303007], "name": "Coffee Chope", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303007, 48.839512]}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "city": "Paris", "country": "France"},
|
||||||
"Coffee Chope": "344Vrue Vaugirard, 75015 Paris, France",
|
{"zipcode": 75010, "address": "5, rue d'Alsace", "prix_salle": "-", "geoloc": [48.876737, 2.357601], "name": "Ext\u00e9rieur Quai", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.357601, 48.876737]}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Lea": "5 rue Claude Bernard, 75005 Paris, France",
|
{"zipcode": 75004, "address": "6 Bd henri IV", "prix_salle": "-", "geoloc": [48.850852, 2.362029], "name": "Le Sully", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362029, 48.850852]}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "city": "Paris", "country": "France"},
|
||||||
"Le Bellerive": "71 quai de Seine, 75019 Paris, France",
|
{"zipcode": 75018, "address": "53 rue du ruisseau", "prix_salle": "-", "geoloc": [48.893517, 2.340271], "name": "O q de poule", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.340271, 48.893517]}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "city": "Paris", "country": "France"},
|
||||||
"Le drapeau de la fidelit\u00e9": "21 rue Copreaux, 75015 Paris, France",
|
{"zipcode": 75002, "address": "1 Passage du Grand Cerf", "prix_salle": "-", "geoloc": [48.864655, 2.350089], "name": "Le Pas Sage", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.350089, 48.864655]}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "city": "Paris", "country": "France"},
|
||||||
"O q de poule": "53 rue du ruisseau, 75018 Paris, France",
|
{"zipcode": 75018, "address": "112 Rue Championnet", "prix_salle": "-", "geoloc": [48.895825, 2.339712], "name": "La Renaissance", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339712, 48.895825]}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "city": "Paris", "country": "France"},
|
||||||
"Le caf\u00e9 des amis": "125 rue Blomet, 75015 Paris, France",
|
{"zipcode": 75011, "address": "Rue de la Fontaine au Roi", "prix_salle": "-", "geoloc": [48.868581, 2.373015], "name": "La Caravane", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.373015, 48.868581]}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "city": "Paris", "country": "France"},
|
||||||
"Le chantereine": "51 Rue Victoire, 75009 Paris, France",
|
{"zipcode": 75009, "address": "51 Rue Victoire", "prix_salle": "1", "geoloc": [48.875155, 2.335536], "name": "Le chantereine", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.335536, 48.875155]}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "city": "Paris", "country": "France"},
|
||||||
"Le M\u00fcller": "11 rue Feutrier, 75018 Paris, France",
|
{"zipcode": 75018, "address": "11 rue Feutrier", "prix_salle": "1", "geoloc": [48.886536, 2.346525], "name": "Le M\u00fcller", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346525, 48.886536]}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "city": "Paris", "country": "France"},
|
||||||
"Ext\u00e9rieur Quai": "5, rue d'Alsace, 75010 Paris, France",
|
{"zipcode": 75015, "address": "21 rue Copreaux", "prix_salle": "1", "geoloc": [48.841494, 2.307117], "name": "Le drapeau de la fidelit\u00e9", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.307117, 48.841494]}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "city": "Paris", "country": "France"},
|
||||||
"La Bauloise": "36 rue du hameau, 75015 Paris, France",
|
{"zipcode": 75015, "address": "125 rue Blomet", "prix_salle": "1", "geoloc": [48.839743, 2.296898], "name": "Le caf\u00e9 des amis", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.296898, 48.839743]}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "city": "Paris", "country": "France"},
|
||||||
"Le Dellac": "14 rue Rougemont, 75009 Paris, France",
|
{"zipcode": 75004, "address": "10 rue Saint Martin", "prix_salle": "-", "geoloc": [48.857728, 2.349641], "name": "Le Caf\u00e9 Livres", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349641, 48.857728]}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "city": "Paris", "country": "France"},
|
||||||
"Le Bosquet": "46 avenue Bosquet, 75007 Paris, France",
|
{"zipcode": 75007, "address": "46 avenue Bosquet", "prix_salle": "-", "geoloc": [48.856003, 2.30457], "name": "Le Bosquet", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30457, 48.856003]}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "city": "Paris", "country": "France"},
|
||||||
"Le Sully": "6 Bd henri IV, 75004 Paris, France",
|
{"zipcode": 75018, "address": "12 rue Armand Carrel", "prix_salle": "-", "geoloc": [48.889426, 2.332954], "name": "Le Chaumontois", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332954, 48.889426]}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "city": "Paris", "country": "France"},
|
||||||
"Le Felteu": "1 rue Pecquay, 75004 Paris, France",
|
{"zipcode": 75013, "address": "34 avenue Pierre Mend\u00e8s-France", "prix_salle": "-", "geoloc": [48.838521, 2.370478], "name": "Le Kleemend's", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.370478, 48.838521]}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "city": "Paris", "country": "France"},
|
||||||
"Le bistrot de Ma\u00eblle et Augustin": "42 rue coquill\u00e8re, 75001 Paris, France",
|
{"zipcode": 75012, "address": "202 rue du faubourg st antoine", "prix_salle": "-", "geoloc": [48.849861, 2.385342], "name": "Caf\u00e9 Pierre", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385342, 48.849861]}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "city": "Paris", "country": "France"},
|
||||||
"D\u00e9d\u00e9 la frite": "52 rue Notre-Dame des Victoires, 75002 Paris, France",
|
{"zipcode": 75008, "address": "61 rue de Ponthieu", "prix_salle": "-", "geoloc": [48.872202, 2.304624], "name": "Les Arcades", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.304624, 48.872202]}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "city": "Paris", "country": "France"},
|
||||||
"Cardinal Saint-Germain": "11 boulevard Saint-Germain, 75005 Paris, France",
|
{"zipcode": 75007, "address": "31 rue Saint-Dominique", "prix_salle": "-", "geoloc": [48.859031, 2.320315], "name": "Le Square", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.320315, 48.859031]}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "city": "Paris", "country": "France"},
|
||||||
"Le Reynou": "2 bis quai de la m\u00e9gisserie, 75001 Paris, France",
|
{"zipcode": 75012, "address": "75, avenue Ledru-Rollin", "prix_salle": "-", "geoloc": [48.850092, 2.37463], "name": "Assaporare Dix sur Dix", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.37463, 48.850092]}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "city": "Paris", "country": "France"},
|
||||||
"Aux cadrans": "21 ter boulevard Diderot, 75012 Paris, France",
|
{"zipcode": 75002, "address": "129 boulevard sebastopol", "prix_salle": "-", "geoloc": [48.86805, 2.353313], "name": "Au cerceau d'or", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.353313, 48.86805]}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "city": "Paris", "country": "France"},
|
||||||
"Le Saint Jean": "23 rue des abbesses, 75018 Paris, France",
|
{"zipcode": 75012, "address": "21 ter boulevard Diderot", "prix_salle": "-", "geoloc": [48.845927, 2.373051], "name": "Aux cadrans", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.373051, 48.845927]}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "city": "Paris", "country": "France"},
|
||||||
"La Renaissance": "112 Rue Championnet, 75018 Paris, France",
|
{"zipcode": 75016, "address": "17 rue Jean de la Fontaine", "prix_salle": "-", "geoloc": [48.851662, 2.273883], "name": "Caf\u00e9 antoine", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.273883, 48.851662]}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "city": "Paris", "country": "France"},
|
||||||
"Le Square": "31 rue Saint-Dominique, 75007 Paris, France",
|
{"zipcode": 75008, "address": "rue de Lisbonne", "prix_salle": "-", "geoloc": [48.877642, 2.312823], "name": "Caf\u00e9 de la Mairie (du VIII)", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.312823, 48.877642]}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "city": "Paris", "country": "France"},
|
||||||
"Les Arcades": "61 rue de Ponthieu, 75008 Paris, France",
|
{"zipcode": 75005, "address": "5 rue Claude Bernard", "prix_salle": "-", "geoloc": [48.838633, 2.349916], "name": "Caf\u00e9 Lea", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349916, 48.838633]}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "city": "Paris", "country": "France"},
|
||||||
"Le Kleemend's": "34 avenue Pierre Mend\u00e8s-France, 75013 Paris, France",
|
{"zipcode": 75005, "address": "11 boulevard Saint-Germain", "prix_salle": "-", "geoloc": [48.849293, 2.354486], "name": "Cardinal Saint-Germain", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354486, 48.849293]}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "city": "Paris", "country": "France"},
|
||||||
"Assaporare Dix sur Dix": "75, avenue Ledru-Rollin, 75012 Paris, France",
|
{"zipcode": 75002, "address": "52 rue Notre-Dame des Victoires", "prix_salle": "-", "geoloc": [48.869771, 2.342501], "name": "D\u00e9d\u00e9 la frite", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.342501, 48.869771]}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Pierre": "202 rue du faubourg st antoine, 75012 Paris, France",
|
{"zipcode": 75015, "address": "36 rue du hameau", "prix_salle": "-", "geoloc": [48.834051, 2.287345], "name": "La Bauloise", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.287345, 48.834051]}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 antoine": "17 rue Jean de la Fontaine, 75016 Paris, France",
|
{"zipcode": 75019, "address": "71 quai de Seine", "prix_salle": "-", "geoloc": [48.888165, 2.377387], "name": "Le Bellerive", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.377387, 48.888165]}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "city": "Paris", "country": "France"},
|
||||||
"Au cerceau d'or": "129 boulevard sebastopol, 75002 Paris, France",
|
{"zipcode": 75001, "address": "42 rue coquill\u00e8re", "prix_salle": "-", "geoloc": [48.864543, 2.340997], "name": "Le bistrot de Ma\u00eblle et Augustin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.340997, 48.864543]}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "city": "Paris", "country": "France"},
|
||||||
"La Caravane": "Rue de la Fontaine au Roi, 75011 Paris, France",
|
{"zipcode": 75009, "address": "14 rue Rougemont", "prix_salle": "-", "geoloc": [48.872103, 2.346161], "name": "Le Dellac", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346161, 48.872103]}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "city": "Paris", "country": "France"},
|
||||||
"Le Pas Sage": "1 Passage du Grand Cerf, 75002 Paris, France",
|
{"zipcode": 75004, "address": "1 rue Pecquay", "prix_salle": "-", "geoloc": [48.859645, 2.355598], "name": "Le Felteu", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355598, 48.859645]}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "city": "Paris", "country": "France"},
|
||||||
"Le Caf\u00e9 Livres": "10 rue Saint Martin, 75004 Paris, France",
|
{"zipcode": 75001, "address": "2 bis quai de la m\u00e9gisserie", "prix_salle": "-", "geoloc": [48.85763, 2.346101], "name": "Le Reynou", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346101, 48.85763]}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "city": "Paris", "country": "France"},
|
||||||
"Le Chaumontois": "12 rue Armand Carrel, 75018 Paris, France",
|
{"zipcode": 75018, "address": "23 rue des abbesses", "prix_salle": "-", "geoloc": [48.884646, 2.337734], "name": "Le Saint Jean", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337734, 48.884646]}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "city": "Paris", "country": "France"},
|
||||||
"Drole d'endroit pour une rencontre": "58 rue de Montorgueil, 75002 Paris, France",
|
{"zipcode": 75015, "address": "65 boulevard Pasteur", "prix_salle": "-", "geoloc": [48.841007, 2.31466], "name": "les montparnos", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.31466, 48.841007]}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "city": "Paris", "country": "France"},
|
||||||
"Le pari's caf\u00e9": "104 rue caulaincourt, 75018 Paris, France",
|
{"zipcode": 75006, "address": "16 rue DE MEZIERES", "prix_salle": "-", "geoloc": [48.850323, 2.33039], "name": "L'antre d'eux", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33039, 48.850323]}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "city": "Paris", "country": "France"},
|
||||||
"Le Poulailler": "60 rue saint-sabin, 75011 Paris, France",
|
{"zipcode": 75002, "address": "58 rue de Montorgueil", "prix_salle": "-", "geoloc": [48.864957, 2.346938], "name": "Drole d'endroit pour une rencontre", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346938, 48.864957]}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "city": "Paris", "country": "France"},
|
||||||
"Chai 33": "33 Cour Saint Emilion, 75012 Paris, France",
|
{"zipcode": 75018, "address": "104 rue caulaincourt", "prix_salle": "-", "geoloc": [48.889565, 2.339735], "name": "Le pari's caf\u00e9", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339735, 48.889565]}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "city": "Paris", "country": "France"},
|
||||||
"L'Assassin": "99 rue Jean-Pierre Timbaud, 75011 Paris, France",
|
{"zipcode": 75011, "address": "60 rue saint-sabin", "prix_salle": "-", "geoloc": [48.859115, 2.368871], "name": "Le Poulailler", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.368871, 48.859115]}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "city": "Paris", "country": "France"},
|
||||||
"l'Usine": "1 rue d'Avron, 75020 Paris, France",
|
{"zipcode": 75012, "address": "33 Cour Saint Emilion", "prix_salle": "-", "geoloc": [48.833595, 2.38604], "name": "Chai 33", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.38604, 48.833595]}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "city": "Paris", "country": "France"},
|
||||||
"La Bricole": "52 rue Liebniz, 75018 Paris, France",
|
{"zipcode": 75011, "address": "99 rue Jean-Pierre Timbaud", "prix_salle": "-", "geoloc": [48.868741, 2.379969], "name": "L'Assassin", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379969, 48.868741]}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "city": "Paris", "country": "France"},
|
||||||
"le ronsard": "place maubert, 75005 Paris, France",
|
{"zipcode": 75020, "address": "1 rue d'Avron", "prix_salle": "-", "geoloc": [48.851463, 2.398691], "name": "l'Usine", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.398691, 48.851463]}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "city": "Paris", "country": "France"},
|
||||||
"Face Bar": "82 rue des archives, 75003 Paris, France",
|
{"zipcode": 75018, "address": "52 rue Liebniz", "prix_salle": "-", "geoloc": [48.896305, 2.332898], "name": "La Bricole", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332898, 48.896305]}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "city": "Paris", "country": "France"},
|
||||||
"American Kitchen": "49 rue bichat, 75010 Paris, France",
|
{"zipcode": 75005, "address": "place maubert", "prix_salle": "-", "geoloc": [48.850311, 2.34885], "name": "le ronsard", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.34885, 48.850311]}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "city": "Paris", "country": "France"},
|
||||||
"La Marine": "55 bis quai de valmy, 75010 Paris, France",
|
{"zipcode": 75003, "address": "82 rue des archives", "prix_salle": "-", "geoloc": [48.863038, 2.3604], "name": "Face Bar", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.3604, 48.863038]}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "city": "Paris", "country": "France"},
|
||||||
"Le Bloc": "21 avenue Brochant, 75017 Paris, France",
|
{"zipcode": 75010, "address": "49 rue bichat", "prix_salle": "-", "geoloc": [48.872746, 2.366392], "name": "American Kitchen", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.366392, 48.872746]}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "city": "Paris", "country": "France"},
|
||||||
"La Recoleta au Manoir": "229 avenue Gambetta, 75020 Paris, France",
|
{"zipcode": 75010, "address": "55 bis quai de valmy", "prix_salle": "-", "geoloc": [48.870598, 2.365413], "name": "La Marine", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365413, 48.870598]}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "city": "Paris", "country": "France"},
|
||||||
"Le Pareloup": "80 Rue Saint-Charles, 75015 Paris, France",
|
{"zipcode": 75017, "address": "21 avenue Brochant", "prix_salle": "-", "geoloc": [48.889101, 2.318001], "name": "Le Bloc", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.318001, 48.889101]}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "city": "Paris", "country": "France"},
|
||||||
"La Brasserie Gait\u00e9": "3 rue de la Gait\u00e9, 75014 Paris, France",
|
{"zipcode": 75020, "address": "229 avenue Gambetta", "prix_salle": "-", "geoloc": [48.874697, 2.405421], "name": "La Recoleta au Manoir", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.405421, 48.874697]}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Zen": "46 rue Victoire, 75009 Paris, France",
|
{"zipcode": 75015, "address": "80 Rue Saint-Charles", "prix_salle": "-", "geoloc": [48.847344, 2.286078], "name": "Le Pareloup", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.286078, 48.847344]}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "city": "Paris", "country": "France"},
|
||||||
"O'Breizh": "27 rue de Penthi\u00e8vre, 75008 Paris, France",
|
{"zipcode": 75014, "address": "3 rue de la Gait\u00e9", "prix_salle": "-", "geoloc": [48.840771, 2.324589], "name": "La Brasserie Gait\u00e9", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": null, "geometry": {"type": "Point", "coordinates": [2.324589, 48.840771]}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "city": "Paris", "country": "France"},
|
||||||
"Le Petit Choiseul": "23 rue saint augustin, 75002 Paris, France",
|
{"zipcode": 75009, "address": "46 rue Victoire", "prix_salle": "-", "geoloc": [48.875232, 2.336036], "name": "Caf\u00e9 Zen", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336036, 48.875232]}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "city": "Paris", "country": "France"},
|
||||||
"Invitez vous chez nous": "7 rue Ep\u00e9e de Bois, 75005 Paris, France",
|
{"zipcode": 75008, "address": "27 rue de Penthi\u00e8vre", "prix_salle": "1", "geoloc": [48.872677, 2.315276], "name": "O'Breizh", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315276, 48.872677]}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "city": "Paris", "country": "France"},
|
||||||
"La Cordonnerie": "142 Rue Saint-Denis 75002 Paris, 75002 Paris, France",
|
{"zipcode": 75002, "address": "23 rue saint augustin", "prix_salle": "1", "geoloc": [48.868838, 2.33605], "name": "Le Petit Choiseul", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33605, 48.868838]}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "city": "Paris", "country": "France"},
|
||||||
"Le Supercoin": "3, rue Baudelique, 75018 Paris, France",
|
{"zipcode": 75005, "address": "7 rue Ep\u00e9e de Bois", "prix_salle": "1", "geoloc": [48.841526, 2.351012], "name": "Invitez vous chez nous", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.351012, 48.841526]}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "city": "Paris", "country": "France"},
|
||||||
"Populettes": "86 bis rue Riquet, 75018 Paris, France",
|
{"zipcode": 75002, "address": "142 Rue Saint-Denis 75002 Paris", "prix_salle": "1", "geoloc": [48.86525, 2.350507], "name": "La Cordonnerie", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.350507, 48.86525]}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "city": "Paris", "country": "France"},
|
||||||
"Au bon coin": "49 rue des Cloys, 75018 Paris, France",
|
{"zipcode": 75018, "address": "3, rue Baudelique", "prix_salle": "1", "geoloc": [48.892244, 2.346973], "name": "Le Supercoin", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346973, 48.892244]}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "city": "Paris", "country": "France"},
|
||||||
"Le Couvent": "69 rue Broca, 75013 Paris, France",
|
{"zipcode": 75018, "address": "86 bis rue Riquet", "prix_salle": "1", "geoloc": [48.890043, 2.362241], "name": "Populettes", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362241, 48.890043]}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "city": "Paris", "country": "France"},
|
||||||
"La Br\u00fblerie des Ternes": "111 rue mouffetard, 75005 Paris, France",
|
{"zipcode": 75018, "address": "49 rue des Cloys", "prix_salle": "1", "geoloc": [48.893017, 2.337776], "name": "Au bon coin", "prix_terasse": "1", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337776, 48.893017]}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "city": "Paris", "country": "France"},
|
||||||
"L'\u00c9cir": "59 Boulevard Saint-Jacques, 75014 Paris, France",
|
{"zipcode": 75013, "address": "69 rue Broca", "prix_salle": "1", "geoloc": [48.836919, 2.347003], "name": "Le Couvent", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.347003, 48.836919]}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "city": "Paris", "country": "France"},
|
||||||
"Le Chat bossu": "126, rue du Faubourg Saint Antoine, 75012 Paris, France",
|
{"zipcode": 75005, "address": "111 rue mouffetard", "prix_salle": "1", "geoloc": [48.840624, 2.349766], "name": "La Br\u00fblerie des Ternes", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349766, 48.840624]}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "city": "Paris", "country": "France"},
|
||||||
"Denfert caf\u00e9": "58 boulvevard Saint Jacques, 75014 Paris, France",
|
{"zipcode": 75014, "address": "59 Boulevard Saint-Jacques", "prix_salle": "-", "geoloc": [48.832825, 2.336116], "name": "L'\u00c9cir", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336116, 48.832825]}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "city": "Paris", "country": "France"},
|
||||||
"Le Caf\u00e9 frapp\u00e9": "95 rue Montmartre, 75002 Paris, France",
|
{"zipcode": 75012, "address": "126, rue du Faubourg Saint Antoine", "prix_salle": "-", "geoloc": [48.850696, 2.378417], "name": "Le Chat bossu", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.378417, 48.850696]}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "city": "Paris", "country": "France"},
|
||||||
"La Perle": "78 rue vieille du temple, 75003 Paris, France",
|
{"zipcode": 75014, "address": "58 boulvevard Saint Jacques", "prix_salle": "-", "geoloc": [48.834157, 2.33381], "name": "Denfert caf\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33381, 48.834157]}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "city": "Paris", "country": "France"},
|
||||||
"Le Descartes": "1 rue Thouin, 75005 Paris, France",
|
{"zipcode": 75002, "address": "95 rue Montmartre", "prix_salle": "-", "geoloc": [48.867948, 2.343582], "name": "Le Caf\u00e9 frapp\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.343582, 48.867948]}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "city": "Paris", "country": "France"},
|
||||||
"Bagels & Coffee Corner": "Place de Clichy, 75017 Paris, France",
|
{"zipcode": 75003, "address": "78 rue vieille du temple", "prix_salle": "-", "geoloc": [48.859772, 2.360558], "name": "La Perle", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360558, 48.859772]}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "city": "Paris", "country": "France"},
|
||||||
"Le petit club": "55 rue de la tombe Issoire, 75014 Paris, France",
|
{"zipcode": 75005, "address": "1 rue Thouin", "prix_salle": "-", "geoloc": [48.845047, 2.349583], "name": "Le Descartes", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349583, 48.845047]}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "city": "Paris", "country": "France"},
|
||||||
"Le Plein soleil": "90 avenue Parmentier, 75011 Paris, France",
|
{"zipcode": 75014, "address": "55 rue de la tombe Issoire", "prix_salle": "-", "geoloc": [48.830151, 2.334213], "name": "Le petit club", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.334213, 48.830151]}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "city": "Paris", "country": "France"},
|
||||||
"Le Relais Haussmann": "146, boulevard Haussmann, 75008 Paris, France",
|
{"zipcode": 75011, "address": "90 avenue Parmentier", "prix_salle": "-", "geoloc": [48.865707, 2.374382], "name": "Le Plein soleil", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.374382, 48.865707]}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "city": "Paris", "country": "France"},
|
||||||
"Le Malar": "88 rue Saint-Dominique, 75007 Paris, France",
|
{"zipcode": 75008, "address": "146, boulevard Haussmann", "prix_salle": "-", "geoloc": [48.875322, 2.312329], "name": "Le Relais Haussmann", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.312329, 48.875322]}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "city": "Paris", "country": "France"},
|
||||||
"Au panini de la place": "47 rue Belgrand, 75020 Paris, France",
|
{"zipcode": 75007, "address": "88 rue Saint-Dominique", "prix_salle": "-", "geoloc": [48.859559, 2.30643], "name": "Le Malar", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30643, 48.859559]}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "city": "Paris", "country": "France"},
|
||||||
"Le Village": "182 rue de Courcelles, 75017 Paris, France",
|
{"zipcode": 75020, "address": "47 rue Belgrand", "prix_salle": "-", "geoloc": [48.864628, 2.408038], "name": "Au panini de la place", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.408038, 48.864628]}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "city": "Paris", "country": "France"},
|
||||||
"Pause Caf\u00e9": "41 rue de Charonne, 75011 Paris, France",
|
{"zipcode": 75017, "address": "182 rue de Courcelles", "prix_salle": "-", "geoloc": [48.88435, 2.297978], "name": "Le Village", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.297978, 48.88435]}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "city": "Paris", "country": "France"},
|
||||||
"Le Pure caf\u00e9": "14 rue Jean Mac\u00e9, 75011 Paris, France",
|
{"zipcode": 75011, "address": "41 rue de Charonne", "prix_salle": "-", "geoloc": [48.853381, 2.376706], "name": "Pause Caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.376706, 48.853381]}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "city": "Paris", "country": "France"},
|
||||||
"Extra old caf\u00e9": "307 fg saint Antoine, 75011 Paris, France",
|
{"zipcode": 75011, "address": "14 rue Jean Mac\u00e9", "prix_salle": "-", "geoloc": [48.853253, 2.383415], "name": "Le Pure caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.383415, 48.853253]}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "city": "Paris", "country": "France"},
|
||||||
"Chez Fafa": "44 rue Vinaigriers, 75010 Paris, France",
|
{"zipcode": 75011, "address": "307 fg saint Antoine", "prix_salle": "-", "geoloc": [48.848873, 2.392859], "name": "Extra old caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.392859, 48.848873]}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "city": "Paris", "country": "France"},
|
||||||
"En attendant l'or": "3 rue Faidherbe, 75011 Paris, France",
|
{"zipcode": 75010, "address": "44 rue Vinaigriers", "prix_salle": "-", "geoloc": [48.873227, 2.360787], "name": "Chez Fafa", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360787, 48.873227]}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "city": "Paris", "country": "France"},
|
||||||
"Br\u00fblerie San Jos\u00e9": "30 rue des Petits-Champs, 75002 Paris, France",
|
{"zipcode": 75011, "address": "3 rue Faidherbe", "prix_salle": "-", "geoloc": [48.850836, 2.384069], "name": "En attendant l'or", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.384069, 48.850836]}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 de la Mairie (du VIII)": "rue de Lisbonne, 75008 Paris, France",
|
{"zipcode": 75002, "address": "30 rue des Petits-Champs", "prix_salle": "-", "geoloc": [48.866993, 2.336006], "name": "Br\u00fblerie San Jos\u00e9", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336006, 48.866993]}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Martin": "2 place Martin Nadaud, 75001 Paris, France",
|
{"zipcode": 75001, "address": "2 place Martin Nadaud", "prix_salle": "-", "geoloc": [48.856434, 2.342683], "name": "Caf\u00e9 Martin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.342683, 48.856434]}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "city": "Paris", "country": "France"},
|
||||||
"Etienne": "14 rue Turbigo, Paris, 75001 Paris, France",
|
{"zipcode": 75001, "address": "14 rue Turbigo, Paris", "prix_salle": "-", "geoloc": [48.863675, 2.348701], "name": "Etienne", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.348701, 48.863675]}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "city": "Paris", "country": "France"},
|
||||||
"L'ing\u00e9nu": "184 bd Voltaire, 75011 Paris, France",
|
{"zipcode": 75011, "address": "184 bd Voltaire", "prix_salle": "-", "geoloc": [48.854584, 2.385193], "name": "L'ing\u00e9nu", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385193, 48.854584]}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "city": "Paris", "country": "France"},
|
||||||
"L'Olive": "8 rue L'Olive, 75018 Paris, France",
|
{"zipcode": 75018, "address": "8 rue L'Olive", "prix_salle": "-", "geoloc": [48.890605, 2.361349], "name": "L'Olive", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.361349, 48.890605]}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "city": "Paris", "country": "France"},
|
||||||
"Le Biz": "18 rue Favart, 75002 Paris, France",
|
{"zipcode": 75002, "address": "18 rue Favart", "prix_salle": "-", "geoloc": [48.871396, 2.338321], "name": "Le Biz", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338321, 48.871396]}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "city": "Paris", "country": "France"},
|
||||||
"Le Cap Bourbon": "1 rue Louis le Grand, 75002 Paris, France",
|
{"zipcode": 75002, "address": "1 rue Louis le Grand", "prix_salle": "-", "geoloc": [48.868109, 2.331785], "name": "Le Cap Bourbon", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.331785, 48.868109]}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "city": "Paris", "country": "France"},
|
||||||
"Le General Beuret": "9 Place du General Beuret, 75015 Paris, France",
|
{"zipcode": 75015, "address": "9 Place du General Beuret", "prix_salle": "-", "geoloc": [48.84167, 2.303053], "name": "Le General Beuret", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303053, 48.84167]}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "city": "Paris", "country": "France"},
|
||||||
"Le Germinal": "95 avenue Emile Zola, 75015 Paris, France",
|
{"zipcode": 75015, "address": "95 avenue Emile Zola", "prix_salle": "-", "geoloc": [48.846814, 2.289311], "name": "Le Germinal", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.289311, 48.846814]}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "city": "Paris", "country": "France"},
|
||||||
"Le Ragueneau": "202 rue Saint-Honor\u00e9, 75001 Paris, France",
|
{"zipcode": 75001, "address": "202 rue Saint-Honor\u00e9", "prix_salle": "-", "geoloc": [48.862655, 2.337607], "name": "Le Ragueneau", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337607, 48.862655]}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "city": "Paris", "country": "France"},
|
||||||
"Le refuge": "72 rue lamarck, 75018 Paris, France",
|
{"zipcode": 75018, "address": "72 rue lamarck", "prix_salle": "-", "geoloc": [48.889982, 2.338933], "name": "Le refuge", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338933, 48.889982]}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "city": "Paris", "country": "France"},
|
||||||
"Le sully": "13 rue du Faubourg Saint Denis, 75010 Paris, France",
|
{"zipcode": 75010, "address": "13 rue du Faubourg Saint Denis", "prix_salle": "-", "geoloc": [48.870294, 2.352821], "name": "Le sully", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.352821, 48.870294]}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "city": "Paris", "country": "France"},
|
||||||
"Le Dunois": "77 rue Dunois, 75013 Paris, France",
|
{"zipcode": 75015, "address": "60 rue des bergers", "prix_salle": "-", "geoloc": [48.842128, 2.280374], "name": "Le bal du pirate", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.280374, 48.842128]}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "city": "Paris", "country": "France"},
|
||||||
"La Montagne Sans Genevi\u00e8ve": "13 Rue du Pot de Fer, 75005 Paris, France",
|
{"zipcode": 75012, "address": "95 rue claude decaen", "prix_salle": "-", "geoloc": [48.838769, 2.39609], "name": "zic zinc", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.39609, 48.838769]}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "city": "Paris", "country": "France"},
|
||||||
"Le Caminito": "48 rue du Dessous des Berges, 75013 Paris, France",
|
{"zipcode": 75011, "address": "35 rue de l'orillon", "prix_salle": "-", "geoloc": [48.870247, 2.376306], "name": "l'orillon bar", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.376306, 48.870247]}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "city": "Paris", "country": "France"},
|
||||||
"Le petit Bretonneau": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital, 75018 Paris, France",
|
{"zipcode": 75020, "address": "116 Rue de M\u00e9nilmontant", "prix_salle": "-", "geoloc": [48.869848, 2.394247], "name": "Le Zazabar", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.394247, 48.869848]}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "city": "Paris", "country": "France"},
|
||||||
"La chaumi\u00e8re gourmande": "Route de la Muette \u00e0 Neuilly",
|
{"zipcode": 75005, "address": "22 rue Linn\u00e9", "prix_salle": "-", "geoloc": [48.845458, 2.354796], "name": "L'In\u00e9vitable", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354796, 48.845458]}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "city": "Paris", "country": "France"},
|
||||||
"Club hippique du Jardin d\u2019Acclimatation": "75016 Paris, France",
|
{"zipcode": 75013, "address": "77 rue Dunois", "prix_salle": "-", "geoloc": [48.83336, 2.365782], "name": "Le Dunois", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365782, 48.83336]}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "city": "Paris", "country": "France"},
|
||||||
"Le bal du pirate": "60 rue des bergers, 75015 Paris, France",
|
{"zipcode": 75001, "address": "202 rue Saint Honor\u00e9", "prix_salle": "-", "geoloc": [48.862655, 2.337607], "name": "Ragueneau", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337607, 48.862655]}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "city": "Paris", "country": "France"},
|
||||||
"Le Zazabar": "116 Rue de M\u00e9nilmontant, 75020 Paris, France",
|
{"zipcode": 75013, "address": "48 rue du Dessous des Berges", "prix_salle": "1", "geoloc": [48.826608, 2.374571], "name": "Le Caminito", "prix_terasse": null, "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.374571, 48.826608]}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "city": "Paris", "country": "France"},
|
||||||
"L'antre d'eux": "16 rue DE MEZIERES, 75006 Paris, France",
|
{"zipcode": 75010, "address": "55bis quai de Valmy", "prix_salle": "1", "geoloc": [48.870598, 2.365413], "name": "Epicerie Musicale", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365413, 48.870598]}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "city": "Paris", "country": "France"},
|
||||||
"l'orillon bar": "35 rue de l'orillon, 75011 Paris, France",
|
{"zipcode": 75018, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "prix_salle": "1", "geoloc": null, "name": "Le petit Bretonneau", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "city": "Paris", "country": "France"},
|
||||||
"zic zinc": "95 rue claude decaen, 75012 Paris, France",
|
{"zipcode": 75011, "address": "104 rue amelot", "prix_salle": "1", "geoloc": [48.862575, 2.367427], "name": "Le Centenaire", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.367427, 48.862575]}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "city": "Paris", "country": "France"},
|
||||||
"Les P\u00e8res Populaires": "46 rue de Buzenval, 75020 Paris, France",
|
{"zipcode": 75005, "address": "13 Rue du Pot de Fer", "prix_salle": "1", "geoloc": [48.842833, 2.348314], "name": "La Montagne Sans Genevi\u00e8ve", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": null, "geometry": {"type": "Point", "coordinates": [2.348314, 48.842833]}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "city": "Paris", "country": "France"},
|
||||||
"Epicerie Musicale": "55bis quai de Valmy, 75010 Paris, France",
|
{"zipcode": 75020, "address": "46 rue de Buzenval", "prix_salle": "1", "geoloc": [48.851325, 2.40171], "name": "Les P\u00e8res Populaires", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.40171, 48.851325]}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "city": "Paris", "country": "France"},
|
||||||
"Le relais de la victoire": "73 rue de la Victoire, 75009 Paris, France",
|
{"zipcode": 75007, "address": "188 rue de Grenelle", "prix_salle": "-", "geoloc": [48.857658, 2.305613], "name": "Cafe de grenelle", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.305613, 48.857658]}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "city": "Paris", "country": "France"},
|
||||||
"Le Centenaire": "104 rue amelot, 75011 Paris, France",
|
{"zipcode": 75009, "address": "73 rue de la Victoire", "prix_salle": "-", "geoloc": [48.875207, 2.332944], "name": "Le relais de la victoire", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332944, 48.875207]}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "city": "Paris", "country": "France"},
|
||||||
"Cafe de grenelle": "188 rue de Grenelle, 75007 Paris, France",
|
{"zipcode": 75016, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "prix_salle": "1", "geoloc": null, "name": "La chaumi\u00e8re gourmande", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": null, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "city": "Paris", "country": "France"},
|
||||||
"Ragueneau": "202 rue Saint Honor\u00e9, 75001 Paris, France",
|
{"zipcode": 75018, "address": "216, rue Marcadet", "prix_salle": "-", "geoloc": [48.891882, 2.33365], "name": "Le Brio", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33365, 48.891882]}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Pistache": "9 rue des petits champs, 75001 Paris, France",
|
{"zipcode": 75017, "address": "22 rue des Dames", "prix_salle": "-", "geoloc": [48.884753, 2.324648], "name": "Caves populaires", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324648, 48.884753]}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "city": "Paris", "country": "France"},
|
||||||
"La Cagnotte": "13 Rue Jean-Baptiste Dumay, 75020 Paris, France",
|
{"zipcode": 75014, "address": "12 avenue Jean Moulin", "prix_salle": "-", "geoloc": [48.827428, 2.325652], "name": "Caprice caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.325652, 48.827428]}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "city": "Paris", "country": "France"},
|
||||||
"Le Killy Jen": "28 bis boulevard Diderot, 75012 Paris, France",
|
{"zipcode": 75013, "address": "7 rue Clisson", "prix_salle": "-", "geoloc": [48.832964, 2.369266], "name": "Tamm Bara", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.369266, 48.832964]}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 beauveau": "9 rue de Miromesnil, 75008 Paris, France",
|
{"zipcode": 75009, "address": "1 rue de Montholon", "prix_salle": "-", "geoloc": [48.876577, 2.348414], "name": "L'anjou", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.348414, 48.876577]}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "city": "Paris", "country": "France"},
|
||||||
"le 1 cinq": "172 rue de vaugirard, 75015 Paris, France",
|
{"zipcode": 75007, "address": "2 rue Robert Esnault Pelterie", "prix_salle": "-", "geoloc": [48.862599, 2.315086], "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315086, 48.862599]}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "city": "Paris", "country": "France"},
|
||||||
"Les Artisans": "106 rue Lecourbe, 75015 Paris, France",
|
{"zipcode": 75005, "address": "10 rue d\"Ulm", "prix_salle": "-", "geoloc": [48.844854, 2.345413], "name": "Waikiki", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.345413, 48.844854]}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "city": "Paris", "country": "France"},
|
||||||
"Peperoni": "83 avenue de Wagram, 75001 Paris, France",
|
{"zipcode": 75010, "address": "36 rue Beaurepaire", "prix_salle": "-", "geoloc": [48.871576, 2.364499], "name": "Chez Prune", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.364499, 48.871576]}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "city": "Paris", "country": "France"},
|
||||||
"Le Brio": "216, rue Marcadet, 75018 Paris, France",
|
{"zipcode": 75014, "address": "21 rue Boulard", "prix_salle": "-", "geoloc": [48.833863, 2.329046], "name": "Au Vin Des Rues", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.329046, 48.833863]}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "city": "Paris", "country": "France"},
|
||||||
"Tamm Bara": "7 rue Clisson, 75013 Paris, France",
|
{"zipcode": 75015, "address": "14 rue d'alleray", "prix_salle": "-", "geoloc": [48.838137, 2.301166], "name": "bistrot les timbr\u00e9s", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.301166, 48.838137]}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 dans l'aerogare Air France Invalides": "2 rue Robert Esnault Pelterie, 75007 Paris, France",
|
{"zipcode": 75008, "address": "9 rue de Miromesnil", "prix_salle": "-", "geoloc": [48.871799, 2.315985], "name": "Caf\u00e9 beauveau", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315985, 48.871799]}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "city": "Paris", "country": "France"},
|
||||||
"bistrot les timbr\u00e9s": "14 rue d'alleray, 75015 Paris, France",
|
{"zipcode": 75001, "address": "9 rue des petits champs", "prix_salle": "-", "geoloc": [48.866259, 2.338739], "name": "Caf\u00e9 Pistache", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338739, 48.866259]}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "city": "Paris", "country": "France"},
|
||||||
"Caprice caf\u00e9": "12 avenue Jean Moulin, 75014 Paris, France",
|
{"zipcode": 75020, "address": "13 Rue Jean-Baptiste Dumay", "prix_salle": "-", "geoloc": [48.874605, 2.387738], "name": "La Cagnotte", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.387738, 48.874605]}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "city": "Paris", "country": "France"},
|
||||||
"Caves populaires": "22 rue des Dames, 75017 Paris, France",
|
{"zipcode": 75015, "address": "172 rue de vaugirard", "prix_salle": "-", "geoloc": [48.842462, 2.310919], "name": "le 1 cinq", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.310919, 48.842462]}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "city": "Paris", "country": "France"},
|
||||||
"Au Vin Des Rues": "21 rue Boulard, 75014 Paris, France",
|
{"zipcode": 75012, "address": "28 bis boulevard Diderot", "prix_salle": "-", "geoloc": [48.84591, 2.375543], "name": "Le Killy Jen", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.375543, 48.84591]}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "city": "Paris", "country": "France"},
|
||||||
"Chez Prune": "36 rue Beaurepaire, 75010 Paris, France",
|
{"zipcode": 75015, "address": "106 rue Lecourbe", "prix_salle": "-", "geoloc": [48.842868, 2.303173], "name": "Les Artisans", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303173, 48.842868]}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "city": "Paris", "country": "France"},
|
||||||
"L'In\u00e9vitable": "22 rue Linn\u00e9, 75005 Paris, France",
|
{"zipcode": 75001, "address": "83 avenue de Wagram", "prix_salle": "-", "geoloc": [48.865684, 2.334416], "name": "Peperoni", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.334416, 48.865684]}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "city": "Paris", "country": "France"},
|
||||||
"L'anjou": "1 rue de Montholon, 75009 Paris, France",
|
{"zipcode": 75015, "address": "380 rue de vaugirard", "prix_salle": "-", "geoloc": [48.833146, 2.288834], "name": "le lutece", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.288834, 48.833146]}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "city": "Paris", "country": "France"},
|
||||||
"Botak cafe": "1 rue Paul albert, 75018 Paris, France",
|
{"zipcode": 75018, "address": "16 rue Ganneron", "prix_salle": "-", "geoloc": [48.886431, 2.327429], "name": "Brasiloja", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.327429, 48.886431]}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "city": "Paris", "country": "France"},
|
||||||
"Bistrot Saint-Antoine": "58 rue du Fbg Saint-Antoine, 75012 Paris, France",
|
{"zipcode": 75004, "address": "16 rue de Rivoli", "prix_salle": "-", "geoloc": [48.855711, 2.359491], "name": "Rivolux", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.359491, 48.855711]}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "city": "Paris", "country": "France"},
|
||||||
"Chez Oscar": "11/13 boulevard Beaumarchais, 75004 Paris, France",
|
{"zipcode": 75012, "address": "21 Bis Boulevard Diderot", "prix_salle": "-", "geoloc": [48.845898, 2.372766], "name": "L'europ\u00e9en", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.372766, 48.845898]}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "city": "Paris", "country": "France"},
|
||||||
"Le Piquet": "48 avenue de la Motte Picquet, 75015 Paris, France",
|
{"zipcode": 75003, "address": "39 rue Notre Dame de Nazareth", "prix_salle": "-", "geoloc": [48.867465, 2.357791], "name": "NoMa", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.357791, 48.867465]}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "city": "Paris", "country": "France"},
|
||||||
"L'avant comptoir": "3 carrefour de l'Od\u00e9on, 75006 Paris, France",
|
{"zipcode": 75020, "address": "1 Rue des Envierges", "prix_salle": "-", "geoloc": [48.871595, 2.385858], "name": "O'Paris", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385858, 48.871595]}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "city": "Paris", "country": "France"},
|
||||||
"le chateau d'eau": "67 rue du Ch\u00e2teau d'eau, 75010 Paris, France",
|
{"zipcode": 75010, "address": "16 avenue Richerand", "prix_salle": "-", "geoloc": [48.872402, 2.366532], "name": "Caf\u00e9 Clochette", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.366532, 48.872402]}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "city": "Paris", "country": "France"},
|
||||||
"Les Vendangeurs": "6/8 rue Stanislas, 75006 Paris, France",
|
{"zipcode": 75011, "address": "40 Boulevard Beaumarchais", "prix_salle": "-", "geoloc": [48.856584, 2.368574], "name": "La cantoche de Paname", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.368574, 48.856584]}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "city": "Paris", "country": "France"},
|
||||||
"maison du vin": "52 rue des plantes, 75014 Paris, France",
|
{"zipcode": 75020, "address": "148 Boulevard de Charonne", "prix_salle": "-", "geoloc": [48.856496, 2.394874], "name": "Le Saint Ren\u00e9", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.394874, 48.856496]}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "city": "Paris", "country": "France"},
|
||||||
"Le Tournebride": "104 rue Mouffetard, 75005 Paris, France",
|
{"zipcode": 75012, "address": "196 rue du faubourg saint-antoine", "prix_salle": "1", "geoloc": [48.850055, 2.383908], "name": "La Libert\u00e9", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.383908, 48.850055]}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "city": "Paris", "country": "France"},
|
||||||
"Le Fronton": "63 rue de Ponthieu, 75008 Paris, France",
|
{"zipcode": 75002, "address": "16 rue des Petits Champs", "prix_salle": "1", "geoloc": [48.866737, 2.33716], "name": "Chez Rutabaga", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33716, 48.866737]}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "city": "Paris", "country": "France"},
|
||||||
"Le BB (Bouchon des Batignolles)": "2 rue Lemercier, 75017 Paris, France",
|
{"zipcode": 75017, "address": "2 rue Lemercier", "prix_salle": "1", "geoloc": [48.885367, 2.325325], "name": "Le BB (Bouchon des Batignolles)", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.325325, 48.885367]}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "city": "Paris", "country": "France"},
|
||||||
"La cantine de Zo\u00e9": "136 rue du Faubourg poissonni\u00e8re, 75010 Paris, France",
|
{"zipcode": 75009, "address": "10 rue Rossini", "prix_salle": "1", "geoloc": [48.873175, 2.339193], "name": "La Brocante", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339193, 48.873175]}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "city": "Paris", "country": "France"},
|
||||||
"Chez Rutabaga": "16 rue des Petits Champs, 75002 Paris, France",
|
{"zipcode": 75014, "address": "3 rue Ga\u00eet\u00e9", "prix_salle": "1", "geoloc": [48.840771, 2.324589], "name": "Le Plomb du cantal", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324589, 48.840771]}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "city": "Paris", "country": "France"},
|
||||||
"Les caves populaires": "22 rue des Dames, 75017 Paris, France",
|
{"zipcode": 75017, "address": "22 rue des Dames", "prix_salle": "1", "geoloc": [48.884753, 2.324648], "name": "Les caves populaires", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324648, 48.884753]}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "city": "Paris", "country": "France"},
|
||||||
"Le Plomb du cantal": "3 rue Ga\u00eet\u00e9, 75014 Paris, France",
|
{"zipcode": 75020, "address": "108 rue de M\u00e9nilmontant", "prix_salle": "-", "geoloc": [48.869519, 2.39339], "name": "Chez Luna", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.39339, 48.869519]}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "city": "Paris", "country": "France"},
|
||||||
"Trois pi\u00e8ces cuisine": "101 rue des dames, 75017 Paris, France",
|
{"zipcode": 75019, "address": "1 rue du Plateau", "prix_salle": "-", "geoloc": [48.877903, 2.385365], "name": "Le bar Fleuri", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385365, 48.877903]}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "city": "Paris", "country": "France"},
|
||||||
"La Brocante": "10 rue Rossini, 75009 Paris, France",
|
{"zipcode": 75017, "address": "101 rue des dames", "prix_salle": "-", "geoloc": [48.882939, 2.31809], "name": "Trois pi\u00e8ces cuisine", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.31809, 48.882939]}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "city": "Paris", "country": "France"},
|
||||||
"Le Zinc": "61 avenue de la Motte Picquet, 75015 Paris, France",
|
{"zipcode": 75015, "address": "61 avenue de la Motte Picquet", "prix_salle": "-", "geoloc": [48.849497, 2.298855], "name": "Le Zinc", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.298855, 48.849497]}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "city": "Paris", "country": "France"},
|
||||||
"Chez Luna": "108 rue de M\u00e9nilmontant, 75020 Paris, France",
|
{"zipcode": 75010, "address": "136 rue du Faubourg poissonni\u00e8re", "prix_salle": "-", "geoloc": [48.880669, 2.349964], "name": "La cantine de Zo\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349964, 48.880669]}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "city": "Paris", "country": "France"},
|
||||||
"Le bar Fleuri": "1 rue du Plateau, 75019 Paris, France",
|
{"zipcode": 75006, "address": "6/8 rue Stanislas", "prix_salle": "-", "geoloc": [48.844057, 2.328402], "name": "Les Vendangeurs", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.328402, 48.844057]}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "city": "Paris", "country": "France"},
|
||||||
"La Libert\u00e9": "196 rue du faubourg saint-antoine, 75012 Paris, France",
|
{"zipcode": 75006, "address": "3 carrefour de l'Od\u00e9on", "prix_salle": "-", "geoloc": [48.852053, 2.338779], "name": "L'avant comptoir", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338779, 48.852053]}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "city": "Paris", "country": "France"},
|
||||||
"La cantoche de Paname": "40 Boulevard Beaumarchais, 75011 Paris, France",
|
{"zipcode": 75018, "address": "1 rue Paul albert", "prix_salle": "1", "geoloc": [48.886504, 2.34498], "name": "Botak cafe", "prix_terasse": "1", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.34498, 48.886504]}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "city": "Paris", "country": "France"},
|
||||||
"Le Saint Ren\u00e9": "148 Boulevard de Charonne, 75020 Paris, France",
|
{"zipcode": 75010, "address": "67 rue du Ch\u00e2teau d'eau", "prix_salle": "-", "geoloc": [48.872722, 2.354594], "name": "le chateau d'eau", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354594, 48.872722]}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Clochette": "16 avenue Richerand, 75010 Paris, France",
|
{"zipcode": 75012, "address": "58 rue du Fbg Saint-Antoine", "prix_salle": "-", "geoloc": [48.85192, 2.373229], "name": "Bistrot Saint-Antoine", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.373229, 48.85192]}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "city": "Paris", "country": "France"},
|
||||||
"L'europ\u00e9en": "21 Bis Boulevard Diderot, 75012 Paris, France",
|
{"zipcode": 75004, "address": "11/13 boulevard Beaumarchais", "prix_salle": "-", "geoloc": [48.854685, 2.368487], "name": "Chez Oscar", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.368487, 48.854685]}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "city": "Paris", "country": "France"},
|
||||||
"NoMa": "39 rue Notre Dame de Nazareth, 75003 Paris, France",
|
{"zipcode": 75008, "address": "63 rue de Ponthieu", "prix_salle": "-", "geoloc": [48.87226, 2.304441], "name": "Le Fronton", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.304441, 48.87226]}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "city": "Paris", "country": "France"},
|
||||||
"le lutece": "380 rue de vaugirard, 75015 Paris, France",
|
{"zipcode": 75015, "address": "48 avenue de la Motte Picquet", "prix_salle": "-", "geoloc": [48.851, 2.300378], "name": "Le Piquet", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.300378, 48.851]}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "city": "Paris", "country": "France"},
|
||||||
"O'Paris": "1 Rue des Envierges, 75020 Paris, France",
|
{"zipcode": 75005, "address": "104 rue Mouffetard", "prix_salle": "-", "geoloc": [48.841089, 2.349565], "name": "Le Tournebride", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349565, 48.841089]}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "city": "Paris", "country": "France"},
|
||||||
"Rivolux": "16 rue de Rivoli, 75004 Paris, France",
|
{"zipcode": 75014, "address": "52 rue des plantes", "prix_salle": "-", "geoloc": [48.828704, 2.322074], "name": "maison du vin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.322074, 48.828704]}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "city": "Paris", "country": "France"},
|
||||||
"Brasiloja": "16 rue Ganneron, 75018 Paris, France",
|
{"zipcode": 75005, "address": "11 Quai de la Tournelle", "prix_salle": "-", "geoloc": [48.849821, 2.355337], "name": "Caf\u00e9 rallye tournelles", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355337, 48.849821]}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "city": "Paris", "country": "France"},
|
||||||
"Institut des Cultures d'Islam": "19-23 rue L\u00e9on, 75018 Paris, France",
|
{"zipcode": 75010, "address": "61 rue du ch\u00e2teau d'eau", "prix_salle": "-", "geoloc": [48.872498, 2.355136], "name": "Brasserie le Morvan", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355136, 48.872498]}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "city": "Paris", "country": "France"},
|
||||||
"Canopy Caf\u00e9 associatif": "19 rue Pajol, 75018 Paris, France",
|
{"zipcode": 75019, "address": "6 rue M\u00e9lingue", "prix_salle": "1", "geoloc": [48.874879, 2.386064], "name": "Chez Miamophile", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.386064, 48.874879]}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "city": "Paris", "country": "France"},
|
||||||
"Petits Freres des Pauvres": "47 rue de Batignolles, 75017 Paris, France",
|
{"zipcode": 75011, "address": "18 rue de Crussol", "prix_salle": "-", "geoloc": [48.864269, 2.36858], "name": "Panem", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.36858, 48.864269]}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "city": "Paris", "country": "France"},
|
||||||
"Le Lucernaire": "53 rue Notre-Dame des Champs, 75006 Paris, France",
|
{"zipcode": 75017, "address": "47 rue de Batignolles", "prix_salle": "-", "geoloc": [48.885662, 2.319591], "name": "Petits Freres des Pauvres", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.319591, 48.885662]}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "city": "Paris", "country": "France"},
|
||||||
"L'Angle": "28 rue de Ponthieu, 75008 Paris, France",
|
{"zipcode": 75015, "address": "198 rue de la Convention", "prix_salle": "-", "geoloc": [48.837212, 2.296046], "name": "Caf\u00e9 Dupont", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.296046, 48.837212]}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "city": "Paris", "country": "France"},
|
||||||
"Le Caf\u00e9 d'avant": "35 rue Claude Bernard, 75005 Paris, France",
|
{"zipcode": 75008, "address": "28 rue de Ponthieu", "prix_salle": "1", "geoloc": [48.871002, 2.30879], "name": "L'Angle", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30879, 48.871002]}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Dupont": "198 rue de la Convention, 75015 Paris, France",
|
{"zipcode": 75018, "address": "19-23 rue L\u00e9on", "prix_salle": "1", "geoloc": [48.888023, 2.353467], "name": "Institut des Cultures d'Islam", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.353467, 48.888023]}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "city": "Paris", "country": "France"},
|
||||||
"Le S\u00e9vign\u00e9": "15 rue du Parc Royal, 75003 Paris, France",
|
{"zipcode": 75018, "address": "19 rue Pajol", "prix_salle": "1", "geoloc": [48.886044, 2.360781], "name": "Canopy Caf\u00e9 associatif", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360781, 48.886044]}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "city": "Paris", "country": "France"},
|
||||||
"L'Entracte": "place de l'opera, 75002 Paris, France",
|
{"zipcode": 75002, "address": "place de l'opera", "prix_salle": "-", "geoloc": [48.870287, 2.332491], "name": "L'Entracte", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332491, 48.870287]}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "city": "Paris", "country": "France"},
|
||||||
"Panem": "18 rue de Crussol, 75011 Paris, France",
|
{"zipcode": 75003, "address": "15 rue du Parc Royal", "prix_salle": "-", "geoloc": [48.858709, 2.362701], "name": "Le S\u00e9vign\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362701, 48.858709]}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "city": "Paris", "country": "France"},
|
||||||
"Au pays de Vannes": "34 bis rue de Wattignies, 75012 Paris, France",
|
{"zipcode": 75005, "address": "35 rue Claude Bernard", "prix_salle": "-", "geoloc": [48.839687, 2.347254], "name": "Le Caf\u00e9 d'avant", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.347254, 48.839687]}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "city": "Paris", "country": "France"},
|
||||||
"l'El\u00e9phant du nil": "125 Rue Saint-Antoine, 75004 Paris, France",
|
{"zipcode": 75006, "address": "53 rue Notre-Dame des Champs", "prix_salle": "-", "geoloc": [48.844244, 2.330407], "name": "Le Lucernaire", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.330407, 48.844244]}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "city": "Paris", "country": "France"},
|
||||||
"L'\u00e2ge d'or": "26 rue du Docteur Magnan, 75013 Paris, France",
|
{"zipcode": 75009, "address": "12 rue Blanche", "prix_salle": "-", "geoloc": [48.877599, 2.332111], "name": "Le Brigadier", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332111, 48.877599]}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "city": "Paris", "country": "France"},
|
||||||
"Le Comptoir": "354 bis rue Vaugirard, 75015 Paris, France",
|
{"zipcode": 75013, "address": "26 rue du Docteur Magnan", "prix_salle": "-", "geoloc": [48.826494, 2.359987], "name": "L'\u00e2ge d'or", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.359987, 48.826494]}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "city": "Paris", "country": "France"},
|
||||||
"L'horizon": "93, rue de la Roquette, 75011 Paris, France",
|
{"zipcode": 75017, "address": "Place de Clichy", "prix_salle": "-", "geoloc": [48.883717, 2.326861], "name": "Bagels & Coffee Corner", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.326861, 48.883717]}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "city": "Paris", "country": "France"},
|
||||||
"L'empreinte": "54, avenue Daumesnil, 75012 Paris, France",
|
{"zipcode": 75015, "address": "10 boulevard Victor", "prix_salle": "-", "geoloc": [48.835843, 2.278501], "name": "Caf\u00e9 Victor", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.278501, 48.835843]}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Victor": "10 boulevard Victor, 75015 Paris, France",
|
{"zipcode": 75012, "address": "54, avenue Daumesnil", "prix_salle": "-", "geoloc": [48.845337, 2.379024], "name": "L'empreinte", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379024, 48.845337]}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 Varenne": "36 rue de Varenne, 75007 Paris, France",
|
{"zipcode": 75011, "address": "93, rue de la Roquette", "prix_salle": "-", "geoloc": [48.857312, 2.379055], "name": "L'horizon", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379055, 48.857312]}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "city": "Paris", "country": "France"},
|
||||||
"Le Brigadier": "12 rue Blanche, 75009 Paris, France",
|
{"zipcode": 75012, "address": "34 bis rue de Wattignies", "prix_salle": "-", "geoloc": [48.835878, 2.395723], "name": "Au pays de Vannes", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.395723, 48.835878]}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "city": "Paris", "country": "France"},
|
||||||
"Waikiki": "10 rue d\"Ulm, 75005 Paris, France",
|
{"zipcode": 75007, "address": "36 rue de Varenne", "prix_salle": "-", "geoloc": [48.85413, 2.323539], "name": "Caf\u00e9 Varenne", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.323539, 48.85413]}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "city": "Paris", "country": "France"},
|
||||||
"Le Parc Vaugirard": "358 rue de Vaugirard, 75015 Paris, France",
|
{"zipcode": 75004, "address": "125 Rue Saint-Antoine", "prix_salle": "-", "geoloc": [48.855161, 2.360218], "name": "l'El\u00e9phant du nil", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360218, 48.855161]}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "city": "Paris", "country": "France"},
|
||||||
"Pari's Caf\u00e9": "174 avenue de Clichy, 75017 Paris, France",
|
{"zipcode": 75015, "address": "354 bis rue Vaugirard", "prix_salle": "-", "geoloc": [48.8357, 2.292961], "name": "Le Comptoir", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.292961, 48.8357]}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "city": "Paris", "country": "France"},
|
||||||
"Melting Pot": "3 rue de Lagny, 75020 Paris, France",
|
{"zipcode": 75015, "address": "358 rue de Vaugirard", "prix_salle": "-", "geoloc": [48.835451, 2.292515], "name": "Le Parc Vaugirard", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.292515, 48.835451]}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "city": "Paris", "country": "France"},
|
||||||
"le Zango": "58 rue Daguerre, 75014 Paris, France",
|
{"zipcode": 75014, "address": "58 rue Daguerre", "prix_salle": "-", "geoloc": [48.834972, 2.327007], "name": "le Zango", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.327007, 48.834972]}, "recordid": "e1b54109015316a822747f788128f997a3478050", "city": "Paris", "country": "France"},
|
||||||
"Chez Miamophile": "6 rue M\u00e9lingue, 75019 Paris, France",
|
{"zipcode": 75020, "address": "3 rue de Lagny", "prix_salle": "-", "geoloc": [48.848887, 2.399972], "name": "Melting Pot", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.399972, 48.848887]}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "city": "Paris", "country": "France"},
|
||||||
"Le caf\u00e9 Monde et M\u00e9dias": "Place de la R\u00e9publique, 75003 Paris, France",
|
{"zipcode": 75017, "address": "174 avenue de Clichy", "prix_salle": "-", "geoloc": [48.892366, 2.317359], "name": "Pari's Caf\u00e9", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.317359, 48.892366]}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "city": "Paris", "country": "France"},
|
||||||
"Caf\u00e9 rallye tournelles": "11 Quai de la Tournelle, 75005 Paris, France",
|
{"zipcode": 75012, "address": "157 rue Bercy 75012 Paris", "prix_salle": "-", "geoloc": [48.842146, 2.375986], "name": "L'entrep\u00f4t", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.375986, 48.842146]}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "city": "Paris", "country": "France"},
|
||||||
"Brasserie le Morvan": "61 rue du ch\u00e2teau d'eau, 75010 Paris, France",
|
{"zipcode": 75003, "address": "Place de la R\u00e9publique", "prix_salle": "-", "geoloc": [48.867092, 2.363288], "name": "Le caf\u00e9 Monde et M\u00e9dias", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.363288, 48.867092]}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "city": "Paris", "country": "France"}]
|
||||||
"L'entrep\u00f4t": "157 rue Bercy 75012 Paris, 75012 Paris, France"}
|
|
||||||
181
bonobo/examples/datasets/coffeeshops.ldjson
Normal file
181
bonobo/examples/datasets/coffeeshops.ldjson
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
{"zipcode": 75015, "address": "344Vrue Vaugirard", "prix_salle": "-", "geoloc": [48.839512, 2.303007], "name": "Coffee Chope", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303007, 48.839512]}, "recordid": "3c276428d45ad68ccdf6875e4ddcfe95d0c0d4cf", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "5, rue d'Alsace", "prix_salle": "-", "geoloc": [48.876737, 2.357601], "name": "Ext\u00e9rieur Quai", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.357601, 48.876737]}, "recordid": "97ad81cd1127a8566085ad796eeb44a06bec7514", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "6 Bd henri IV", "prix_salle": "-", "geoloc": [48.850852, 2.362029], "name": "Le Sully", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362029, 48.850852]}, "recordid": "aa4294c1b8d660a23db0dc81321e509bae1dae68", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "53 rue du ruisseau", "prix_salle": "-", "geoloc": [48.893517, 2.340271], "name": "O q de poule", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.340271, 48.893517]}, "recordid": "a81362dbed35247555fb105bd83ff2906904a66e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "1 Passage du Grand Cerf", "prix_salle": "-", "geoloc": [48.864655, 2.350089], "name": "Le Pas Sage", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.350089, 48.864655]}, "recordid": "7ced86acbd5ccfba229bcc07d70d0d117aee16a5", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "112 Rue Championnet", "prix_salle": "-", "geoloc": [48.895825, 2.339712], "name": "La Renaissance", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339712, 48.895825]}, "recordid": "5582c8572bd7637bf305b74c1c0bdb74a8e4247f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "Rue de la Fontaine au Roi", "prix_salle": "-", "geoloc": [48.868581, 2.373015], "name": "La Caravane", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.373015, 48.868581]}, "recordid": "50bb0fa06e562a242f115ddbdae2ed9c7df93d57", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "51 Rue Victoire", "prix_salle": "1", "geoloc": [48.875155, 2.335536], "name": "Le chantereine", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.335536, 48.875155]}, "recordid": "eb8a62feeedaf7ed8b8c912305270ee857068689", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "11 rue Feutrier", "prix_salle": "1", "geoloc": [48.886536, 2.346525], "name": "Le M\u00fcller", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346525, 48.886536]}, "recordid": "62c552f167f671f88569c1f2d6a44098fb514c51", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "21 rue Copreaux", "prix_salle": "1", "geoloc": [48.841494, 2.307117], "name": "Le drapeau de la fidelit\u00e9", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.307117, 48.841494]}, "recordid": "5120ea0b9d7387766072b90655166486928e25c8", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "125 rue Blomet", "prix_salle": "1", "geoloc": [48.839743, 2.296898], "name": "Le caf\u00e9 des amis", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.296898, 48.839743]}, "recordid": "865f62415adc5c34e3ca38a1748b7a324dfba209", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "10 rue Saint Martin", "prix_salle": "-", "geoloc": [48.857728, 2.349641], "name": "Le Caf\u00e9 Livres", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349641, 48.857728]}, "recordid": "7ef54a78802d49cafd2701458df2b0d0530d123b", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "46 avenue Bosquet", "prix_salle": "-", "geoloc": [48.856003, 2.30457], "name": "Le Bosquet", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30457, 48.856003]}, "recordid": "d701a759e08a71f4bbb01f29473274b0152135d0", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "12 rue Armand Carrel", "prix_salle": "-", "geoloc": [48.889426, 2.332954], "name": "Le Chaumontois", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332954, 48.889426]}, "recordid": "e12ff00a644c91ad910ddc63a770c190be93a393", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "34 avenue Pierre Mend\u00e8s-France", "prix_salle": "-", "geoloc": [48.838521, 2.370478], "name": "Le Kleemend's", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.370478, 48.838521]}, "recordid": "0f6cd1ee7751b00c9574efcfdcf66fa0e857d251", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "202 rue du faubourg st antoine", "prix_salle": "-", "geoloc": [48.849861, 2.385342], "name": "Caf\u00e9 Pierre", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385342, 48.849861]}, "recordid": "f9de9d0fb5e92f047a6f1986a31f9dd4d38bcb36", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "61 rue de Ponthieu", "prix_salle": "-", "geoloc": [48.872202, 2.304624], "name": "Les Arcades", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.304624, 48.872202]}, "recordid": "67eaf58afc856077c0680601e453e75c0922c9c0", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "31 rue Saint-Dominique", "prix_salle": "-", "geoloc": [48.859031, 2.320315], "name": "Le Square", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.320315, 48.859031]}, "recordid": "678558317bc9ad46652e5b1643e70c2142a76e7e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "75, avenue Ledru-Rollin", "prix_salle": "-", "geoloc": [48.850092, 2.37463], "name": "Assaporare Dix sur Dix", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.37463, 48.850092]}, "recordid": "667474321887d08a3cc636adf043ad354b65fa61", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "129 boulevard sebastopol", "prix_salle": "-", "geoloc": [48.86805, 2.353313], "name": "Au cerceau d'or", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.353313, 48.86805]}, "recordid": "c9ef52ba2fabe0286700329f18bbbbea9a10b474", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "21 ter boulevard Diderot", "prix_salle": "-", "geoloc": [48.845927, 2.373051], "name": "Aux cadrans", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.373051, 48.845927]}, "recordid": "ed5f98686856bf4ddd2b381b43ad229246741a90", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75016, "address": "17 rue Jean de la Fontaine", "prix_salle": "-", "geoloc": [48.851662, 2.273883], "name": "Caf\u00e9 antoine", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.273883, 48.851662]}, "recordid": "ab6d1e054e2e6ae7d6150013173f55e83c05ca23", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "rue de Lisbonne", "prix_salle": "-", "geoloc": [48.877642, 2.312823], "name": "Caf\u00e9 de la Mairie (du VIII)", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.312823, 48.877642]}, "recordid": "7de8a79b026ac63f453556612505b5bcd9229036", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "5 rue Claude Bernard", "prix_salle": "-", "geoloc": [48.838633, 2.349916], "name": "Caf\u00e9 Lea", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349916, 48.838633]}, "recordid": "fecd8900cf83027f74ceced9fc4ad80ac73b63a7", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "11 boulevard Saint-Germain", "prix_salle": "-", "geoloc": [48.849293, 2.354486], "name": "Cardinal Saint-Germain", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354486, 48.849293]}, "recordid": "e4a078c30c98082896787f4e4b41a07554392529", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "52 rue Notre-Dame des Victoires", "prix_salle": "-", "geoloc": [48.869771, 2.342501], "name": "D\u00e9d\u00e9 la frite", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.342501, 48.869771]}, "recordid": "ccb2ba2f98043e8eefd5fda829dee1ea7f1d2c7a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "36 rue du hameau", "prix_salle": "-", "geoloc": [48.834051, 2.287345], "name": "La Bauloise", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.287345, 48.834051]}, "recordid": "c9fe10abd15ede7ccaeb55c309898d30d7b19d0e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75019, "address": "71 quai de Seine", "prix_salle": "-", "geoloc": [48.888165, 2.377387], "name": "Le Bellerive", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.377387, 48.888165]}, "recordid": "4e0b5c2d33d7c25fc54c51171f3d37e509959fc0", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "42 rue coquill\u00e8re", "prix_salle": "-", "geoloc": [48.864543, 2.340997], "name": "Le bistrot de Ma\u00eblle et Augustin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.340997, 48.864543]}, "recordid": "52acab12469af291984e9a70962e08c72b058e10", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "14 rue Rougemont", "prix_salle": "-", "geoloc": [48.872103, 2.346161], "name": "Le Dellac", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346161, 48.872103]}, "recordid": "4d1d627ecea2ffa279bb862f8ba495d95ca75350", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "1 rue Pecquay", "prix_salle": "-", "geoloc": [48.859645, 2.355598], "name": "Le Felteu", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355598, 48.859645]}, "recordid": "2c1fa55460af282266d86fd003af4f929fdf4e7d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "2 bis quai de la m\u00e9gisserie", "prix_salle": "-", "geoloc": [48.85763, 2.346101], "name": "Le Reynou", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346101, 48.85763]}, "recordid": "d4ddd30ab3e721a317fc7ea89d5b9001255ce9f4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "23 rue des abbesses", "prix_salle": "-", "geoloc": [48.884646, 2.337734], "name": "Le Saint Jean", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337734, 48.884646]}, "recordid": "51b47cf167b7f32eeebb108330956694d75d4268", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "65 boulevard Pasteur", "prix_salle": "-", "geoloc": [48.841007, 2.31466], "name": "les montparnos", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.31466, 48.841007]}, "recordid": "2aaca891ffd0694c657a43889516ab72afdfba07", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75006, "address": "16 rue DE MEZIERES", "prix_salle": "-", "geoloc": [48.850323, 2.33039], "name": "L'antre d'eux", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33039, 48.850323]}, "recordid": "4ff4337934c66f61e00d1d9551f7cdddba03e544", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "58 rue de Montorgueil", "prix_salle": "-", "geoloc": [48.864957, 2.346938], "name": "Drole d'endroit pour une rencontre", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346938, 48.864957]}, "recordid": "3451657f880abe75d0c7e386fc698405556c53e8", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "104 rue caulaincourt", "prix_salle": "-", "geoloc": [48.889565, 2.339735], "name": "Le pari's caf\u00e9", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339735, 48.889565]}, "recordid": "e8c34a537b673fcb26c76e02deca4f5a728929dc", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "60 rue saint-sabin", "prix_salle": "-", "geoloc": [48.859115, 2.368871], "name": "Le Poulailler", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.368871, 48.859115]}, "recordid": "325ea74ba83f716dde87c08cffd36f7df7722a49", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "33 Cour Saint Emilion", "prix_salle": "-", "geoloc": [48.833595, 2.38604], "name": "Chai 33", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.38604, 48.833595]}, "recordid": "528de8d5d8780bee83145637e315483d48f5ae3c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "99 rue Jean-Pierre Timbaud", "prix_salle": "-", "geoloc": [48.868741, 2.379969], "name": "L'Assassin", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379969, 48.868741]}, "recordid": "fac0483890ff8bdaeb3feddbdb032c5112f24678", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "1 rue d'Avron", "prix_salle": "-", "geoloc": [48.851463, 2.398691], "name": "l'Usine", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.398691, 48.851463]}, "recordid": "fee1e3eb103bbc98e19e45d34365da0f27166541", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "52 rue Liebniz", "prix_salle": "-", "geoloc": [48.896305, 2.332898], "name": "La Bricole", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332898, 48.896305]}, "recordid": "4744e866c244c59eec43b3fe159542d2ef433065", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "place maubert", "prix_salle": "-", "geoloc": [48.850311, 2.34885], "name": "le ronsard", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.34885, 48.850311]}, "recordid": "49a390322b45246bc2c1e50fcd46815ad271bca0", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75003, "address": "82 rue des archives", "prix_salle": "-", "geoloc": [48.863038, 2.3604], "name": "Face Bar", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.3604, 48.863038]}, "recordid": "d96e16ebf2460bb2f6c34198918a071233725cbc", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "49 rue bichat", "prix_salle": "-", "geoloc": [48.872746, 2.366392], "name": "American Kitchen", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.366392, 48.872746]}, "recordid": "6b9395475cbbbbbacbaaeb070f71d31c2d183dc4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "55 bis quai de valmy", "prix_salle": "-", "geoloc": [48.870598, 2.365413], "name": "La Marine", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365413, 48.870598]}, "recordid": "d4d2f92d27f38de59e57744f434781e61283551c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "21 avenue Brochant", "prix_salle": "-", "geoloc": [48.889101, 2.318001], "name": "Le Bloc", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.318001, 48.889101]}, "recordid": "e425882ee969d1e8bffe7234336ae40da88c8439", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "229 avenue Gambetta", "prix_salle": "-", "geoloc": [48.874697, 2.405421], "name": "La Recoleta au Manoir", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.405421, 48.874697]}, "recordid": "02de82cffb2918beafb740f4e924029d470b07a1", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "80 Rue Saint-Charles", "prix_salle": "-", "geoloc": [48.847344, 2.286078], "name": "Le Pareloup", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.286078, 48.847344]}, "recordid": "0227ca95f76bb6097ae0a0e6f455af2624d49ae3", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "3 rue de la Gait\u00e9", "prix_salle": "-", "geoloc": [48.840771, 2.324589], "name": "La Brasserie Gait\u00e9", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": null, "geometry": {"type": "Point", "coordinates": [2.324589, 48.840771]}, "recordid": "e7c4cba08749c892a73db2715d06623d9e0c2f67", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "46 rue Victoire", "prix_salle": "-", "geoloc": [48.875232, 2.336036], "name": "Caf\u00e9 Zen", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336036, 48.875232]}, "recordid": "5e9a6172f8b64cd098a5f2cf9b1d42567fe0a894", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "27 rue de Penthi\u00e8vre", "prix_salle": "1", "geoloc": [48.872677, 2.315276], "name": "O'Breizh", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315276, 48.872677]}, "recordid": "ead3108add27ef41bb92517aca834f7d7f632816", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "23 rue saint augustin", "prix_salle": "1", "geoloc": [48.868838, 2.33605], "name": "Le Petit Choiseul", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33605, 48.868838]}, "recordid": "de601277ca00567b62fa4f5277e4a17679faa753", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "7 rue Ep\u00e9e de Bois", "prix_salle": "1", "geoloc": [48.841526, 2.351012], "name": "Invitez vous chez nous", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.351012, 48.841526]}, "recordid": "1d2373bdec8a07306298e3ee54894ac295ee1d55", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "142 Rue Saint-Denis 75002 Paris", "prix_salle": "1", "geoloc": [48.86525, 2.350507], "name": "La Cordonnerie", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.350507, 48.86525]}, "recordid": "5c9bf60617a99ad75445b454f98e75e1a104021d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "3, rue Baudelique", "prix_salle": "1", "geoloc": [48.892244, 2.346973], "name": "Le Supercoin", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.346973, 48.892244]}, "recordid": "68a4ee10f1fc4d2a659501e811d148420fa80e95", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "86 bis rue Riquet", "prix_salle": "1", "geoloc": [48.890043, 2.362241], "name": "Populettes", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362241, 48.890043]}, "recordid": "8cc55d58d72621a7e91cf6b456731d2cb2863afc", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "49 rue des Cloys", "prix_salle": "1", "geoloc": [48.893017, 2.337776], "name": "Au bon coin", "prix_terasse": "1", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337776, 48.893017]}, "recordid": "0408f272c08c52e3cae035ffdeb8928698787ea9", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "69 rue Broca", "prix_salle": "1", "geoloc": [48.836919, 2.347003], "name": "Le Couvent", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.347003, 48.836919]}, "recordid": "729b2f228d3fd2db6f78bb624d451f59555e4a04", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "111 rue mouffetard", "prix_salle": "1", "geoloc": [48.840624, 2.349766], "name": "La Br\u00fblerie des Ternes", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349766, 48.840624]}, "recordid": "233dd2a17620cd5eae70fef11cc627748e3313d5", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "59 Boulevard Saint-Jacques", "prix_salle": "-", "geoloc": [48.832825, 2.336116], "name": "L'\u00c9cir", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336116, 48.832825]}, "recordid": "4a44324a5a806801fd3e05af89cad7c0f1e69d1e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "126, rue du Faubourg Saint Antoine", "prix_salle": "-", "geoloc": [48.850696, 2.378417], "name": "Le Chat bossu", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.378417, 48.850696]}, "recordid": "d1d02463f7c90d38cccffd898e15f51e65910baf", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "58 boulvevard Saint Jacques", "prix_salle": "-", "geoloc": [48.834157, 2.33381], "name": "Denfert caf\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33381, 48.834157]}, "recordid": "f78f406e5ccb95cb902ce618ed54eba1d4776a3c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "95 rue Montmartre", "prix_salle": "-", "geoloc": [48.867948, 2.343582], "name": "Le Caf\u00e9 frapp\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.343582, 48.867948]}, "recordid": "4dd2a924a7b2c5f061cecaba2f272548a8c83c6c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75003, "address": "78 rue vieille du temple", "prix_salle": "-", "geoloc": [48.859772, 2.360558], "name": "La Perle", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360558, 48.859772]}, "recordid": "476c54e643613ec36a9b1c533f32122fd873f3c3", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "1 rue Thouin", "prix_salle": "-", "geoloc": [48.845047, 2.349583], "name": "Le Descartes", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349583, 48.845047]}, "recordid": "e9cb0b0d6b6b512e9ecab889267ba342a4f0ea93", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "55 rue de la tombe Issoire", "prix_salle": "-", "geoloc": [48.830151, 2.334213], "name": "Le petit club", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.334213, 48.830151]}, "recordid": "c6ca1166fa7fd7050f5d1c626a1e17050116c91e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "90 avenue Parmentier", "prix_salle": "-", "geoloc": [48.865707, 2.374382], "name": "Le Plein soleil", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.374382, 48.865707]}, "recordid": "95cf5fb735bd19826db70a3af4fe72fce647d4e5", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "146, boulevard Haussmann", "prix_salle": "-", "geoloc": [48.875322, 2.312329], "name": "Le Relais Haussmann", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.312329, 48.875322]}, "recordid": "6c5f68f47c916638342b77bd5edfc30bd7051303", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "88 rue Saint-Dominique", "prix_salle": "-", "geoloc": [48.859559, 2.30643], "name": "Le Malar", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30643, 48.859559]}, "recordid": "c633cb01686aa5f3171bf59976dc9b7f23cbca54", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "47 rue Belgrand", "prix_salle": "-", "geoloc": [48.864628, 2.408038], "name": "Au panini de la place", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.408038, 48.864628]}, "recordid": "66dedd35fcbbbb24f328882d49098de7aa5f26ba", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "182 rue de Courcelles", "prix_salle": "-", "geoloc": [48.88435, 2.297978], "name": "Le Village", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.297978, 48.88435]}, "recordid": "1e07eaf8a93875906d0b18eb6a897c651943589a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "41 rue de Charonne", "prix_salle": "-", "geoloc": [48.853381, 2.376706], "name": "Pause Caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.376706, 48.853381]}, "recordid": "60d98c3236a70824df50e9aca83e7d7f13a310c5", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "14 rue Jean Mac\u00e9", "prix_salle": "-", "geoloc": [48.853253, 2.383415], "name": "Le Pure caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.383415, 48.853253]}, "recordid": "66707fb2e707d2145fc2eb078a1b980a45921616", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "307 fg saint Antoine", "prix_salle": "-", "geoloc": [48.848873, 2.392859], "name": "Extra old caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.392859, 48.848873]}, "recordid": "039ec7dcb219cfc434547b06938ba497afeb83b4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "44 rue Vinaigriers", "prix_salle": "-", "geoloc": [48.873227, 2.360787], "name": "Chez Fafa", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360787, 48.873227]}, "recordid": "1572d199f186bf86d7753fe71ac23477a7a8bd2c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "3 rue Faidherbe", "prix_salle": "-", "geoloc": [48.850836, 2.384069], "name": "En attendant l'or", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.384069, 48.850836]}, "recordid": "de5789bb4a4ffbd244cded8dc555639dbe7d2279", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "30 rue des Petits-Champs", "prix_salle": "-", "geoloc": [48.866993, 2.336006], "name": "Br\u00fblerie San Jos\u00e9", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.336006, 48.866993]}, "recordid": "b736e5fa17396ee56a642212ccd0ab29c7f2bef1", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "2 place Martin Nadaud", "prix_salle": "-", "geoloc": [48.856434, 2.342683], "name": "Caf\u00e9 Martin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.342683, 48.856434]}, "recordid": "25fbf857029c54d57909c158e3039349b77344ed", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "14 rue Turbigo, Paris", "prix_salle": "-", "geoloc": [48.863675, 2.348701], "name": "Etienne", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.348701, 48.863675]}, "recordid": "0190edd7b0766c6d3e43093deb41abe9446c1b22", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "184 bd Voltaire", "prix_salle": "-", "geoloc": [48.854584, 2.385193], "name": "L'ing\u00e9nu", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385193, 48.854584]}, "recordid": "7fcef7475ec632d5c61c9c36c1d52a402ad2e9e8", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "8 rue L'Olive", "prix_salle": "-", "geoloc": [48.890605, 2.361349], "name": "L'Olive", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.361349, 48.890605]}, "recordid": "35286273f281c8c5082b3d3bd17f6bbf207426f9", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "18 rue Favart", "prix_salle": "-", "geoloc": [48.871396, 2.338321], "name": "Le Biz", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338321, 48.871396]}, "recordid": "cf2f6b9e283aaeeca2214cc1fe57b45e45668e25", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "1 rue Louis le Grand", "prix_salle": "-", "geoloc": [48.868109, 2.331785], "name": "Le Cap Bourbon", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.331785, 48.868109]}, "recordid": "339030e95e0846b41a8e2b91045456c4e4a50043", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "9 Place du General Beuret", "prix_salle": "-", "geoloc": [48.84167, 2.303053], "name": "Le General Beuret", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303053, 48.84167]}, "recordid": "12b37036d28d28b32ebe81756ad15eb68372947f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "95 avenue Emile Zola", "prix_salle": "-", "geoloc": [48.846814, 2.289311], "name": "Le Germinal", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.289311, 48.846814]}, "recordid": "4e03831a64e886a28a7232e54f2812c1ced23c5a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "202 rue Saint-Honor\u00e9", "prix_salle": "-", "geoloc": [48.862655, 2.337607], "name": "Le Ragueneau", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337607, 48.862655]}, "recordid": "e303131b2600d4c2287749a36bf7193d2fa60bd7", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "72 rue lamarck", "prix_salle": "-", "geoloc": [48.889982, 2.338933], "name": "Le refuge", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338933, 48.889982]}, "recordid": "f64310461736a769c6854fdefb99b9f2e7b230a9", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "13 rue du Faubourg Saint Denis", "prix_salle": "-", "geoloc": [48.870294, 2.352821], "name": "Le sully", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.352821, 48.870294]}, "recordid": "166be8588ff16e50838fa6a164d1e580497b795d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "60 rue des bergers", "prix_salle": "-", "geoloc": [48.842128, 2.280374], "name": "Le bal du pirate", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.280374, 48.842128]}, "recordid": "93ff6e35406a074a6ba2667d2b286abf91132f6a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "95 rue claude decaen", "prix_salle": "-", "geoloc": [48.838769, 2.39609], "name": "zic zinc", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.39609, 48.838769]}, "recordid": "8fdc739d64ff1f01973235301e3ec86791016759", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "35 rue de l'orillon", "prix_salle": "-", "geoloc": [48.870247, 2.376306], "name": "l'orillon bar", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.376306, 48.870247]}, "recordid": "6e6e9695ef04c3fdbd4cfa19f0cfb0c0f93f4933", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "116 Rue de M\u00e9nilmontant", "prix_salle": "-", "geoloc": [48.869848, 2.394247], "name": "Le Zazabar", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.394247, 48.869848]}, "recordid": "22edfe92a72ce1bb0eea972508b5caa7af2db2df", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "22 rue Linn\u00e9", "prix_salle": "-", "geoloc": [48.845458, 2.354796], "name": "L'In\u00e9vitable", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354796, 48.845458]}, "recordid": "6893cb08e99319091de9ba80305f22e0ce4cc08d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "77 rue Dunois", "prix_salle": "-", "geoloc": [48.83336, 2.365782], "name": "Le Dunois", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365782, 48.83336]}, "recordid": "c3be1246dbc4ca5734d5bbd569436ba655105248", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "202 rue Saint Honor\u00e9", "prix_salle": "-", "geoloc": [48.862655, 2.337607], "name": "Ragueneau", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.337607, 48.862655]}, "recordid": "e946d9e8f8c5a130f98eca945efadfd9eec40dcb", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "48 rue du Dessous des Berges", "prix_salle": "1", "geoloc": [48.826608, 2.374571], "name": "Le Caminito", "prix_terasse": null, "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.374571, 48.826608]}, "recordid": "5d9ad8bcfbbc20adaec2aa7dcdb71326868c7686", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "55bis quai de Valmy", "prix_salle": "1", "geoloc": [48.870598, 2.365413], "name": "Epicerie Musicale", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.365413, 48.870598]}, "recordid": "8ffea133d93c608d337bc0129f4f9f3d5cad8dae", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "Le petit Bretonneau - \u00e0 l'int\u00e9rieur de l'H\u00f4pital", "prix_salle": "1", "geoloc": null, "name": "Le petit Bretonneau", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {}, "recordid": "dd1ffdbd55c2dc651201302b8015258f7d35fd35", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "104 rue amelot", "prix_salle": "1", "geoloc": [48.862575, 2.367427], "name": "Le Centenaire", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.367427, 48.862575]}, "recordid": "e47d25752c2c8bcab5efb0e3a41920ec7a8a766a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "13 Rue du Pot de Fer", "prix_salle": "1", "geoloc": [48.842833, 2.348314], "name": "La Montagne Sans Genevi\u00e8ve", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": null, "geometry": {"type": "Point", "coordinates": [2.348314, 48.842833]}, "recordid": "314d170601f81e3a3f26d8801f0fbee39981c788", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "46 rue de Buzenval", "prix_salle": "1", "geoloc": [48.851325, 2.40171], "name": "Les P\u00e8res Populaires", "prix_terasse": "-", "date": "2012-10-18", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.40171, 48.851325]}, "recordid": "e75e8a3fe6212a0e576beec82f0128dd394e56fa", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "188 rue de Grenelle", "prix_salle": "-", "geoloc": [48.857658, 2.305613], "name": "Cafe de grenelle", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.305613, 48.857658]}, "recordid": "72e274046d671e68bc7754808feacfc95b91b6ed", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "73 rue de la Victoire", "prix_salle": "-", "geoloc": [48.875207, 2.332944], "name": "Le relais de la victoire", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332944, 48.875207]}, "recordid": "01c21abdf35484f9ad184782979ecd078de84523", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75016, "address": "Route de la Muette \u00e0 Neuilly\nClub hippique du Jardin d\u2019Acclimatation", "prix_salle": "1", "geoloc": null, "name": "La chaumi\u00e8re gourmande", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": null, "geometry": {}, "recordid": "438ec18d35793d12eb6a137373c3fe4f3aa38a69", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "216, rue Marcadet", "prix_salle": "-", "geoloc": [48.891882, 2.33365], "name": "Le Brio", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33365, 48.891882]}, "recordid": "49e3584ce3d6a6a236b4a0db688865bdd3483fec", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "22 rue des Dames", "prix_salle": "-", "geoloc": [48.884753, 2.324648], "name": "Caves populaires", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324648, 48.884753]}, "recordid": "af81e5eca2b84ea706ac2d379edf65b0fb2f879a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "12 avenue Jean Moulin", "prix_salle": "-", "geoloc": [48.827428, 2.325652], "name": "Caprice caf\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.325652, 48.827428]}, "recordid": "88e07bdb723b49cd27c90403b5930bca1c93b458", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "7 rue Clisson", "prix_salle": "-", "geoloc": [48.832964, 2.369266], "name": "Tamm Bara", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.369266, 48.832964]}, "recordid": "baf59c98acafbe48ed9e91b64377da216a20cbcc", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "1 rue de Montholon", "prix_salle": "-", "geoloc": [48.876577, 2.348414], "name": "L'anjou", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.348414, 48.876577]}, "recordid": "e6f5949dca40548aad296208c61c498f639f648c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "2 rue Robert Esnault Pelterie", "prix_salle": "-", "geoloc": [48.862599, 2.315086], "name": "Caf\u00e9 dans l'aerogare Air France Invalides", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315086, 48.862599]}, "recordid": "6d175eb48c577fafdfc99df0ab55da468cf17164", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "10 rue d\"Ulm", "prix_salle": "-", "geoloc": [48.844854, 2.345413], "name": "Waikiki", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.345413, 48.844854]}, "recordid": "fb1e2bc2ae55d3d47da682c71093a12fa64fbd45", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "36 rue Beaurepaire", "prix_salle": "-", "geoloc": [48.871576, 2.364499], "name": "Chez Prune", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.364499, 48.871576]}, "recordid": "19399ede5b619761877822185bbb4c98b565974c", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "21 rue Boulard", "prix_salle": "-", "geoloc": [48.833863, 2.329046], "name": "Au Vin Des Rues", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.329046, 48.833863]}, "recordid": "87263873b6f8346b5777844be0122a307f29fcab", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "14 rue d'alleray", "prix_salle": "-", "geoloc": [48.838137, 2.301166], "name": "bistrot les timbr\u00e9s", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.301166, 48.838137]}, "recordid": "74b6d7113e5b14eb8e890663f061208bf4ff6728", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "9 rue de Miromesnil", "prix_salle": "-", "geoloc": [48.871799, 2.315985], "name": "Caf\u00e9 beauveau", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.315985, 48.871799]}, "recordid": "2759f5cdda4bcb88ca3ae2f7299b37b8e62596c8", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "9 rue des petits champs", "prix_salle": "-", "geoloc": [48.866259, 2.338739], "name": "Caf\u00e9 Pistache", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338739, 48.866259]}, "recordid": "6d2675cdc912118d0376229be8e436feca9c8af7", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "13 Rue Jean-Baptiste Dumay", "prix_salle": "-", "geoloc": [48.874605, 2.387738], "name": "La Cagnotte", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.387738, 48.874605]}, "recordid": "f7085c754c0c97e418d7e5213753f74bd396fc27", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "172 rue de vaugirard", "prix_salle": "-", "geoloc": [48.842462, 2.310919], "name": "le 1 cinq", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.310919, 48.842462]}, "recordid": "17e917723fc99d6e5bd77eb9633ac2e789a9a6d9", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "28 bis boulevard Diderot", "prix_salle": "-", "geoloc": [48.84591, 2.375543], "name": "Le Killy Jen", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.375543, 48.84591]}, "recordid": "93132bd8b3ae67dfcc8cf8c1166e312ac4acb9b9", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "106 rue Lecourbe", "prix_salle": "-", "geoloc": [48.842868, 2.303173], "name": "Les Artisans", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.303173, 48.842868]}, "recordid": "c37ef573b4cb2d1e61795d6a9ef11de433dc9a99", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75001, "address": "83 avenue de Wagram", "prix_salle": "-", "geoloc": [48.865684, 2.334416], "name": "Peperoni", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.334416, 48.865684]}, "recordid": "9461f859ca009ced25555fa6af1e6867dda9223e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "380 rue de vaugirard", "prix_salle": "-", "geoloc": [48.833146, 2.288834], "name": "le lutece", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.288834, 48.833146]}, "recordid": "ddd13990c1408700085366bd4ba313acd69a44ea", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "16 rue Ganneron", "prix_salle": "-", "geoloc": [48.886431, 2.327429], "name": "Brasiloja", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.327429, 48.886431]}, "recordid": "d679bb1642534278f4c0203d67be0bafd5306d81", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "16 rue de Rivoli", "prix_salle": "-", "geoloc": [48.855711, 2.359491], "name": "Rivolux", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.359491, 48.855711]}, "recordid": "bdd2b008cc765c7fe195c037b830cd2628420a2f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "21 Bis Boulevard Diderot", "prix_salle": "-", "geoloc": [48.845898, 2.372766], "name": "L'europ\u00e9en", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.372766, 48.845898]}, "recordid": "693c0da7d4db24781ed161c01a661c36074a94fa", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75003, "address": "39 rue Notre Dame de Nazareth", "prix_salle": "-", "geoloc": [48.867465, 2.357791], "name": "NoMa", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.357791, 48.867465]}, "recordid": "60d8b670810cc95eb0439dd0c238f8205ea8ef76", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "1 Rue des Envierges", "prix_salle": "-", "geoloc": [48.871595, 2.385858], "name": "O'Paris", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385858, 48.871595]}, "recordid": "297c040284a05efe35c69bb621505e6acfdcdda4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "16 avenue Richerand", "prix_salle": "-", "geoloc": [48.872402, 2.366532], "name": "Caf\u00e9 Clochette", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.366532, 48.872402]}, "recordid": "a561a941f538e8a1d321bf8d98576d06be037962", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "40 Boulevard Beaumarchais", "prix_salle": "-", "geoloc": [48.856584, 2.368574], "name": "La cantoche de Paname", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.368574, 48.856584]}, "recordid": "5ba2aaec9f1de9d01e65be95215cab13c693cdf3", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "148 Boulevard de Charonne", "prix_salle": "-", "geoloc": [48.856496, 2.394874], "name": "Le Saint Ren\u00e9", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.394874, 48.856496]}, "recordid": "4d60b350d04d4b1bf4bfd4dd6cc59687dc792c74", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "196 rue du faubourg saint-antoine", "prix_salle": "1", "geoloc": [48.850055, 2.383908], "name": "La Libert\u00e9", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.383908, 48.850055]}, "recordid": "ee94e76326f8dcbe3500afec69f1a21eb1215ad0", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "16 rue des Petits Champs", "prix_salle": "1", "geoloc": [48.866737, 2.33716], "name": "Chez Rutabaga", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.33716, 48.866737]}, "recordid": "a420ea4608440b8dc8e0267fe8cc513daa950551", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "2 rue Lemercier", "prix_salle": "1", "geoloc": [48.885367, 2.325325], "name": "Le BB (Bouchon des Batignolles)", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.325325, 48.885367]}, "recordid": "20986cbfe11018bd0aba8150a49db1c435f7642d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "10 rue Rossini", "prix_salle": "1", "geoloc": [48.873175, 2.339193], "name": "La Brocante", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.339193, 48.873175]}, "recordid": "2e10601c35669394d43936a771b18408be0338ba", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "3 rue Ga\u00eet\u00e9", "prix_salle": "1", "geoloc": [48.840771, 2.324589], "name": "Le Plomb du cantal", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324589, 48.840771]}, "recordid": "6fb510614e00b065bf16a5af8e2c0eaf561a5654", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "22 rue des Dames", "prix_salle": "1", "geoloc": [48.884753, 2.324648], "name": "Les caves populaires", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.324648, 48.884753]}, "recordid": "d650c509a0aa8ed7b9c9b88861263f31463bbd0e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "108 rue de M\u00e9nilmontant", "prix_salle": "-", "geoloc": [48.869519, 2.39339], "name": "Chez Luna", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.39339, 48.869519]}, "recordid": "736f4d996f1f8b7c3a0ce2abfeebfcce2a4bab13", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75019, "address": "1 rue du Plateau", "prix_salle": "-", "geoloc": [48.877903, 2.385365], "name": "Le bar Fleuri", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.385365, 48.877903]}, "recordid": "be55720646093788ec161c6cadc5ad8059f4b90b", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "101 rue des dames", "prix_salle": "-", "geoloc": [48.882939, 2.31809], "name": "Trois pi\u00e8ces cuisine", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.31809, 48.882939]}, "recordid": "7bbbfb755020a2c25cce0067601994ce5ee4193f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "61 avenue de la Motte Picquet", "prix_salle": "-", "geoloc": [48.849497, 2.298855], "name": "Le Zinc", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.298855, 48.849497]}, "recordid": "e7c35a94454518de6de5bbecbc015fc37f7aea14", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "136 rue du Faubourg poissonni\u00e8re", "prix_salle": "-", "geoloc": [48.880669, 2.349964], "name": "La cantine de Zo\u00e9", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349964, 48.880669]}, "recordid": "0edc473b3432a869b8ed66b6c4c989766b699947", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75006, "address": "6/8 rue Stanislas", "prix_salle": "-", "geoloc": [48.844057, 2.328402], "name": "Les Vendangeurs", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.328402, 48.844057]}, "recordid": "e9766ea36f6293bf670ed938bff02b975d012973", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75006, "address": "3 carrefour de l'Od\u00e9on", "prix_salle": "-", "geoloc": [48.852053, 2.338779], "name": "L'avant comptoir", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.338779, 48.852053]}, "recordid": "fe843d2f43dcaac9129f5b36dc367558dfd3b3e4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "1 rue Paul albert", "prix_salle": "1", "geoloc": [48.886504, 2.34498], "name": "Botak cafe", "prix_terasse": "1", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.34498, 48.886504]}, "recordid": "9e19c375e612f5fb803ec6a27881858619207812", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "67 rue du Ch\u00e2teau d'eau", "prix_salle": "-", "geoloc": [48.872722, 2.354594], "name": "le chateau d'eau", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.354594, 48.872722]}, "recordid": "05bb6a26ec5bfbba25da2d19a5f0e83d69800f38", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "58 rue du Fbg Saint-Antoine", "prix_salle": "-", "geoloc": [48.85192, 2.373229], "name": "Bistrot Saint-Antoine", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.373229, 48.85192]}, "recordid": "daa3908ddf69d378fec5b4548494727e1121adc4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "11/13 boulevard Beaumarchais", "prix_salle": "-", "geoloc": [48.854685, 2.368487], "name": "Chez Oscar", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.368487, 48.854685]}, "recordid": "c73be0483480c59e6ab6bc3a906c8d9dd474887f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "63 rue de Ponthieu", "prix_salle": "-", "geoloc": [48.87226, 2.304441], "name": "Le Fronton", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.304441, 48.87226]}, "recordid": "85a8200d3e3aed7724d3207ed8b1ee5ec50c1f90", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "48 avenue de la Motte Picquet", "prix_salle": "-", "geoloc": [48.851, 2.300378], "name": "Le Piquet", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.300378, 48.851]}, "recordid": "460e2adc95fd172f753b1b6ed296c2711639d49d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "104 rue Mouffetard", "prix_salle": "-", "geoloc": [48.841089, 2.349565], "name": "Le Tournebride", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.349565, 48.841089]}, "recordid": "8a7a23ed68366f70ab939c877cbdce46f19d75c7", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "52 rue des plantes", "prix_salle": "-", "geoloc": [48.828704, 2.322074], "name": "maison du vin", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.322074, 48.828704]}, "recordid": "482507a8f0fe4960f94372b6fa12b16e7d4f2a93", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "11 Quai de la Tournelle", "prix_salle": "-", "geoloc": [48.849821, 2.355337], "name": "Caf\u00e9 rallye tournelles", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355337, 48.849821]}, "recordid": "91d88e321a75b6a8c4dea816c399fda77c41f9d1", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75010, "address": "61 rue du ch\u00e2teau d'eau", "prix_salle": "-", "geoloc": [48.872498, 2.355136], "name": "Brasserie le Morvan", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.355136, 48.872498]}, "recordid": "6ec62058995948fc18d331f01a2d03acc0d9e0fa", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75019, "address": "6 rue M\u00e9lingue", "prix_salle": "1", "geoloc": [48.874879, 2.386064], "name": "Chez Miamophile", "prix_terasse": "1", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.386064, 48.874879]}, "recordid": "13924872737e4fc640a43da58937c3777c2ac753", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "18 rue de Crussol", "prix_salle": "-", "geoloc": [48.864269, 2.36858], "name": "Panem", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.36858, 48.864269]}, "recordid": "67bdf3a6989f80749a1ba33a17b1370de0a0e1cd", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "47 rue de Batignolles", "prix_salle": "-", "geoloc": [48.885662, 2.319591], "name": "Petits Freres des Pauvres", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.319591, 48.885662]}, "recordid": "e27fd00149514bbfad7dd7e8f9b0c677df2d3f25", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "198 rue de la Convention", "prix_salle": "-", "geoloc": [48.837212, 2.296046], "name": "Caf\u00e9 Dupont", "prix_terasse": "-", "date": "2012-05-11", "prix_comptoir": 0, "geometry": {"type": "Point", "coordinates": [2.296046, 48.837212]}, "recordid": "4d40e6d864dae81c152a05cb98e30933bde96aa1", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75008, "address": "28 rue de Ponthieu", "prix_salle": "1", "geoloc": [48.871002, 2.30879], "name": "L'Angle", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.30879, 48.871002]}, "recordid": "c40bd2d1f98b415e539c27cf68518d060ebab51e", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "19-23 rue L\u00e9on", "prix_salle": "1", "geoloc": [48.888023, 2.353467], "name": "Institut des Cultures d'Islam", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.353467, 48.888023]}, "recordid": "68d6d37b846e39bd8554e1f8f75974b486b0f27b", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75018, "address": "19 rue Pajol", "prix_salle": "1", "geoloc": [48.886044, 2.360781], "name": "Canopy Caf\u00e9 associatif", "prix_terasse": "-", "date": "2012-10-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360781, 48.886044]}, "recordid": "ff73bafb514bb68eb925c81aee43c3a58ac3c70d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75002, "address": "place de l'opera", "prix_salle": "-", "geoloc": [48.870287, 2.332491], "name": "L'Entracte", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332491, 48.870287]}, "recordid": "0039cd8bceb5e281677a158f832a660789088071", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75003, "address": "15 rue du Parc Royal", "prix_salle": "-", "geoloc": [48.858709, 2.362701], "name": "Le S\u00e9vign\u00e9", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.362701, 48.858709]}, "recordid": "adcc8b4f78f05ba7b24b0593e1516dfb7b415f91", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75005, "address": "35 rue Claude Bernard", "prix_salle": "-", "geoloc": [48.839687, 2.347254], "name": "Le Caf\u00e9 d'avant", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.347254, 48.839687]}, "recordid": "b904fc48763938eee2169ba25aad2ffcc0dd6a9f", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75006, "address": "53 rue Notre-Dame des Champs", "prix_salle": "-", "geoloc": [48.844244, 2.330407], "name": "Le Lucernaire", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.330407, 48.844244]}, "recordid": "cc72af04314fd40e16ff611c799d378515043508", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75009, "address": "12 rue Blanche", "prix_salle": "-", "geoloc": [48.877599, 2.332111], "name": "Le Brigadier", "prix_terasse": "-", "date": "2012-10-09", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.332111, 48.877599]}, "recordid": "978d4bc68c9ebf81029d3e77274d2107777b8a75", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75013, "address": "26 rue du Docteur Magnan", "prix_salle": "-", "geoloc": [48.826494, 2.359987], "name": "L'\u00e2ge d'or", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.359987, 48.826494]}, "recordid": "40bffbdc0c9ed1cbce820fed875d7c21d8964640", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "Place de Clichy", "prix_salle": "-", "geoloc": [48.883717, 2.326861], "name": "Bagels & Coffee Corner", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.326861, 48.883717]}, "recordid": "262facde9b8c4568c9ba7fbce8f069ff8c76948d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "10 boulevard Victor", "prix_salle": "-", "geoloc": [48.835843, 2.278501], "name": "Caf\u00e9 Victor", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.278501, 48.835843]}, "recordid": "e5817ec44ac5a7ea2e4a34b6a2e13d535156642b", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "54, avenue Daumesnil", "prix_salle": "-", "geoloc": [48.845337, 2.379024], "name": "L'empreinte", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379024, 48.845337]}, "recordid": "b96ddd35cbbf5d93aaff79487afdf083b5ff0817", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75011, "address": "93, rue de la Roquette", "prix_salle": "-", "geoloc": [48.857312, 2.379055], "name": "L'horizon", "prix_terasse": "-", "date": "2012-03-07", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.379055, 48.857312]}, "recordid": "84c6b7335e7f82ac942c4f398723ec99076f148d", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "34 bis rue de Wattignies", "prix_salle": "-", "geoloc": [48.835878, 2.395723], "name": "Au pays de Vannes", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.395723, 48.835878]}, "recordid": "a17869dbb9d0d5b1e5ed7bb288053900b04ee944", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75007, "address": "36 rue de Varenne", "prix_salle": "-", "geoloc": [48.85413, 2.323539], "name": "Caf\u00e9 Varenne", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.323539, 48.85413]}, "recordid": "a26ec0d5fca47b8de77d862ad8a99b75bb520a09", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75004, "address": "125 Rue Saint-Antoine", "prix_salle": "-", "geoloc": [48.855161, 2.360218], "name": "l'El\u00e9phant du nil", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.360218, 48.855161]}, "recordid": "7b7ceefd1f9ed85041265c9577e0dc8bee01d45a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "354 bis rue Vaugirard", "prix_salle": "-", "geoloc": [48.8357, 2.292961], "name": "Le Comptoir", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.292961, 48.8357]}, "recordid": "59d8fa304e535f4eb41f9746028034c9b30cbde4", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75015, "address": "358 rue de Vaugirard", "prix_salle": "-", "geoloc": [48.835451, 2.292515], "name": "Le Parc Vaugirard", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.292515, 48.835451]}, "recordid": "19f655206a8446959c8e796c2b3cb9001890f985", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75014, "address": "58 rue Daguerre", "prix_salle": "-", "geoloc": [48.834972, 2.327007], "name": "le Zango", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.327007, 48.834972]}, "recordid": "e1b54109015316a822747f788128f997a3478050", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75020, "address": "3 rue de Lagny", "prix_salle": "-", "geoloc": [48.848887, 2.399972], "name": "Melting Pot", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.399972, 48.848887]}, "recordid": "fd0de2cbf73e0a728cd73e4e2a9a4a9c646f76f2", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75017, "address": "174 avenue de Clichy", "prix_salle": "-", "geoloc": [48.892366, 2.317359], "name": "Pari's Caf\u00e9", "prix_terasse": "-", "date": "2012-06-27", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.317359, 48.892366]}, "recordid": "831446ae203f89de26d3300e625c20717e82d40a", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75012, "address": "157 rue Bercy 75012 Paris", "prix_salle": "-", "geoloc": [48.842146, 2.375986], "name": "L'entrep\u00f4t", "prix_terasse": "-", "date": "2014-02-01", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.375986, 48.842146]}, "recordid": "d8746118429eb118f38ecbee904636d9b33fa8ba", "city": "Paris", "country": "France"}
|
||||||
|
{"zipcode": 75003, "address": "Place de la R\u00e9publique", "prix_salle": "-", "geoloc": [48.867092, 2.363288], "name": "Le caf\u00e9 Monde et M\u00e9dias", "prix_terasse": "-", "date": "2013-08-22", "prix_comptoir": 1, "geometry": {"type": "Point", "coordinates": [2.363288, 48.867092]}, "recordid": "af04c90f25e25daf7f5cbbab1bc740bac26541d4", "city": "Paris", "country": "France"}
|
||||||
@ -1,29 +0,0 @@
|
|||||||
"""
|
|
||||||
Extracts a list of parisian bars where you can buy a coffee for a reasonable price, and store them in a flat text file.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "ODS()" -> "transform" -> "FileWriter()";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import bonobo
|
|
||||||
from bonobo.commands import get_default_services
|
|
||||||
from bonobo.contrib.opendatasoft import OpenDataSoftAPI
|
|
||||||
|
|
||||||
filename = 'coffeeshops.txt'
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
|
||||||
OpenDataSoftAPI(dataset='liste-des-cafes-a-un-euro', netloc='opendata.paris.fr'),
|
|
||||||
lambda row: '{nom_du_cafe}, {adresse}, {arrondissement} Paris, France'.format(**row),
|
|
||||||
bonobo.FileWriter(path=filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
bonobo.run(graph, services=get_default_services(__file__))
|
|
||||||
@ -1,182 +1,183 @@
|
|||||||
Extérieur Quai, 5, rue d'Alsace, 75010 Paris, France
|
name,address,zipcode,city
|
||||||
Le Sully, 6 Bd henri IV, 75004 Paris, France
|
Coffee Chope,344Vrue Vaugirard,75015,Paris
|
||||||
O q de poule, 53 rue du ruisseau, 75018 Paris, France
|
Extérieur Quai,"5, rue d'Alsace",75010,Paris
|
||||||
Le Pas Sage, 1 Passage du Grand Cerf, 75002 Paris, France
|
Le Sully,6 Bd henri IV,75004,Paris
|
||||||
La Renaissance, 112 Rue Championnet, 75018 Paris, France
|
O q de poule,53 rue du ruisseau,75018,Paris
|
||||||
La Caravane, Rue de la Fontaine au Roi, 75011 Paris, France
|
Le Pas Sage,1 Passage du Grand Cerf,75002,Paris
|
||||||
Le chantereine, 51 Rue Victoire, 75009 Paris, France
|
La Renaissance,112 Rue Championnet,75018,Paris
|
||||||
Le Müller, 11 rue Feutrier, 75018 Paris, France
|
La Caravane,Rue de la Fontaine au Roi,75011,Paris
|
||||||
Le drapeau de la fidelité, 21 rue Copreaux, 75015 Paris, France
|
Le chantereine,51 Rue Victoire,75009,Paris
|
||||||
Le café des amis, 125 rue Blomet, 75015 Paris, France
|
Le Müller,11 rue Feutrier,75018,Paris
|
||||||
Le Café Livres, 10 rue Saint Martin, 75004 Paris, France
|
Le drapeau de la fidelité,21 rue Copreaux,75015,Paris
|
||||||
Le Bosquet, 46 avenue Bosquet, 75007 Paris, France
|
Le café des amis,125 rue Blomet,75015,Paris
|
||||||
Le Chaumontois, 12 rue Armand Carrel, 75018 Paris, France
|
Le Café Livres,10 rue Saint Martin,75004,Paris
|
||||||
Le Kleemend's, 34 avenue Pierre Mendès-France, 75013 Paris, France
|
Le Bosquet,46 avenue Bosquet,75007,Paris
|
||||||
Café Pierre, 202 rue du faubourg st antoine, 75012 Paris, France
|
Le Chaumontois,12 rue Armand Carrel,75018,Paris
|
||||||
Les Arcades, 61 rue de Ponthieu, 75008 Paris, France
|
Le Kleemend's,34 avenue Pierre Mendès-France,75013,Paris
|
||||||
Le Square, 31 rue Saint-Dominique, 75007 Paris, France
|
Café Pierre,202 rue du faubourg st antoine,75012,Paris
|
||||||
Assaporare Dix sur Dix, 75, avenue Ledru-Rollin, 75012 Paris, France
|
Les Arcades,61 rue de Ponthieu,75008,Paris
|
||||||
Au cerceau d'or, 129 boulevard sebastopol, 75002 Paris, France
|
Le Square,31 rue Saint-Dominique,75007,Paris
|
||||||
Aux cadrans, 21 ter boulevard Diderot, 75012 Paris, France
|
Assaporare Dix sur Dix,"75, avenue Ledru-Rollin",75012,Paris
|
||||||
Café antoine, 17 rue Jean de la Fontaine, 75016 Paris, France
|
Au cerceau d'or,129 boulevard sebastopol,75002,Paris
|
||||||
Café de la Mairie (du VIII), rue de Lisbonne, 75008 Paris, France
|
Aux cadrans,21 ter boulevard Diderot,75012,Paris
|
||||||
Café Lea, 5 rue Claude Bernard, 75005 Paris, France
|
Café antoine,17 rue Jean de la Fontaine,75016,Paris
|
||||||
Cardinal Saint-Germain, 11 boulevard Saint-Germain, 75005 Paris, France
|
Café de la Mairie (du VIII),rue de Lisbonne,75008,Paris
|
||||||
Dédé la frite, 52 rue Notre-Dame des Victoires, 75002 Paris, France
|
Café Lea,5 rue Claude Bernard,75005,Paris
|
||||||
La Bauloise, 36 rue du hameau, 75015 Paris, France
|
Cardinal Saint-Germain,11 boulevard Saint-Germain,75005,Paris
|
||||||
Le Bellerive, 71 quai de Seine, 75019 Paris, France
|
Dédé la frite,52 rue Notre-Dame des Victoires,75002,Paris
|
||||||
Le bistrot de Maëlle et Augustin, 42 rue coquillère, 75001 Paris, France
|
La Bauloise,36 rue du hameau,75015,Paris
|
||||||
Le Dellac, 14 rue Rougemont, 75009 Paris, France
|
Le Bellerive,71 quai de Seine,75019,Paris
|
||||||
Le Felteu, 1 rue Pecquay, 75004 Paris, France
|
Le bistrot de Maëlle et Augustin,42 rue coquillère,75001,Paris
|
||||||
Le Reynou, 2 bis quai de la mégisserie, 75001 Paris, France
|
Le Dellac,14 rue Rougemont,75009,Paris
|
||||||
Le Saint Jean, 23 rue des abbesses, 75018 Paris, France
|
Le Felteu,1 rue Pecquay,75004,Paris
|
||||||
les montparnos, 65 boulevard Pasteur, 75015 Paris, France
|
Le Reynou,2 bis quai de la mégisserie,75001,Paris
|
||||||
L'antre d'eux, 16 rue DE MEZIERES, 75006 Paris, France
|
Le Saint Jean,23 rue des abbesses,75018,Paris
|
||||||
Drole d'endroit pour une rencontre, 58 rue de Montorgueil, 75002 Paris, France
|
les montparnos,65 boulevard Pasteur,75015,Paris
|
||||||
Le pari's café, 104 rue caulaincourt, 75018 Paris, France
|
Le Supercoin,"3, rue Baudelique",75018,Paris
|
||||||
Le Poulailler, 60 rue saint-sabin, 75011 Paris, France
|
Populettes,86 bis rue Riquet,75018,Paris
|
||||||
Chai 33, 33 Cour Saint Emilion, 75012 Paris, France
|
Au bon coin,49 rue des Cloys,75018,Paris
|
||||||
L'Assassin, 99 rue Jean-Pierre Timbaud, 75011 Paris, France
|
Le Couvent,69 rue Broca,75013,Paris
|
||||||
l'Usine, 1 rue d'Avron, 75020 Paris, France
|
La Brûlerie des Ternes,111 rue mouffetard,75005,Paris
|
||||||
La Bricole, 52 rue Liebniz, 75018 Paris, France
|
L'Écir,59 Boulevard Saint-Jacques,75014,Paris
|
||||||
le ronsard, place maubert, 75005 Paris, France
|
Le Chat bossu,"126, rue du Faubourg Saint Antoine",75012,Paris
|
||||||
Face Bar, 82 rue des archives, 75003 Paris, France
|
Denfert café,58 boulvevard Saint Jacques,75014,Paris
|
||||||
American Kitchen, 49 rue bichat, 75010 Paris, France
|
Le Café frappé,95 rue Montmartre,75002,Paris
|
||||||
La Marine, 55 bis quai de valmy, 75010 Paris, France
|
La Perle,78 rue vieille du temple,75003,Paris
|
||||||
Le Bloc, 21 avenue Brochant, 75017 Paris, France
|
Le Descartes,1 rue Thouin,75005,Paris
|
||||||
La Recoleta au Manoir, 229 avenue Gambetta, 75020 Paris, France
|
Le petit club,55 rue de la tombe Issoire,75014,Paris
|
||||||
Le Pareloup, 80 Rue Saint-Charles, 75015 Paris, France
|
Le Plein soleil,90 avenue Parmentier,75011,Paris
|
||||||
La Brasserie Gaité, 3 rue de la Gaité, 75014 Paris, France
|
Le Relais Haussmann,"146, boulevard Haussmann",75008,Paris
|
||||||
Café Zen, 46 rue Victoire, 75009 Paris, France
|
Le Malar,88 rue Saint-Dominique,75007,Paris
|
||||||
O'Breizh, 27 rue de Penthièvre, 75008 Paris, France
|
Au panini de la place,47 rue Belgrand,75020,Paris
|
||||||
Le Petit Choiseul, 23 rue saint augustin, 75002 Paris, France
|
Le Village,182 rue de Courcelles,75017,Paris
|
||||||
Invitez vous chez nous, 7 rue Epée de Bois, 75005 Paris, France
|
Pause Café,41 rue de Charonne,75011,Paris
|
||||||
La Cordonnerie, 142 Rue Saint-Denis 75002 Paris, 75002 Paris, France
|
Le Pure café,14 rue Jean Macé,75011,Paris
|
||||||
Le Supercoin, 3, rue Baudelique, 75018 Paris, France
|
Extra old café,307 fg saint Antoine,75011,Paris
|
||||||
Populettes, 86 bis rue Riquet, 75018 Paris, France
|
Chez Fafa,44 rue Vinaigriers,75010,Paris
|
||||||
Au bon coin, 49 rue des Cloys, 75018 Paris, France
|
En attendant l'or,3 rue Faidherbe,75011,Paris
|
||||||
Le Couvent, 69 rue Broca, 75013 Paris, France
|
Brûlerie San José,30 rue des Petits-Champs,75002,Paris
|
||||||
La Brûlerie des Ternes, 111 rue mouffetard, 75005 Paris, France
|
Café Martin,2 place Martin Nadaud,75001,Paris
|
||||||
L'Écir, 59 Boulevard Saint-Jacques, 75014 Paris, France
|
Etienne,"14 rue Turbigo, Paris",75001,Paris
|
||||||
Le Chat bossu, 126, rue du Faubourg Saint Antoine, 75012 Paris, France
|
L'ingénu,184 bd Voltaire,75011,Paris
|
||||||
Denfert café, 58 boulvevard Saint Jacques, 75014 Paris, France
|
L'Olive,8 rue L'Olive,75018,Paris
|
||||||
Le Café frappé, 95 rue Montmartre, 75002 Paris, France
|
Le Biz,18 rue Favart,75002,Paris
|
||||||
La Perle, 78 rue vieille du temple, 75003 Paris, France
|
Le Cap Bourbon,1 rue Louis le Grand,75002,Paris
|
||||||
Le Descartes, 1 rue Thouin, 75005 Paris, France
|
Le General Beuret,9 Place du General Beuret,75015,Paris
|
||||||
Le petit club, 55 rue de la tombe Issoire, 75014 Paris, France
|
Le Germinal,95 avenue Emile Zola,75015,Paris
|
||||||
Le Plein soleil, 90 avenue Parmentier, 75011 Paris, France
|
Le Ragueneau,202 rue Saint-Honoré,75001,Paris
|
||||||
Le Relais Haussmann, 146, boulevard Haussmann, 75008 Paris, France
|
Le refuge,72 rue lamarck,75018,Paris
|
||||||
Le Malar, 88 rue Saint-Dominique, 75007 Paris, France
|
Le sully,13 rue du Faubourg Saint Denis,75010,Paris
|
||||||
Au panini de la place, 47 rue Belgrand, 75020 Paris, France
|
L'antre d'eux,16 rue DE MEZIERES,75006,Paris
|
||||||
Le Village, 182 rue de Courcelles, 75017 Paris, France
|
Drole d'endroit pour une rencontre,58 rue de Montorgueil,75002,Paris
|
||||||
Pause Café, 41 rue de Charonne, 75011 Paris, France
|
Le pari's café,104 rue caulaincourt,75018,Paris
|
||||||
Le Pure café, 14 rue Jean Macé, 75011 Paris, France
|
Le Poulailler,60 rue saint-sabin,75011,Paris
|
||||||
Extra old café, 307 fg saint Antoine, 75011 Paris, France
|
Chai 33,33 Cour Saint Emilion,75012,Paris
|
||||||
Chez Fafa, 44 rue Vinaigriers, 75010 Paris, France
|
L'Assassin,99 rue Jean-Pierre Timbaud,75011,Paris
|
||||||
En attendant l'or, 3 rue Faidherbe, 75011 Paris, France
|
l'Usine,1 rue d'Avron,75020,Paris
|
||||||
Brûlerie San José, 30 rue des Petits-Champs, 75002 Paris, France
|
La Bricole,52 rue Liebniz,75018,Paris
|
||||||
Café Martin, 2 place Martin Nadaud, 75001 Paris, France
|
le ronsard,place maubert,75005,Paris
|
||||||
Etienne, 14 rue Turbigo, Paris, 75001 Paris, France
|
Face Bar,82 rue des archives,75003,Paris
|
||||||
L'ingénu, 184 bd Voltaire, 75011 Paris, France
|
American Kitchen,49 rue bichat,75010,Paris
|
||||||
L'Olive, 8 rue L'Olive, 75018 Paris, France
|
La Marine,55 bis quai de valmy,75010,Paris
|
||||||
Le Biz, 18 rue Favart, 75002 Paris, France
|
Le Bloc,21 avenue Brochant,75017,Paris
|
||||||
Le Cap Bourbon, 1 rue Louis le Grand, 75002 Paris, France
|
La Recoleta au Manoir,229 avenue Gambetta,75020,Paris
|
||||||
Le General Beuret, 9 Place du General Beuret, 75015 Paris, France
|
Le Pareloup,80 Rue Saint-Charles,75015,Paris
|
||||||
Le Germinal, 95 avenue Emile Zola, 75015 Paris, France
|
La Brasserie Gaité,3 rue de la Gaité,75014,Paris
|
||||||
Le Ragueneau, 202 rue Saint-Honoré, 75001 Paris, France
|
Café Zen,46 rue Victoire,75009,Paris
|
||||||
Le refuge, 72 rue lamarck, 75018 Paris, France
|
O'Breizh,27 rue de Penthièvre,75008,Paris
|
||||||
Le sully, 13 rue du Faubourg Saint Denis, 75010 Paris, France
|
Le Petit Choiseul,23 rue saint augustin,75002,Paris
|
||||||
Coffee Chope, 344Vrue Vaugirard, 75015 Paris, France
|
Invitez vous chez nous,7 rue Epée de Bois,75005,Paris
|
||||||
Le bal du pirate, 60 rue des bergers, 75015 Paris, France
|
La Cordonnerie,142 Rue Saint-Denis 75002 Paris,75002,Paris
|
||||||
zic zinc, 95 rue claude decaen, 75012 Paris, France
|
Le bal du pirate,60 rue des bergers,75015,Paris
|
||||||
l'orillon bar, 35 rue de l'orillon, 75011 Paris, France
|
zic zinc,95 rue claude decaen,75012,Paris
|
||||||
Le Zazabar, 116 Rue de Ménilmontant, 75020 Paris, France
|
l'orillon bar,35 rue de l'orillon,75011,Paris
|
||||||
L'Inévitable, 22 rue Linné, 75005 Paris, France
|
Le Zazabar,116 Rue de Ménilmontant,75020,Paris
|
||||||
Le Dunois, 77 rue Dunois, 75013 Paris, France
|
L'Inévitable,22 rue Linné,75005,Paris
|
||||||
Ragueneau, 202 rue Saint Honoré, 75001 Paris, France
|
Le Dunois,77 rue Dunois,75013,Paris
|
||||||
Le Caminito, 48 rue du Dessous des Berges, 75013 Paris, France
|
Ragueneau,202 rue Saint Honoré,75001,Paris
|
||||||
Epicerie Musicale, 55bis quai de Valmy, 75010 Paris, France
|
Le Caminito,48 rue du Dessous des Berges,75013,Paris
|
||||||
Le petit Bretonneau, Le petit Bretonneau - à l'intérieur de l'Hôpital, 75018 Paris, France
|
Epicerie Musicale,55bis quai de Valmy,75010,Paris
|
||||||
Le Centenaire, 104 rue amelot, 75011 Paris, France
|
Le petit Bretonneau,Le petit Bretonneau - à l'intérieur de l'Hôpital,75018,Paris
|
||||||
La Montagne Sans Geneviève, 13 Rue du Pot de Fer, 75005 Paris, France
|
Le Centenaire,104 rue amelot,75011,Paris
|
||||||
Les Pères Populaires, 46 rue de Buzenval, 75020 Paris, France
|
La Montagne Sans Geneviève,13 Rue du Pot de Fer,75005,Paris
|
||||||
Cafe de grenelle, 188 rue de Grenelle, 75007 Paris, France
|
Les Pères Populaires,46 rue de Buzenval,75020,Paris
|
||||||
Le relais de la victoire, 73 rue de la Victoire, 75009 Paris, France
|
Cafe de grenelle,188 rue de Grenelle,75007,Paris
|
||||||
La chaumière gourmande, Route de la Muette à Neuilly
|
Le relais de la victoire,73 rue de la Victoire,75009,Paris
|
||||||
Club hippique du Jardin d’Acclimatation, 75016 Paris, France
|
La chaumière gourmande,"Route de la Muette à Neuilly
|
||||||
Le Brio, 216, rue Marcadet, 75018 Paris, France
|
Club hippique du Jardin d’Acclimatation",75016,Paris
|
||||||
Caves populaires, 22 rue des Dames, 75017 Paris, France
|
Le Brio,"216, rue Marcadet",75018,Paris
|
||||||
Caprice café, 12 avenue Jean Moulin, 75014 Paris, France
|
Caves populaires,22 rue des Dames,75017,Paris
|
||||||
Tamm Bara, 7 rue Clisson, 75013 Paris, France
|
Caprice café,12 avenue Jean Moulin,75014,Paris
|
||||||
L'anjou, 1 rue de Montholon, 75009 Paris, France
|
Tamm Bara,7 rue Clisson,75013,Paris
|
||||||
Café dans l'aerogare Air France Invalides, 2 rue Robert Esnault Pelterie, 75007 Paris, France
|
L'anjou,1 rue de Montholon,75009,Paris
|
||||||
Chez Prune, 36 rue Beaurepaire, 75010 Paris, France
|
Café dans l'aerogare Air France Invalides,2 rue Robert Esnault Pelterie,75007,Paris
|
||||||
Au Vin Des Rues, 21 rue Boulard, 75014 Paris, France
|
Waikiki,"10 rue d""Ulm",75005,Paris
|
||||||
bistrot les timbrés, 14 rue d'alleray, 75015 Paris, France
|
Chez Prune,36 rue Beaurepaire,75010,Paris
|
||||||
Café beauveau, 9 rue de Miromesnil, 75008 Paris, France
|
Au Vin Des Rues,21 rue Boulard,75014,Paris
|
||||||
Café Pistache, 9 rue des petits champs, 75001 Paris, France
|
bistrot les timbrés,14 rue d'alleray,75015,Paris
|
||||||
La Cagnotte, 13 Rue Jean-Baptiste Dumay, 75020 Paris, France
|
Café beauveau,9 rue de Miromesnil,75008,Paris
|
||||||
le 1 cinq, 172 rue de vaugirard, 75015 Paris, France
|
Café Pistache,9 rue des petits champs,75001,Paris
|
||||||
Le Killy Jen, 28 bis boulevard Diderot, 75012 Paris, France
|
La Cagnotte,13 Rue Jean-Baptiste Dumay,75020,Paris
|
||||||
Les Artisans, 106 rue Lecourbe, 75015 Paris, France
|
le 1 cinq,172 rue de vaugirard,75015,Paris
|
||||||
Peperoni, 83 avenue de Wagram, 75001 Paris, France
|
Le Killy Jen,28 bis boulevard Diderot,75012,Paris
|
||||||
le lutece, 380 rue de vaugirard, 75015 Paris, France
|
Les Artisans,106 rue Lecourbe,75015,Paris
|
||||||
Brasiloja, 16 rue Ganneron, 75018 Paris, France
|
Peperoni,83 avenue de Wagram,75001,Paris
|
||||||
Rivolux, 16 rue de Rivoli, 75004 Paris, France
|
le lutece,380 rue de vaugirard,75015,Paris
|
||||||
L'européen, 21 Bis Boulevard Diderot, 75012 Paris, France
|
Brasiloja,16 rue Ganneron,75018,Paris
|
||||||
NoMa, 39 rue Notre Dame de Nazareth, 75003 Paris, France
|
Rivolux,16 rue de Rivoli,75004,Paris
|
||||||
O'Paris, 1 Rue des Envierges, 75020 Paris, France
|
L'européen,21 Bis Boulevard Diderot,75012,Paris
|
||||||
Café Clochette, 16 avenue Richerand, 75010 Paris, France
|
NoMa,39 rue Notre Dame de Nazareth,75003,Paris
|
||||||
La cantoche de Paname, 40 Boulevard Beaumarchais, 75011 Paris, France
|
O'Paris,1 Rue des Envierges,75020,Paris
|
||||||
Le Saint René, 148 Boulevard de Charonne, 75020 Paris, France
|
Café Clochette,16 avenue Richerand,75010,Paris
|
||||||
La Liberté, 196 rue du faubourg saint-antoine, 75012 Paris, France
|
La cantoche de Paname,40 Boulevard Beaumarchais,75011,Paris
|
||||||
Chez Rutabaga, 16 rue des Petits Champs, 75002 Paris, France
|
Le Saint René,148 Boulevard de Charonne,75020,Paris
|
||||||
Le BB (Bouchon des Batignolles), 2 rue Lemercier, 75017 Paris, France
|
La Liberté,196 rue du faubourg saint-antoine,75012,Paris
|
||||||
La Brocante, 10 rue Rossini, 75009 Paris, France
|
Chez Rutabaga,16 rue des Petits Champs,75002,Paris
|
||||||
Le Plomb du cantal, 3 rue Gaîté, 75014 Paris, France
|
Le BB (Bouchon des Batignolles),2 rue Lemercier,75017,Paris
|
||||||
Les caves populaires, 22 rue des Dames, 75017 Paris, France
|
La Brocante,10 rue Rossini,75009,Paris
|
||||||
Chez Luna, 108 rue de Ménilmontant, 75020 Paris, France
|
Le Plomb du cantal,3 rue Gaîté,75014,Paris
|
||||||
Le bar Fleuri, 1 rue du Plateau, 75019 Paris, France
|
Les caves populaires,22 rue des Dames,75017,Paris
|
||||||
Trois pièces cuisine, 101 rue des dames, 75017 Paris, France
|
Chez Luna,108 rue de Ménilmontant,75020,Paris
|
||||||
Le Zinc, 61 avenue de la Motte Picquet, 75015 Paris, France
|
Le bar Fleuri,1 rue du Plateau,75019,Paris
|
||||||
La cantine de Zoé, 136 rue du Faubourg poissonnière, 75010 Paris, France
|
Trois pièces cuisine,101 rue des dames,75017,Paris
|
||||||
Les Vendangeurs, 6/8 rue Stanislas, 75006 Paris, France
|
Le Zinc,61 avenue de la Motte Picquet,75015,Paris
|
||||||
L'avant comptoir, 3 carrefour de l'Odéon, 75006 Paris, France
|
La cantine de Zoé,136 rue du Faubourg poissonnière,75010,Paris
|
||||||
Botak cafe, 1 rue Paul albert, 75018 Paris, France
|
Les Vendangeurs,6/8 rue Stanislas,75006,Paris
|
||||||
le chateau d'eau, 67 rue du Château d'eau, 75010 Paris, France
|
L'avant comptoir,3 carrefour de l'Odéon,75006,Paris
|
||||||
Bistrot Saint-Antoine, 58 rue du Fbg Saint-Antoine, 75012 Paris, France
|
Botak cafe,1 rue Paul albert,75018,Paris
|
||||||
Chez Oscar, 11/13 boulevard Beaumarchais, 75004 Paris, France
|
le chateau d'eau,67 rue du Château d'eau,75010,Paris
|
||||||
Le Fronton, 63 rue de Ponthieu, 75008 Paris, France
|
Bistrot Saint-Antoine,58 rue du Fbg Saint-Antoine,75012,Paris
|
||||||
Le Piquet, 48 avenue de la Motte Picquet, 75015 Paris, France
|
Chez Oscar,11/13 boulevard Beaumarchais,75004,Paris
|
||||||
Le Tournebride, 104 rue Mouffetard, 75005 Paris, France
|
Le Fronton,63 rue de Ponthieu,75008,Paris
|
||||||
maison du vin, 52 rue des plantes, 75014 Paris, France
|
Le Piquet,48 avenue de la Motte Picquet,75015,Paris
|
||||||
L'entrepôt, 157 rue Bercy 75012 Paris, 75012 Paris, France
|
Le Tournebride,104 rue Mouffetard,75005,Paris
|
||||||
Le café Monde et Médias, Place de la République, 75003 Paris, France
|
maison du vin,52 rue des plantes,75014,Paris
|
||||||
Café rallye tournelles, 11 Quai de la Tournelle, 75005 Paris, France
|
L'entrepôt,157 rue Bercy 75012 Paris,75012,Paris
|
||||||
Brasserie le Morvan, 61 rue du château d'eau, 75010 Paris, France
|
Le café Monde et Médias,Place de la République,75003,Paris
|
||||||
Chez Miamophile, 6 rue Mélingue, 75019 Paris, France
|
Café rallye tournelles,11 Quai de la Tournelle,75005,Paris
|
||||||
Panem, 18 rue de Crussol, 75011 Paris, France
|
Brasserie le Morvan,61 rue du château d'eau,75010,Paris
|
||||||
Petits Freres des Pauvres, 47 rue de Batignolles, 75017 Paris, France
|
Chez Miamophile,6 rue Mélingue,75019,Paris
|
||||||
Café Dupont, 198 rue de la Convention, 75015 Paris, France
|
Panem,18 rue de Crussol,75011,Paris
|
||||||
L'Angle, 28 rue de Ponthieu, 75008 Paris, France
|
Petits Freres des Pauvres,47 rue de Batignolles,75017,Paris
|
||||||
Institut des Cultures d'Islam, 19-23 rue Léon, 75018 Paris, France
|
Café Dupont,198 rue de la Convention,75015,Paris
|
||||||
Canopy Café associatif, 19 rue Pajol, 75018 Paris, France
|
L'Angle,28 rue de Ponthieu,75008,Paris
|
||||||
L'Entracte, place de l'opera, 75002 Paris, France
|
Institut des Cultures d'Islam,19-23 rue Léon,75018,Paris
|
||||||
Le Sévigné, 15 rue du Parc Royal, 75003 Paris, France
|
Canopy Café associatif,19 rue Pajol,75018,Paris
|
||||||
Le Café d'avant, 35 rue Claude Bernard, 75005 Paris, France
|
L'Entracte,place de l'opera,75002,Paris
|
||||||
Le Lucernaire, 53 rue Notre-Dame des Champs, 75006 Paris, France
|
Le Sévigné,15 rue du Parc Royal,75003,Paris
|
||||||
Le Brigadier, 12 rue Blanche, 75009 Paris, France
|
Le Café d'avant,35 rue Claude Bernard,75005,Paris
|
||||||
L'âge d'or, 26 rue du Docteur Magnan, 75013 Paris, France
|
Le Lucernaire,53 rue Notre-Dame des Champs,75006,Paris
|
||||||
Bagels & Coffee Corner, Place de Clichy, 75017 Paris, France
|
Le Brigadier,12 rue Blanche,75009,Paris
|
||||||
Café Victor, 10 boulevard Victor, 75015 Paris, France
|
L'âge d'or,26 rue du Docteur Magnan,75013,Paris
|
||||||
L'empreinte, 54, avenue Daumesnil, 75012 Paris, France
|
Bagels & Coffee Corner,Place de Clichy,75017,Paris
|
||||||
L'horizon, 93, rue de la Roquette, 75011 Paris, France
|
Café Victor,10 boulevard Victor,75015,Paris
|
||||||
Waikiki, 10 rue d"Ulm, 75005 Paris, France
|
L'empreinte,"54, avenue Daumesnil",75012,Paris
|
||||||
Au pays de Vannes, 34 bis rue de Wattignies, 75012 Paris, France
|
L'horizon,"93, rue de la Roquette",75011,Paris
|
||||||
Café Varenne, 36 rue de Varenne, 75007 Paris, France
|
Au pays de Vannes,34 bis rue de Wattignies,75012,Paris
|
||||||
l'Eléphant du nil, 125 Rue Saint-Antoine, 75004 Paris, France
|
Café Varenne,36 rue de Varenne,75007,Paris
|
||||||
Le Comptoir, 354 bis rue Vaugirard, 75015 Paris, France
|
l'Eléphant du nil,125 Rue Saint-Antoine,75004,Paris
|
||||||
Le Parc Vaugirard, 358 rue de Vaugirard, 75015 Paris, France
|
Le Comptoir,354 bis rue Vaugirard,75015,Paris
|
||||||
le Zango, 58 rue Daguerre, 75014 Paris, France
|
Le Parc Vaugirard,358 rue de Vaugirard,75015,Paris
|
||||||
Melting Pot, 3 rue de Lagny, 75020 Paris, France
|
le Zango,58 rue Daguerre,75014,Paris
|
||||||
Pari's Café, 174 avenue de Clichy, 75017 Paris, France
|
Melting Pot,3 rue de Lagny,75020,Paris
|
||||||
|
Pari's Café,174 avenue de Clichy,75017,Paris
|
||||||
|
|||||||
@ -2,4 +2,7 @@ from bonobo import get_examples_path, open_fs
|
|||||||
|
|
||||||
|
|
||||||
def get_services():
|
def get_services():
|
||||||
return {'fs': open_fs(get_examples_path())}
|
return {
|
||||||
|
'fs': open_fs(get_examples_path()),
|
||||||
|
'fs.output': open_fs(),
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,36 @@
|
|||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.commands import get_default_services
|
from bonobo.examples.files._services import get_services
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph(*, _limit=None, _print=False):
|
||||||
|
return bonobo.Graph(
|
||||||
|
bonobo.CsvReader('datasets/coffeeshops.txt'),
|
||||||
|
*((bonobo.Limit(_limit), ) if _limit else ()),
|
||||||
|
*((bonobo.PrettyPrinter(), ) if _print else ()),
|
||||||
|
bonobo.CsvWriter('coffeeshops.csv', fs='fs.output')
|
||||||
|
)
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
|
||||||
bonobo.CsvReader('datasets/coffeeshops.txt', headers=('item', )),
|
|
||||||
bonobo.PrettyPrinter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bonobo.run(graph, services=get_default_services(__file__))
|
parser = bonobo.get_argument_parser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--limit',
|
||||||
|
'-l',
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help='If set, limits the number of processed lines.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--print',
|
||||||
|
'-p',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='If set, pretty prints before writing to output file.'
|
||||||
|
)
|
||||||
|
|
||||||
|
with bonobo.parse_args(parser) as options:
|
||||||
|
bonobo.run(
|
||||||
|
get_graph(_limit=options['limit'], _print=options['print']),
|
||||||
|
services=get_services()
|
||||||
|
)
|
||||||
|
|||||||
@ -1,17 +1,50 @@
|
|||||||
import bonobo
|
import bonobo
|
||||||
from bonobo import Bag
|
from bonobo.examples.files._services import get_services
|
||||||
from bonobo.commands import get_default_services
|
|
||||||
|
|
||||||
|
|
||||||
def get_fields(**row):
|
def get_graph(*, _limit=None, _print=False):
|
||||||
return Bag(**row['fields'])
|
graph = bonobo.Graph()
|
||||||
|
|
||||||
|
trunk = graph.add_chain(
|
||||||
graph = bonobo.Graph(
|
|
||||||
bonobo.JsonReader('datasets/theaters.json'),
|
bonobo.JsonReader('datasets/theaters.json'),
|
||||||
get_fields,
|
*((bonobo.Limit(_limit), ) if _limit else ()),
|
||||||
bonobo.PrettyPrinter(),
|
)
|
||||||
)
|
|
||||||
|
if _print:
|
||||||
|
graph.add_chain(bonobo.PrettyPrinter(), _input=trunk.output)
|
||||||
|
|
||||||
|
graph.add_chain(
|
||||||
|
bonobo.JsonWriter('theaters.json', fs='fs.output'),
|
||||||
|
_input=trunk.output
|
||||||
|
)
|
||||||
|
graph.add_chain(
|
||||||
|
bonobo.LdjsonWriter('theaters.ldjson', fs='fs.output'),
|
||||||
|
_input=trunk.output
|
||||||
|
)
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bonobo.run(graph, services=get_default_services(__file__))
|
parser = bonobo.get_argument_parser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--limit',
|
||||||
|
'-l',
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help='If set, limits the number of processed lines.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--print',
|
||||||
|
'-p',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='If set, pretty prints before writing to output file.'
|
||||||
|
)
|
||||||
|
|
||||||
|
with bonobo.parse_args(parser) as options:
|
||||||
|
bonobo.run(
|
||||||
|
get_graph(_limit=options['limit'], _print=options['print']),
|
||||||
|
services=get_services()
|
||||||
|
)
|
||||||
|
|||||||
@ -27,33 +27,51 @@ messages categorized as spam, and (3) prints the output.
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import bonobo
|
|
||||||
from bonobo.commands import get_default_services
|
|
||||||
from fs.tarfs import TarFS
|
from fs.tarfs import TarFS
|
||||||
|
|
||||||
|
import bonobo
|
||||||
|
from bonobo import examples
|
||||||
|
|
||||||
def cleanse_sms(**row):
|
|
||||||
if row['category'] == 'spam':
|
def cleanse_sms(category, sms):
|
||||||
row['sms_clean'] = '**MARKED AS SPAM** ' + row['sms'][0:50] + (
|
if category == 'spam':
|
||||||
'...' if len(row['sms']) > 50 else ''
|
sms_clean = '**MARKED AS SPAM** ' + sms[0:50] + (
|
||||||
|
'...' if len(sms) > 50 else ''
|
||||||
)
|
)
|
||||||
|
elif category == 'ham':
|
||||||
|
sms_clean = sms
|
||||||
else:
|
else:
|
||||||
row['sms_clean'] = row['sms']
|
raise ValueError('Unknown category {!r}.'.format(category))
|
||||||
|
|
||||||
return row['sms_clean']
|
return category, sms, sms_clean
|
||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
def get_graph(*, _limit=(), _print=()):
|
||||||
|
graph = bonobo.Graph()
|
||||||
|
|
||||||
|
graph.add_chain(
|
||||||
# spam.pkl is within the gzipped tarball
|
# spam.pkl is within the gzipped tarball
|
||||||
bonobo.PickleReader('spam.pkl'),
|
bonobo.PickleReader('spam.pkl'),
|
||||||
|
*_limit,
|
||||||
cleanse_sms,
|
cleanse_sms,
|
||||||
bonobo.PrettyPrinter(),
|
*_print,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
def get_services():
|
def get_services():
|
||||||
return {'fs': TarFS(bonobo.get_examples_path('datasets/spam.tgz'))}
|
from ._services import get_services
|
||||||
|
return {
|
||||||
|
**get_services(),
|
||||||
|
'fs': TarFS(bonobo.get_examples_path('datasets/spam.tgz'))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bonobo.run(graph, services=get_default_services(__file__))
|
parser = examples.get_argument_parser()
|
||||||
|
with bonobo.parse_args(parser) as options:
|
||||||
|
bonobo.run(
|
||||||
|
get_graph(**examples.get_graph_options(options)),
|
||||||
|
services=get_services()
|
||||||
|
)
|
||||||
|
|||||||
@ -1,19 +1,29 @@
|
|||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.commands import get_default_services
|
from bonobo import examples
|
||||||
|
from bonobo.examples.files._services import get_services
|
||||||
|
|
||||||
|
|
||||||
def skip_comments(line):
|
def skip_comments(line):
|
||||||
|
line = line.strip()
|
||||||
if not line.startswith('#'):
|
if not line.startswith('#'):
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
def get_graph(*, _limit=(), _print=()):
|
||||||
|
return bonobo.Graph(
|
||||||
bonobo.FileReader('datasets/passwd.txt'),
|
bonobo.FileReader('datasets/passwd.txt'),
|
||||||
skip_comments,
|
skip_comments,
|
||||||
lambda s: s.split(':'),
|
*_limit,
|
||||||
lambda l: l[0],
|
lambda s: s.split(':')[0],
|
||||||
print,
|
*_print,
|
||||||
)
|
bonobo.FileWriter('usernames.txt', fs='fs.output'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bonobo.run(graph, services=get_default_services(__file__))
|
parser = examples.get_argument_parser()
|
||||||
|
with bonobo.parse_args(parser) as options:
|
||||||
|
bonobo.run(
|
||||||
|
get_graph(**examples.get_graph_options(options)),
|
||||||
|
services=get_services()
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
from bonobo import get_examples_path, open_fs
|
|
||||||
|
|
||||||
|
|
||||||
def get_services():
|
|
||||||
return {'fs': open_fs(get_examples_path())}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
"""
|
|
||||||
Example on how to use :class:`bonobo.Bag` instances to pass flexible args/kwargs to the next callable.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "extract()" -> "transform(...)" -> "load(...)";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from bonobo import Bag, Graph
|
|
||||||
|
|
||||||
|
|
||||||
def extract():
|
|
||||||
yield Bag(topic='foo')
|
|
||||||
yield Bag(topic='bar')
|
|
||||||
yield Bag(topic='baz')
|
|
||||||
|
|
||||||
|
|
||||||
def transform(topic: str):
|
|
||||||
return Bag.inherit(title=topic.title(), rand=randint(10, 99))
|
|
||||||
|
|
||||||
|
|
||||||
def load(topic: str, title: str, rand: int):
|
|
||||||
print('{} ({}) wait={}'.format(title, topic, rand))
|
|
||||||
|
|
||||||
|
|
||||||
graph = Graph()
|
|
||||||
graph.add_chain(extract, transform, load)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from bonobo import run
|
|
||||||
|
|
||||||
run(graph)
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
"""
|
|
||||||
Simple example of :func:`bonobo.count` usage.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "range()" -> "count" -> "print";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import bonobo
|
|
||||||
|
|
||||||
graph = bonobo.Graph(range(42), bonobo.count, print)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
bonobo.run(graph)
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
"""
|
|
||||||
Example on how to use symple python dictionaries to communicate between transformations.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "extract()" -> "transform(row: dict)" -> "load(row: dict)";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from bonobo import Graph
|
|
||||||
|
|
||||||
|
|
||||||
def extract():
|
|
||||||
yield {'topic': 'foo'}
|
|
||||||
yield {'topic': 'bar'}
|
|
||||||
yield {'topic': 'baz'}
|
|
||||||
|
|
||||||
|
|
||||||
def transform(row: dict):
|
|
||||||
return {
|
|
||||||
'topic': row['topic'].title(),
|
|
||||||
'randint': randint(10, 99),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load(row: dict):
|
|
||||||
print(row)
|
|
||||||
|
|
||||||
|
|
||||||
graph = Graph(extract, transform, load)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from bonobo import run
|
|
||||||
|
|
||||||
run(graph)
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import bonobo
|
|
||||||
|
|
||||||
from bonobo import Filter
|
|
||||||
|
|
||||||
|
|
||||||
class OddOnlyFilter(Filter):
|
|
||||||
def filter(self, i):
|
|
||||||
return i % 2
|
|
||||||
|
|
||||||
|
|
||||||
@Filter
|
|
||||||
def multiples_of_three(i):
|
|
||||||
return not (i % 3)
|
|
||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
|
||||||
lambda: tuple(range(50)),
|
|
||||||
OddOnlyFilter(),
|
|
||||||
multiples_of_three,
|
|
||||||
print,
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
bonobo.run(graph)
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import bonobo
|
|
||||||
import time
|
|
||||||
|
|
||||||
from bonobo.constants import NOT_MODIFIED
|
|
||||||
|
|
||||||
|
|
||||||
def pause(*args, **kwargs):
|
|
||||||
time.sleep(0.1)
|
|
||||||
return NOT_MODIFIED
|
|
||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
|
||||||
lambda: tuple(range(20)),
|
|
||||||
pause,
|
|
||||||
print,
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
bonobo.run(graph)
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
"""
|
|
||||||
Example on how to use symple python strings to communicate between transformations.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "extract()" -> "transform(s: str)" -> "load(s: str)";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from bonobo import Graph
|
|
||||||
|
|
||||||
|
|
||||||
def extract():
|
|
||||||
yield 'foo'
|
|
||||||
yield 'bar'
|
|
||||||
yield 'baz'
|
|
||||||
|
|
||||||
|
|
||||||
def transform(s: str):
|
|
||||||
return '{} ({})'.format(s.title(), randint(10, 99))
|
|
||||||
|
|
||||||
|
|
||||||
def load(s: str):
|
|
||||||
print(s)
|
|
||||||
|
|
||||||
|
|
||||||
graph = Graph(extract, transform, load)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from bonobo import run
|
|
||||||
|
|
||||||
run(graph)
|
|
||||||
@ -13,7 +13,8 @@ class MyJsonWriter(bonobo.JsonWriter):
|
|||||||
|
|
||||||
def write(self, fs, file, lineno, **row):
|
def write(self, fs, file, lineno, **row):
|
||||||
return bonobo.FileWriter.write(
|
return bonobo.FileWriter.write(
|
||||||
self, fs, file, lineno, json.dumps(row)[1:-1]
|
self, fs, file, lineno,
|
||||||
|
json.dumps(row)[1:-1]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
from . import bags, dicts, strings
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'bags',
|
|
||||||
'dicts',
|
|
||||||
'strings',
|
|
||||||
]
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
"""
|
|
||||||
Example on how to use :class:`bonobo.Bag` instances to pass flexible args/kwargs to the next callable.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "extract()" -> "transform(...)" -> "load(...)";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from bonobo import Bag, Graph
|
|
||||||
|
|
||||||
|
|
||||||
def extract():
|
|
||||||
yield Bag(topic='foo')
|
|
||||||
yield Bag(topic='bar')
|
|
||||||
yield Bag(topic='baz')
|
|
||||||
|
|
||||||
|
|
||||||
def transform(topic: str):
|
|
||||||
return Bag.inherit(title=topic.title(), rand=randint(10, 99))
|
|
||||||
|
|
||||||
|
|
||||||
def load(topic: str, title: str, rand: int):
|
|
||||||
print('{} ({}) wait={}'.format(title, topic, rand))
|
|
||||||
|
|
||||||
|
|
||||||
graph = Graph()
|
|
||||||
graph.add_chain(extract, transform, load)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from bonobo import run
|
|
||||||
|
|
||||||
run(graph)
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
"""
|
|
||||||
Example on how to use symple python dictionaries to communicate between transformations.
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph {
|
|
||||||
rankdir = LR;
|
|
||||||
stylesheet = "../_static/graphs.css";
|
|
||||||
|
|
||||||
BEGIN [shape="point"];
|
|
||||||
BEGIN -> "extract()" -> "transform(row: dict)" -> "load(row: dict)";
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from bonobo import Graph
|
|
||||||
|
|
||||||
|
|
||||||
def extract():
|
|
||||||
yield {'topic': 'foo'}
|
|
||||||
yield {'topic': 'bar'}
|
|
||||||
yield {'topic': 'baz'}
|
|
||||||
|
|
||||||
|
|
||||||
def transform(row: dict):
|
|
||||||
return {
|
|
||||||
'topic': row['topic'].title(),
|
|
||||||
'randint': randint(10, 99),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load(row: dict):
|
|
||||||
print(row)
|
|
||||||
|
|
||||||
|
|
||||||
graph = Graph(extract, transform, load)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from bonobo import run
|
|
||||||
|
|
||||||
run(graph)
|
|
||||||
@ -23,11 +23,11 @@ def extract():
|
|||||||
yield 'baz'
|
yield 'baz'
|
||||||
|
|
||||||
|
|
||||||
def transform(s: str):
|
def transform(s):
|
||||||
return '{} ({})'.format(s.title(), randint(10, 99))
|
return '{} ({})'.format(s.title(), randint(10, 99))
|
||||||
|
|
||||||
|
|
||||||
def load(s: str):
|
def load(s):
|
||||||
print(s)
|
print(s)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import sys
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from logging import ERROR
|
from logging import ERROR
|
||||||
|
|
||||||
from bonobo.util.objects import Wrapper, get_name
|
|
||||||
from mondrian import term
|
from mondrian import term
|
||||||
|
|
||||||
|
from bonobo.util import deprecated
|
||||||
|
from bonobo.util.objects import Wrapper, get_name
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def recoverable(error_handler):
|
def recoverable(error_handler):
|
||||||
@ -107,6 +109,13 @@ class Lifecycle:
|
|||||||
|
|
||||||
self._killed = True
|
self._killed = True
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def handle_error(self, exctype, exc, tb, *, level=logging.ERROR):
|
||||||
|
return self.error((exctype, exc, tb), level=level)
|
||||||
|
|
||||||
|
def error(self, exc_info, *, level=logging.ERROR):
|
||||||
|
logging.getLogger(__name__).log(level, repr(self), exc_info=exc_info)
|
||||||
|
|
||||||
def fatal(self, exc_info, *, level=logging.CRITICAL):
|
def fatal(self, exc_info, *, level=logging.CRITICAL):
|
||||||
logging.getLogger(__name__).log(level, repr(self), exc_info=exc_info)
|
logging.getLogger(__name__).log(level, repr(self), exc_info=exc_info)
|
||||||
self._defunct = True
|
self._defunct = True
|
||||||
|
|||||||
@ -41,8 +41,8 @@ class GraphExecutionContext:
|
|||||||
outputs = self.graph.outputs_of(i)
|
outputs = self.graph.outputs_of(i)
|
||||||
if len(outputs):
|
if len(outputs):
|
||||||
node_context.outputs = [self[j].input for j in outputs]
|
node_context.outputs = [self[j].input for j in outputs]
|
||||||
node_context.input.on_begin = partial(node_context.send, BEGIN, _control=True)
|
node_context.input.on_begin = partial(node_context._send, BEGIN, _control=True)
|
||||||
node_context.input.on_end = partial(node_context.send, END, _control=True)
|
node_context.input.on_end = partial(node_context._send, END, _control=True)
|
||||||
node_context.input.on_finalize = partial(node_context.stop)
|
node_context.input.on_finalize = partial(node_context.stop)
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
|
|||||||
@ -1,25 +1,36 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from types import GeneratorType
|
from types import GeneratorType
|
||||||
|
|
||||||
from bonobo.config import create_container
|
from bonobo.config import create_container
|
||||||
from bonobo.config.processors import ContextCurrifier
|
from bonobo.config.processors import ContextCurrifier
|
||||||
from bonobo.constants import NOT_MODIFIED, BEGIN, END, TICK_PERIOD
|
from bonobo.constants import NOT_MODIFIED, BEGIN, END, TICK_PERIOD, Token
|
||||||
from bonobo.errors import InactiveReadableError, UnrecoverableError
|
from bonobo.errors import InactiveReadableError, UnrecoverableError, UnrecoverableTypeError
|
||||||
from bonobo.execution.contexts.base import BaseContext
|
from bonobo.execution.contexts.base import BaseContext
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
from bonobo.structs.inputs import Input
|
from bonobo.structs.inputs import Input
|
||||||
from bonobo.structs.tokens import Token
|
from bonobo.util import get_name, istuple, isconfigurabletype, ensure_tuple
|
||||||
from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag, istuple, isconfigurabletype
|
from bonobo.util.bags import BagType
|
||||||
from bonobo.util.statistics import WithStatistics
|
from bonobo.util.statistics import WithStatistics
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UnboundArguments = namedtuple('UnboundArguments', ['args', 'kwargs'])
|
||||||
|
|
||||||
|
|
||||||
class NodeExecutionContext(BaseContext, WithStatistics):
|
class NodeExecutionContext(BaseContext, WithStatistics):
|
||||||
def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None):
|
def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None):
|
||||||
|
"""
|
||||||
|
Node execution context has the responsibility fo storing the state of a transformation during its execution.
|
||||||
|
|
||||||
|
:param wrapped: wrapped transformation
|
||||||
|
:param parent: parent context, most probably a graph context
|
||||||
|
:param services: dict-like collection of services
|
||||||
|
:param _input: input queue (optional)
|
||||||
|
:param _outputs: output queues (optional)
|
||||||
|
"""
|
||||||
BaseContext.__init__(self, wrapped, parent=parent)
|
BaseContext.__init__(self, wrapped, parent=parent)
|
||||||
WithStatistics.__init__(self, 'in', 'out', 'err', 'warn')
|
WithStatistics.__init__(self, 'in', 'out', 'err', 'warn')
|
||||||
|
|
||||||
@ -37,6 +48,10 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
self.input = _input or Input()
|
self.input = _input or Input()
|
||||||
self.outputs = _outputs or []
|
self.outputs = _outputs or []
|
||||||
|
|
||||||
|
# Types
|
||||||
|
self._input_type, self._input_length = None, None
|
||||||
|
self._output_type = None
|
||||||
|
|
||||||
# Stack: context decorators for the execution
|
# Stack: context decorators for the execution
|
||||||
self._stack = None
|
self._stack = None
|
||||||
|
|
||||||
@ -48,22 +63,40 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' '))
|
return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' '))
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""
|
||||||
|
Starts this context, a.k.a the phase where you setup everything which will be necessary during the whole
|
||||||
|
lifetime of a transformation.
|
||||||
|
|
||||||
|
The "ContextCurrifier" is in charge of setting up a decorating stack, that includes both services and context
|
||||||
|
processors, and will call the actual node callable with additional parameters.
|
||||||
|
|
||||||
|
"""
|
||||||
super().start()
|
super().start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context())
|
initial = self._get_initial_context()
|
||||||
|
self._stack = ContextCurrifier(self.wrapped, *initial.args, **initial.kwargs)
|
||||||
if isconfigurabletype(self.wrapped):
|
if isconfigurabletype(self.wrapped):
|
||||||
# Not normal to have a partially configured object here, so let's warn the user instead of having get into
|
# Not normal to have a partially configured object here, so let's warn the user instead of having get into
|
||||||
# the hard trouble of understanding that by himself.
|
# the hard trouble of understanding that by himself.
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'The Configurable should be fully instanciated by now, unfortunately I got a PartiallyConfigured object...'
|
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(self.wrapped)
|
||||||
)
|
)
|
||||||
self._stack.setup(self)
|
self._stack.setup(self)
|
||||||
except Exception:
|
except Exception:
|
||||||
return self.fatal(sys.exc_info())
|
# Set the logging level to the lowest possible, to avoid double log.
|
||||||
|
self.fatal(sys.exc_info(), level=0)
|
||||||
|
|
||||||
|
# We raise again, so the error is not ignored out of execution loops.
|
||||||
|
raise
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
"""
|
||||||
|
The actual infinite loop for this transformation.
|
||||||
|
|
||||||
|
"""
|
||||||
logger.debug('Node loop starts for {!r}.'.format(self))
|
logger.debug('Node loop starts for {!r}.'.format(self))
|
||||||
|
|
||||||
while self.should_loop:
|
while self.should_loop:
|
||||||
try:
|
try:
|
||||||
self.step()
|
self.step()
|
||||||
@ -72,29 +105,31 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
except Empty:
|
except Empty:
|
||||||
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
|
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
|
||||||
continue
|
continue
|
||||||
except UnrecoverableError:
|
except (
|
||||||
self.handle_error(*sys.exc_info())
|
NotImplementedError,
|
||||||
self.input.shutdown()
|
UnrecoverableError,
|
||||||
break
|
):
|
||||||
|
self.fatal(sys.exc_info()) # exit loop
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
self.handle_error(*sys.exc_info())
|
self.error(sys.exc_info()) # does not exit loop
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self.handle_error(*sys.exc_info())
|
self.fatal(sys.exc_info()) # exit loop
|
||||||
break
|
|
||||||
logger.debug('Node loop ends for {!r}.'.format(self))
|
logger.debug('Node loop ends for {!r}.'.format(self))
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
"""Runs a transformation callable with given args/kwargs and flush the result into the right
|
"""
|
||||||
output channel."""
|
A single step in the loop.
|
||||||
|
|
||||||
# Pull data
|
Basically gets an input bag, send it to the node, interpret the results.
|
||||||
input_bag = self.get()
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pull and check data
|
||||||
|
input_bag = self._get()
|
||||||
|
|
||||||
# Sent through the stack
|
# Sent through the stack
|
||||||
try:
|
results = self._stack(input_bag)
|
||||||
results = input_bag.apply(self._stack)
|
|
||||||
except Exception:
|
|
||||||
return self.handle_error(*sys.exc_info())
|
|
||||||
|
|
||||||
# self._exec_time += timer.duration
|
# self._exec_time += timer.duration
|
||||||
# Put data onto output channels
|
# Put data onto output channels
|
||||||
@ -109,32 +144,85 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
# That's not an error, we're just done.
|
# That's not an error, we're just done.
|
||||||
break
|
break
|
||||||
except Exception:
|
|
||||||
# Let's kill this loop, won't be able to generate next.
|
|
||||||
self.handle_error(*sys.exc_info())
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
self.send(_resolve(input_bag, result))
|
# Push data (in case of an iterator)
|
||||||
|
self._send(self._cast(input_bag, result))
|
||||||
elif results:
|
elif results:
|
||||||
self.send(_resolve(input_bag, results))
|
# Push data (returned value)
|
||||||
|
self._send(self._cast(input_bag, results))
|
||||||
else:
|
else:
|
||||||
# case with no result, an execution went through anyway, use for stats.
|
# case with no result, an execution went through anyway, use for stats.
|
||||||
# self._exec_count += 1
|
# self._exec_count += 1
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Cleanup the context, after the loop ended.
|
||||||
|
|
||||||
|
"""
|
||||||
if self._stack:
|
if self._stack:
|
||||||
|
try:
|
||||||
self._stack.teardown()
|
self._stack.teardown()
|
||||||
|
except:
|
||||||
|
self.fatal(sys.exc_info())
|
||||||
|
|
||||||
super().stop()
|
super().stop()
|
||||||
|
|
||||||
def handle_error(self, exctype, exc, tb, *, level=logging.ERROR):
|
def send(self, *_output, _input=None):
|
||||||
self.increment('err')
|
return self._send(self._cast(_input, _output))
|
||||||
logging.getLogger(__name__).log(level, repr(self), exc_info=(exctype, exc, tb))
|
|
||||||
|
|
||||||
def fatal(self, exc_info, *, level=logging.CRITICAL):
|
### Input type and fields
|
||||||
super().fatal(exc_info, level=level)
|
@property
|
||||||
self.input.shutdown()
|
def input_type(self):
|
||||||
|
return self._input_type
|
||||||
|
|
||||||
|
def set_input_type(self, input_type):
|
||||||
|
if self._input_type is not None:
|
||||||
|
raise RuntimeError('Cannot override input type, already have %r.', self._input_type)
|
||||||
|
|
||||||
|
if type(input_type) is not type:
|
||||||
|
raise UnrecoverableTypeError('Input types must be regular python types.')
|
||||||
|
|
||||||
|
if not issubclass(input_type, tuple):
|
||||||
|
raise UnrecoverableTypeError('Input types must be subclasses of tuple (and act as tuples).')
|
||||||
|
|
||||||
|
self._input_type = input_type
|
||||||
|
|
||||||
|
def get_input_fields(self):
|
||||||
|
return self._input_type._fields if self._input_type and hasattr(self._input_type, '_fields') else None
|
||||||
|
|
||||||
|
def set_input_fields(self, fields, typename='Bag'):
|
||||||
|
self.set_input_type(BagType(typename, fields))
|
||||||
|
|
||||||
|
### Output type and fields
|
||||||
|
@property
|
||||||
|
def output_type(self):
|
||||||
|
return self._output_type
|
||||||
|
|
||||||
|
def set_output_type(self, output_type):
|
||||||
|
if self._output_type is not None:
|
||||||
|
raise RuntimeError('Cannot override output type, already have %r.', self._output_type)
|
||||||
|
|
||||||
|
if type(output_type) is not type:
|
||||||
|
raise UnrecoverableTypeError('Output types must be regular python types.')
|
||||||
|
|
||||||
|
if not issubclass(output_type, tuple):
|
||||||
|
raise UnrecoverableTypeError('Output types must be subclasses of tuple (and act as tuples).')
|
||||||
|
|
||||||
|
self._output_type = output_type
|
||||||
|
|
||||||
|
def get_output_fields(self):
|
||||||
|
return self._output_type._fields if self._output_type and hasattr(self._output_type, '_fields') else None
|
||||||
|
|
||||||
|
def set_output_fields(self, fields, typename='Bag'):
|
||||||
|
self.set_output_type(BagType(typename, fields))
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
def setdefault(self, attr, value):
|
||||||
|
try:
|
||||||
|
getattr(self, attr)
|
||||||
|
except AttributeError:
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
def write(self, *messages):
|
def write(self, *messages):
|
||||||
"""
|
"""
|
||||||
@ -143,14 +231,83 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
:param mixed value: message
|
:param mixed value: message
|
||||||
"""
|
"""
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.input.put(message if isinstance(message, (Bag, Token)) else Bag(message))
|
if isinstance(message, Token):
|
||||||
|
self.input.put(message)
|
||||||
|
elif self._input_type:
|
||||||
|
self.input.put(ensure_tuple(message, cls=self._input_type))
|
||||||
|
else:
|
||||||
|
self.input.put(ensure_tuple(message))
|
||||||
|
|
||||||
def write_sync(self, *messages):
|
def write_sync(self, *messages):
|
||||||
self.write(BEGIN, *messages, END)
|
self.write(BEGIN, *messages, END)
|
||||||
for _ in messages:
|
for _ in messages:
|
||||||
self.step()
|
self.step()
|
||||||
|
|
||||||
def send(self, value, _control=False):
|
def error(self, exc_info, *, level=logging.ERROR):
|
||||||
|
self.increment('err')
|
||||||
|
super().error(exc_info, level=level)
|
||||||
|
|
||||||
|
def fatal(self, exc_info, *, level=logging.CRITICAL):
|
||||||
|
self.increment('err')
|
||||||
|
super().fatal(exc_info, level=level)
|
||||||
|
self.input.shutdown()
|
||||||
|
|
||||||
|
def get_service(self, name):
|
||||||
|
if self.parent:
|
||||||
|
return self.parent.services.get(name)
|
||||||
|
return self.services.get(name)
|
||||||
|
|
||||||
|
def _get(self):
|
||||||
|
"""
|
||||||
|
Read from the input queue.
|
||||||
|
|
||||||
|
If Queue raises (like Timeout or Empty), stat won't be changed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
input_bag = self.input.get()
|
||||||
|
|
||||||
|
# Store or check input type
|
||||||
|
if self._input_type is None:
|
||||||
|
self._input_type = type(input_bag)
|
||||||
|
elif type(input_bag) is not self._input_type:
|
||||||
|
raise UnrecoverableTypeError(
|
||||||
|
'Input type changed between calls to {!r}.\nGot {!r} which is not of type {!r}.'.format(
|
||||||
|
self.wrapped, input_bag, self._input_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store or check input length, which is a soft fallback in case we're just using tuples
|
||||||
|
if self._input_length is None:
|
||||||
|
self._input_length = len(input_bag)
|
||||||
|
elif len(input_bag) != self._input_length:
|
||||||
|
raise UnrecoverableTypeError(
|
||||||
|
'Input length changed between calls to {!r}.\nExpected {} but got {}: {!r}.'.format(
|
||||||
|
self.wrapped, self._input_length, len(input_bag), input_bag
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.increment('in') # XXX should that go before type check ?
|
||||||
|
|
||||||
|
return input_bag
|
||||||
|
|
||||||
|
def _cast(self, _input, _output):
|
||||||
|
"""
|
||||||
|
Transforms a pair of input/output into what is the real output.
|
||||||
|
|
||||||
|
:param _input: Bag
|
||||||
|
:param _output: mixed
|
||||||
|
:return: Bag
|
||||||
|
"""
|
||||||
|
|
||||||
|
if _output is NOT_MODIFIED:
|
||||||
|
if self._output_type is None:
|
||||||
|
return _input
|
||||||
|
else:
|
||||||
|
return self._output_type(*_input)
|
||||||
|
|
||||||
|
return ensure_tuple(_output, cls=(self.output_type or tuple))
|
||||||
|
|
||||||
|
def _send(self, value, _control=False):
|
||||||
"""
|
"""
|
||||||
Sends a message to all of this context's outputs.
|
Sends a message to all of this context's outputs.
|
||||||
|
|
||||||
@ -161,29 +318,15 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
if not _control:
|
if not _control:
|
||||||
self.increment('out')
|
self.increment('out')
|
||||||
|
|
||||||
if iserrorbag(value):
|
|
||||||
value.apply(self.handle_error)
|
|
||||||
elif isloopbackbag(value):
|
|
||||||
self.input.put(value)
|
|
||||||
else:
|
|
||||||
for output in self.outputs:
|
for output in self.outputs:
|
||||||
output.put(value)
|
output.put(value)
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get from the queue first, then increment stats, so if Queue raise Timeout or Empty, stat won't be changed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
row = self.input.get() # XXX TIMEOUT ???
|
|
||||||
self.increment('in')
|
|
||||||
return row
|
|
||||||
|
|
||||||
def _get_initial_context(self):
|
def _get_initial_context(self):
|
||||||
if self.parent:
|
if self.parent:
|
||||||
return self.parent.services.args_for(self.wrapped)
|
return UnboundArguments((), self.parent.services.kwargs_for(self.wrapped))
|
||||||
if self.services:
|
if self.services:
|
||||||
return self.services.args_for(self.wrapped)
|
return UnboundArguments((), self.services.kwargs_for(self.wrapped))
|
||||||
return ()
|
return UnboundArguments((), {})
|
||||||
|
|
||||||
|
|
||||||
def isflag(param):
|
def isflag(param):
|
||||||
@ -210,23 +353,3 @@ def split_tokens(output):
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return output[:i], output[i:]
|
return output[:i], output[i:]
|
||||||
|
|
||||||
|
|
||||||
def _resolve(input_bag, output):
|
|
||||||
"""
|
|
||||||
This function is key to how bonobo works (and internal, too). It transforms a pair of input/output into what is the
|
|
||||||
real output.
|
|
||||||
|
|
||||||
:param input_bag: Bag
|
|
||||||
:param output: mixed
|
|
||||||
:return: Bag
|
|
||||||
"""
|
|
||||||
if isbag(output):
|
|
||||||
return output
|
|
||||||
|
|
||||||
tokens, output = split_tokens(output)
|
|
||||||
|
|
||||||
if len(tokens) == 1 and tokens[0] is NOT_MODIFIED:
|
|
||||||
return input_bag
|
|
||||||
|
|
||||||
return output if isbag(output) else Bag(output)
|
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
|
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
|
||||||
|
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import BEGIN, END
|
||||||
from bonobo.execution.strategies.base import Strategy
|
from bonobo.execution.strategies.base import Strategy
|
||||||
from bonobo.util import get_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -24,7 +22,7 @@ class ExecutorStrategy(Strategy):
|
|||||||
|
|
||||||
def execute(self, graph, **kwargs):
|
def execute(self, graph, **kwargs):
|
||||||
context = self.create_graph_execution_context(graph, **kwargs)
|
context = self.create_graph_execution_context(graph, **kwargs)
|
||||||
context.write(BEGIN, Bag(), END)
|
context.write(BEGIN, (), END)
|
||||||
|
|
||||||
futures = []
|
futures = []
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import BEGIN, END
|
||||||
from bonobo.execution.strategies.base import Strategy
|
from bonobo.execution.strategies.base import Strategy
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
|
|
||||||
|
|
||||||
class NaiveStrategy(Strategy):
|
class NaiveStrategy(Strategy):
|
||||||
@ -8,7 +7,7 @@ class NaiveStrategy(Strategy):
|
|||||||
|
|
||||||
def execute(self, graph, **kwargs):
|
def execute(self, graph, **kwargs):
|
||||||
context = self.create_graph_execution_context(graph, **kwargs)
|
context = self.create_graph_execution_context(graph, **kwargs)
|
||||||
context.write(BEGIN, Bag(), END)
|
context.write(BEGIN, (), END)
|
||||||
|
|
||||||
# start
|
# start
|
||||||
context.start()
|
context.start()
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
import operator
|
||||||
|
import pprint
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
from bonobo.util import ensure_tuple
|
||||||
|
from mondrian import term
|
||||||
|
|
||||||
from bonobo import settings
|
from bonobo import settings
|
||||||
from bonobo.config import Configurable, Option
|
from bonobo.config import Configurable, Option, Method, use_raw_input, use_context, use_no_input
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.functools import transformation_factory
|
||||||
from bonobo.constants import NOT_MODIFIED, ARGNAMES
|
from bonobo.config.processors import ContextProcessor, use_context_processor
|
||||||
from bonobo.structs.bags import Bag
|
from bonobo.constants import NOT_MODIFIED
|
||||||
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
|
||||||
|
|
||||||
@ -14,11 +20,9 @@ __all__ = [
|
|||||||
'Limit',
|
'Limit',
|
||||||
'PrettyPrinter',
|
'PrettyPrinter',
|
||||||
'Tee',
|
'Tee',
|
||||||
'Update',
|
'SetFields',
|
||||||
'arg0_to_kwargs',
|
|
||||||
'count',
|
'count',
|
||||||
'identity',
|
'identity',
|
||||||
'kwargs_to_arg0',
|
|
||||||
'noop',
|
'noop',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -35,6 +39,8 @@ class Limit(Configurable):
|
|||||||
|
|
||||||
Number of rows to let go through.
|
Number of rows to let go through.
|
||||||
|
|
||||||
|
TODO: simplify into a closure building factory?
|
||||||
|
|
||||||
"""
|
"""
|
||||||
limit = Option(positional=True, default=10)
|
limit = Option(positional=True, default=10)
|
||||||
|
|
||||||
@ -42,7 +48,7 @@ class Limit(Configurable):
|
|||||||
def counter(self, context):
|
def counter(self, context):
|
||||||
yield ValueHolder(0)
|
yield ValueHolder(0)
|
||||||
|
|
||||||
def call(self, counter, *args, **kwargs):
|
def __call__(self, counter, *args, **kwargs):
|
||||||
counter += 1
|
counter += 1
|
||||||
if counter <= self.limit:
|
if counter <= self.limit:
|
||||||
yield NOT_MODIFIED
|
yield NOT_MODIFIED
|
||||||
@ -60,17 +66,6 @@ def Tee(f):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def count(counter, *args, **kwargs):
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
|
|
||||||
@ContextProcessor.decorate(count)
|
|
||||||
def _count_counter(self, context):
|
|
||||||
counter = ValueHolder(0)
|
|
||||||
yield counter
|
|
||||||
context.send(Bag(counter._value))
|
|
||||||
|
|
||||||
|
|
||||||
def _shorten(s, w):
|
def _shorten(s, w):
|
||||||
if w and len(s) > w:
|
if w and len(s) > w:
|
||||||
s = s[0:w - 3] + '...'
|
s = s[0:w - 3] + '...'
|
||||||
@ -80,85 +75,71 @@ def _shorten(s, w):
|
|||||||
class PrettyPrinter(Configurable):
|
class PrettyPrinter(Configurable):
|
||||||
max_width = Option(
|
max_width = Option(
|
||||||
int,
|
int,
|
||||||
|
default=term.get_size()[0],
|
||||||
required=False,
|
required=False,
|
||||||
__doc__='''
|
__doc__='''
|
||||||
If set, truncates the output values longer than this to this width.
|
If set, truncates the output values longer than this to this width.
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
filter = Method(
|
||||||
formater = self._format_quiet if settings.QUIET.get() else self._format_console
|
default=
|
||||||
argnames = kwargs.get(ARGNAMES, None)
|
(lambda self, index, key, value: (value is not None) and (not isinstance(key, str) or not key.startswith('_'))),
|
||||||
|
__doc__='''
|
||||||
|
A filter that determine what to print.
|
||||||
|
|
||||||
for i, (item, value) in enumerate(
|
Default is to ignore any key starting with an underscore and none values.
|
||||||
itertools.chain(enumerate(args), filter(lambda x: not x[0].startswith('_'), kwargs.items()))
|
'''
|
||||||
):
|
|
||||||
print(formater(i, item, value, argnames=argnames))
|
|
||||||
|
|
||||||
def _format_quiet(self, i, item, value, *, argnames=None):
|
|
||||||
# XXX should we implement argnames here ?
|
|
||||||
return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip()))
|
|
||||||
|
|
||||||
def _format_console(self, i, item, value, *, argnames=None):
|
|
||||||
argnames = argnames or []
|
|
||||||
if not isinstance(item, str):
|
|
||||||
if len(argnames) >= item:
|
|
||||||
item = '{} / {}'.format(item, argnames[item])
|
|
||||||
else:
|
|
||||||
item = str(i)
|
|
||||||
|
|
||||||
return ' '.join(
|
|
||||||
(
|
|
||||||
(' ' if i else '•'), item, '=', _shorten(str(value).strip(),
|
|
||||||
self.max_width).replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def context(self, context):
|
||||||
|
yield context
|
||||||
|
|
||||||
|
def __call__(self, context, *args, **kwargs):
|
||||||
|
quiet = settings.QUIET.get()
|
||||||
|
formater = self._format_quiet if quiet else self._format_console
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
print('\u250e' + '\u2500' * (self.max_width - 1))
|
||||||
|
|
||||||
|
for index, (key, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())):
|
||||||
|
if self.filter(index, key, value):
|
||||||
|
print(formater(index, key, value, fields=context.get_input_fields()))
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
print('\u2516' + '\u2500' * (self.max_width - 1))
|
||||||
|
|
||||||
def noop(*args, **kwargs): # pylint: disable=unused-argument
|
|
||||||
from bonobo.constants import NOT_MODIFIED
|
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
def _format_quiet(self, index, key, value, *, fields=None):
|
||||||
|
# XXX should we implement argnames here ?
|
||||||
|
return ' '.join(((' ' if index else '-'), str(key), ':', str(value).strip()))
|
||||||
|
|
||||||
def arg0_to_kwargs(row):
|
def _format_console(self, index, key, value, *, fields=None):
|
||||||
"""
|
fields = fields or []
|
||||||
Transform items in a stream from "arg0" format (each call only has one positional argument, which is a dict-like
|
if not isinstance(key, str):
|
||||||
object) to "kwargs" format (each call only has keyword arguments that represent a row).
|
if len(fields) >= key and str(key) != str(fields[key]):
|
||||||
|
key = '{}{}'.format(fields[key], term.lightblack('[{}]'.format(key)))
|
||||||
|
else:
|
||||||
|
key = str(index)
|
||||||
|
|
||||||
:param row:
|
prefix = '\u2503 {} = '.format(key)
|
||||||
:return: bonobo.Bag
|
prefix_length = len(prefix)
|
||||||
"""
|
|
||||||
return Bag(**row)
|
def indent(text, prefix):
|
||||||
|
for i, line in enumerate(text.splitlines()):
|
||||||
|
yield (prefix if i else '') + line + CLEAR_EOL + '\n'
|
||||||
|
|
||||||
|
repr_of_value = ''.join(
|
||||||
|
indent(pprint.pformat(value, width=self.max_width - prefix_length), '\u2503' + ' ' * (len(prefix) - 1))
|
||||||
|
).strip()
|
||||||
|
return '{}{}{}'.format(prefix, repr_of_value.replace('\n', CLEAR_EOL + '\n'), CLEAR_EOL)
|
||||||
|
|
||||||
|
|
||||||
def kwargs_to_arg0(**row):
|
@use_no_input
|
||||||
"""
|
def noop(*args, **kwargs):
|
||||||
Transform items in a stream from "kwargs" format (each call only has keyword arguments that represent a row) to
|
return NOT_MODIFIED
|
||||||
"arg0" format (each call only has one positional argument, which is a dict-like object) .
|
|
||||||
|
|
||||||
:param **row:
|
|
||||||
:return: bonobo.Bag
|
|
||||||
"""
|
|
||||||
return Bag(row)
|
|
||||||
|
|
||||||
|
|
||||||
def Update(*consts, **kwconsts):
|
|
||||||
"""
|
|
||||||
Transformation factory to update a stream with constant values, by appending to args and updating kwargs.
|
|
||||||
|
|
||||||
:param consts: what to append to the input stream args
|
|
||||||
:param kwconsts: what to use to update input stream kwargs
|
|
||||||
:return: function
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def update(*args, **kwargs):
|
|
||||||
nonlocal consts, kwconsts
|
|
||||||
return (*args, *consts, {**kwargs, **kwconsts})
|
|
||||||
|
|
||||||
update.__name__ = 'Update({})'.format(Bag.format_args(*consts, **kwconsts))
|
|
||||||
|
|
||||||
return update
|
|
||||||
|
|
||||||
|
|
||||||
class FixedWindow(Configurable):
|
class FixedWindow(Configurable):
|
||||||
@ -176,10 +157,112 @@ class FixedWindow(Configurable):
|
|||||||
def buffer(self, context):
|
def buffer(self, context):
|
||||||
buffer = yield ValueHolder([])
|
buffer = yield ValueHolder([])
|
||||||
if len(buffer):
|
if len(buffer):
|
||||||
context.send(Bag(buffer.get()))
|
last_value = buffer.get()
|
||||||
|
last_value += [None] * (self.length - len(last_value))
|
||||||
|
context.send(*last_value)
|
||||||
|
|
||||||
def call(self, buffer, x):
|
@use_raw_input
|
||||||
buffer.append(x)
|
def __call__(self, buffer, bag):
|
||||||
|
buffer.append(bag)
|
||||||
if len(buffer) >= self.length:
|
if len(buffer) >= self.length:
|
||||||
yield buffer.get()
|
yield tuple(buffer.get())
|
||||||
buffer.set([])
|
buffer.set([])
|
||||||
|
|
||||||
|
|
||||||
|
@transformation_factory
|
||||||
|
def SetFields(fields):
|
||||||
|
@use_context
|
||||||
|
@use_no_input
|
||||||
|
def _SetFields(context):
|
||||||
|
nonlocal fields
|
||||||
|
if not context.output_type:
|
||||||
|
context.set_output_fields(fields)
|
||||||
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
return _SetFields
|
||||||
|
|
||||||
|
|
||||||
|
@transformation_factory
|
||||||
|
def UnpackItems(*items, fields=None, defaults=None):
|
||||||
|
"""
|
||||||
|
>>> UnpackItems(0)
|
||||||
|
|
||||||
|
:param items:
|
||||||
|
:param fields:
|
||||||
|
:param defaults:
|
||||||
|
:return: callable
|
||||||
|
"""
|
||||||
|
defaults = defaults or {}
|
||||||
|
|
||||||
|
@use_context
|
||||||
|
@use_raw_input
|
||||||
|
def _UnpackItems(context, bag):
|
||||||
|
nonlocal fields, items, defaults
|
||||||
|
|
||||||
|
if fields is None:
|
||||||
|
fields = ()
|
||||||
|
for item in items:
|
||||||
|
fields += tuple(bag[item].keys())
|
||||||
|
context.set_output_fields(fields)
|
||||||
|
|
||||||
|
values = ()
|
||||||
|
for item in items:
|
||||||
|
values += tuple(bag[item].get(field, defaults.get(field)) for field in fields)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
return _UnpackItems
|
||||||
|
|
||||||
|
|
||||||
|
@transformation_factory
|
||||||
|
def Rename(**translations):
|
||||||
|
# XXX todo handle duplicated
|
||||||
|
|
||||||
|
fields = None
|
||||||
|
translations = {v: k for k, v in translations.items()}
|
||||||
|
|
||||||
|
@use_context
|
||||||
|
@use_raw_input
|
||||||
|
def _Rename(context, bag):
|
||||||
|
nonlocal fields, translations
|
||||||
|
|
||||||
|
if not fields:
|
||||||
|
fields = tuple(translations.get(field, field) for field in context.get_input_fields())
|
||||||
|
context.set_output_fields(fields)
|
||||||
|
|
||||||
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
return _Rename
|
||||||
|
|
||||||
|
|
||||||
|
@transformation_factory
|
||||||
|
def Format(**formats):
|
||||||
|
fields, newfields = None, None
|
||||||
|
|
||||||
|
@use_context
|
||||||
|
@use_raw_input
|
||||||
|
def _Format(context, bag):
|
||||||
|
nonlocal fields, newfields, formats
|
||||||
|
|
||||||
|
if not context.output_type:
|
||||||
|
fields = context.input_type._fields
|
||||||
|
newfields = tuple(field for field in formats if not field in fields)
|
||||||
|
context.set_output_fields(fields + newfields)
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
formats[field].format(**bag._asdict()) if field in formats else bag.get(field)
|
||||||
|
for field in fields + newfields
|
||||||
|
)
|
||||||
|
|
||||||
|
return _Format
|
||||||
|
|
||||||
|
|
||||||
|
def _count(self, context):
|
||||||
|
counter = yield ValueHolder(0)
|
||||||
|
context.send(counter.get())
|
||||||
|
|
||||||
|
|
||||||
|
@use_no_input
|
||||||
|
@use_context_processor(_count)
|
||||||
|
def count(counter):
|
||||||
|
counter += 1
|
||||||
|
|||||||
@ -21,6 +21,6 @@ class Filter(Configurable):
|
|||||||
|
|
||||||
filter = Method()
|
filter = Method()
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
if self.filter(*args, **kwargs):
|
if self.filter(*args, **kwargs):
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|||||||
@ -5,10 +5,11 @@ class FileHandler(Configurable):
|
|||||||
"""Abstract component factory for file-related components.
|
"""Abstract component factory for file-related components.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
fs (str): service name to use for filesystem.
|
||||||
path (str): which path to use within the provided filesystem.
|
path (str): which path to use within the provided filesystem.
|
||||||
eol (str): which character to use to separate lines.
|
eol (str): which character to use to separate lines.
|
||||||
mode (str): which mode to use when opening the file.
|
mode (str): which mode to use when opening the file.
|
||||||
fs (str): service name to use for filesystem.
|
encoding (str): which encoding to use when opening the file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = Option(str, required=True, positional=True) # type: str
|
path = Option(str, required=True, positional=True) # type: str
|
||||||
@ -19,7 +20,7 @@ class FileHandler(Configurable):
|
|||||||
fs = Service('fs') # type: str
|
fs = Service('fs') # type: str
|
||||||
|
|
||||||
@ContextProcessor
|
@ContextProcessor
|
||||||
def file(self, context, fs):
|
def file(self, context, *, fs):
|
||||||
with self.open(fs) as file:
|
with self.open(fs) as file:
|
||||||
yield file
|
yield file
|
||||||
|
|
||||||
@ -28,22 +29,8 @@ class FileHandler(Configurable):
|
|||||||
|
|
||||||
|
|
||||||
class Reader:
|
class Reader:
|
||||||
"""Abstract component factory for readers.
|
pass
|
||||||
"""
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
yield from self.read(*args, **kwargs)
|
|
||||||
|
|
||||||
def read(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError('Abstract.')
|
|
||||||
|
|
||||||
|
|
||||||
class Writer:
|
class Writer:
|
||||||
"""Abstract component factory for writers.
|
pass
|
||||||
"""
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
return self.write(*args, **kwargs)
|
|
||||||
|
|
||||||
def write(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError('Abstract.')
|
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import csv
|
import csv
|
||||||
import warnings
|
|
||||||
|
|
||||||
from bonobo.config import Option, ContextProcessor
|
from bonobo.config import Option, use_raw_input, use_context
|
||||||
from bonobo.config.options import RemovedOption, Method
|
from bonobo.config.options import Method, RenamedOption
|
||||||
from bonobo.constants import NOT_MODIFIED, ARGNAMES
|
from bonobo.constants import NOT_MODIFIED
|
||||||
from bonobo.nodes.io.base import FileHandler
|
from bonobo.nodes.io.base import FileHandler
|
||||||
from bonobo.nodes.io.file import FileReader, FileWriter
|
from bonobo.nodes.io.file import FileReader, FileWriter
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
from bonobo.util import ensure_tuple
|
from bonobo.util import ensure_tuple
|
||||||
|
from bonobo.util.bags import BagType
|
||||||
|
|
||||||
|
|
||||||
class CsvHandler(FileHandler):
|
class CsvHandler(FileHandler):
|
||||||
@ -21,17 +20,38 @@ class CsvHandler(FileHandler):
|
|||||||
|
|
||||||
The CSV quote character.
|
The CSV quote character.
|
||||||
|
|
||||||
.. attribute:: headers
|
.. attribute:: fields
|
||||||
|
|
||||||
The list of column names, if the CSV does not contain it as its first line.
|
The list of column names, if the CSV does not contain it as its first line.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
delimiter = Option(str, default=';')
|
|
||||||
quotechar = Option(str, default='"')
|
# Dialect related options
|
||||||
headers = Option(ensure_tuple, required=False)
|
delimiter = Option(str, default=csv.excel.delimiter, required=False)
|
||||||
ioformat = RemovedOption(positional=False, value='kwargs')
|
quotechar = Option(str, default=csv.excel.quotechar, required=False)
|
||||||
|
escapechar = Option(str, default=csv.excel.escapechar, required=False)
|
||||||
|
doublequote = Option(str, default=csv.excel.doublequote, required=False)
|
||||||
|
skipinitialspace = Option(str, default=csv.excel.skipinitialspace, required=False)
|
||||||
|
lineterminator = Option(str, default=csv.excel.lineterminator, required=False)
|
||||||
|
quoting = Option(str, default=csv.excel.quoting, required=False)
|
||||||
|
|
||||||
|
# Fields (renamed from headers)
|
||||||
|
headers = RenamedOption('fields')
|
||||||
|
fields = Option(ensure_tuple, required=False)
|
||||||
|
|
||||||
|
def get_dialect_kwargs(self):
|
||||||
|
return {
|
||||||
|
'delimiter': self.delimiter,
|
||||||
|
'quotechar': self.quotechar,
|
||||||
|
'escapechar': self.escapechar,
|
||||||
|
'doublequote': self.doublequote,
|
||||||
|
'skipinitialspace': self.skipinitialspace,
|
||||||
|
'lineterminator': self.lineterminator,
|
||||||
|
'quoting': self.quoting,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
class CsvReader(FileReader, CsvHandler):
|
class CsvReader(FileReader, CsvHandler):
|
||||||
"""
|
"""
|
||||||
Reads a CSV and yield the values as dicts.
|
Reads a CSV and yield the values as dicts.
|
||||||
@ -45,6 +65,7 @@ class CsvReader(FileReader, CsvHandler):
|
|||||||
skip = Option(int, default=0)
|
skip = Option(int, default=0)
|
||||||
|
|
||||||
@Method(
|
@Method(
|
||||||
|
positional=False,
|
||||||
__doc__='''
|
__doc__='''
|
||||||
Builds the CSV reader, a.k.a an object we can iterate, each iteration giving one line of fields, as an
|
Builds the CSV reader, a.k.a an object we can iterate, each iteration giving one line of fields, as an
|
||||||
iterable.
|
iterable.
|
||||||
@ -53,20 +74,37 @@ class CsvReader(FileReader, CsvHandler):
|
|||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
def reader_factory(self, file):
|
def reader_factory(self, file):
|
||||||
return csv.reader(file, delimiter=self.delimiter, quotechar=self.quotechar)
|
return csv.reader(file, **self.get_dialect_kwargs())
|
||||||
|
|
||||||
def read(self, fs, file):
|
def read(self, file, context, *, fs):
|
||||||
|
context.setdefault('skipped', 0)
|
||||||
reader = self.reader_factory(file)
|
reader = self.reader_factory(file)
|
||||||
headers = self.headers or next(reader)
|
skip = self.skip
|
||||||
|
|
||||||
|
if not context.output_type:
|
||||||
|
context.set_output_fields(self.fields or next(reader))
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
yield Bag(*row, **{ARGNAMES: headers})
|
if context.skipped < skip:
|
||||||
|
context.skipped += 1
|
||||||
|
continue
|
||||||
|
yield tuple(row)
|
||||||
|
|
||||||
|
__call__ = read
|
||||||
|
|
||||||
|
|
||||||
|
def get_values(args, *, fields):
|
||||||
|
print(fields, args)
|
||||||
|
|
||||||
|
return
|
||||||
|
if context.input_type and context.input_type is tuple:
|
||||||
|
context.writer(bag[0:len(context.fields)])
|
||||||
|
else:
|
||||||
|
context.writer([bag.get(field) if type(field) is str else bag[field] for field in context.fields])
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
class CsvWriter(FileWriter, CsvHandler):
|
class CsvWriter(FileWriter, CsvHandler):
|
||||||
@ContextProcessor
|
|
||||||
def context(self, context, *args):
|
|
||||||
yield context
|
|
||||||
|
|
||||||
@Method(
|
@Method(
|
||||||
__doc__='''
|
__doc__='''
|
||||||
Builds the CSV writer, a.k.a an object we can pass a field collection to be written as one line in the
|
Builds the CSV writer, a.k.a an object we can pass a field collection to be written as one line in the
|
||||||
@ -76,34 +114,31 @@ class CsvWriter(FileWriter, CsvHandler):
|
|||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
def writer_factory(self, file):
|
def writer_factory(self, file):
|
||||||
return csv.writer(file, delimiter=self.delimiter, quotechar=self.quotechar, lineterminator=self.eol).writerow
|
return csv.writer(file, **self.get_dialect_kwargs()).writerow
|
||||||
|
|
||||||
def write(self, fs, file, lineno, context, *args, _argnames=None):
|
def write(self, file, context, *values, fs):
|
||||||
try:
|
context.setdefault('lineno', 0)
|
||||||
writer = context.writer
|
fields = context.get_input_fields()
|
||||||
except AttributeError:
|
|
||||||
|
if not context.lineno:
|
||||||
context.writer = self.writer_factory(file)
|
context.writer = self.writer_factory(file)
|
||||||
writer = context.writer
|
|
||||||
context.headers = self.headers or _argnames
|
|
||||||
|
|
||||||
if context.headers and not lineno:
|
if fields:
|
||||||
writer(context.headers)
|
context.writer(fields)
|
||||||
|
context.lineno += 1
|
||||||
|
|
||||||
lineno += 1
|
if fields:
|
||||||
|
if len(values) != len(fields):
|
||||||
if context.headers:
|
raise ValueError(
|
||||||
try:
|
'Values length differs from input fields length. Expected: {}. Got: {}. Values: {!r}.'.format(
|
||||||
row = [args[i] for i, header in enumerate(context.headers) if header]
|
len(fields), len(values), values
|
||||||
except IndexError:
|
|
||||||
warnings.warn(
|
|
||||||
'At line #{}, expected {} fields but only got {}. Padding with empty strings.'.format(
|
|
||||||
lineno, len(context.headers), len(args)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
row = [(args[i] if i < len(args) else '') for i, header in enumerate(context.headers) if header]
|
context.writer(values)
|
||||||
else:
|
else:
|
||||||
row = args
|
for arg in values:
|
||||||
|
context.writer(ensure_tuple(arg))
|
||||||
writer(row)
|
|
||||||
|
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
__call__ = write
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from bonobo.config import Option
|
from bonobo.config import Option, ContextProcessor, use_context
|
||||||
from bonobo.config.processors import ContextProcessor
|
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.constants import NOT_MODIFIED
|
||||||
|
from bonobo.errors import UnrecoverableError
|
||||||
from bonobo.nodes.io.base import FileHandler, Reader, Writer
|
from bonobo.nodes.io.base import FileHandler, Reader, Writer
|
||||||
from bonobo.util.objects import ValueHolder
|
from bonobo.util import ensure_tuple
|
||||||
|
|
||||||
|
|
||||||
class FileReader(Reader, FileHandler):
|
class FileReader(Reader, FileHandler):
|
||||||
@ -14,7 +14,44 @@ class FileReader(Reader, FileHandler):
|
|||||||
|
|
||||||
mode = Option(str, default='r')
|
mode = Option(str, default='r')
|
||||||
|
|
||||||
def read(self, fs, file):
|
output_fields = Option(
|
||||||
|
ensure_tuple,
|
||||||
|
required=False,
|
||||||
|
__doc__='''
|
||||||
|
Specify the field names of output lines.
|
||||||
|
Mutually exclusive with "output_type".
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
output_type = Option(
|
||||||
|
required=False,
|
||||||
|
__doc__='''
|
||||||
|
Specify the type of output lines.
|
||||||
|
Mutually exclusive with "output_fields".
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def output(self, context, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Allow all readers to use eventually use output_fields XOR output_type options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
output_fields = self.output_fields
|
||||||
|
output_type = self.output_type
|
||||||
|
|
||||||
|
if output_fields and output_type:
|
||||||
|
raise UnrecoverableError('Cannot specify both output_fields and output_type option.')
|
||||||
|
|
||||||
|
if self.output_type:
|
||||||
|
context.set_output_type(self.output_type)
|
||||||
|
|
||||||
|
if self.output_fields:
|
||||||
|
context.set_output_fields(self.output_fields)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
def read(self, file, *, fs):
|
||||||
"""
|
"""
|
||||||
Write a row on the next line of given file.
|
Write a row on the next line of given file.
|
||||||
Prefix is used for newlines.
|
Prefix is used for newlines.
|
||||||
@ -22,7 +59,10 @@ class FileReader(Reader, FileHandler):
|
|||||||
for line in file:
|
for line in file:
|
||||||
yield line.rstrip(self.eol)
|
yield line.rstrip(self.eol)
|
||||||
|
|
||||||
|
__call__ = read
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
class FileWriter(Writer, FileHandler):
|
class FileWriter(Writer, FileHandler):
|
||||||
"""Component factory for file or file-like writers.
|
"""Component factory for file or file-like writers.
|
||||||
|
|
||||||
@ -32,18 +72,16 @@ class FileWriter(Writer, FileHandler):
|
|||||||
|
|
||||||
mode = Option(str, default='w+')
|
mode = Option(str, default='w+')
|
||||||
|
|
||||||
@ContextProcessor
|
def write(self, file, context, line, *, fs):
|
||||||
def lineno(self, context, fs, file):
|
|
||||||
lineno = ValueHolder(0)
|
|
||||||
yield lineno
|
|
||||||
|
|
||||||
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 '') + line)
|
context.setdefault('lineno', 0)
|
||||||
lineno += 1
|
self._write_line(file, (self.eol if context.lineno else '') + line)
|
||||||
|
context.lineno += 1
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|
||||||
def _write_line(self, file, line):
|
def _write_line(self, file, line):
|
||||||
return file.write(line)
|
return file.write(line)
|
||||||
|
|
||||||
|
__call__ = write
|
||||||
|
|||||||
@ -1,78 +1,86 @@
|
|||||||
import json
|
import json
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from bonobo.config.options import RemovedOption
|
from bonobo.config import Method
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor, use_context
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.constants import NOT_MODIFIED
|
||||||
from bonobo.nodes.io.base import FileHandler
|
from bonobo.nodes.io.base import FileHandler
|
||||||
from bonobo.nodes.io.file import FileReader, FileWriter
|
from bonobo.nodes.io.file import FileReader, FileWriter
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
|
|
||||||
|
|
||||||
class JsonHandler(FileHandler):
|
class JsonHandler(FileHandler):
|
||||||
eol = ',\n'
|
eol = ',\n'
|
||||||
prefix, suffix = '[', ']'
|
prefix, suffix = '[', ']'
|
||||||
ioformat = RemovedOption(positional=False, value='kwargs')
|
|
||||||
|
|
||||||
|
|
||||||
class JsonReader(FileReader, JsonHandler):
|
class LdjsonHandler(FileHandler):
|
||||||
loader = staticmethod(json.load)
|
eol = '\n'
|
||||||
|
prefix, suffix = '', ''
|
||||||
def read(self, fs, file):
|
|
||||||
for line in self.loader(file):
|
|
||||||
yield line
|
|
||||||
|
|
||||||
|
|
||||||
class JsonDictItemsReader(JsonReader):
|
class JsonReader(JsonHandler, FileReader):
|
||||||
def read(self, fs, file):
|
@Method(positional=False)
|
||||||
for line in self.loader(file).items():
|
def loader(self, file):
|
||||||
yield Bag(*line)
|
return json.loads(file)
|
||||||
|
|
||||||
|
def read(self, file, *, fs):
|
||||||
|
yield from self.loader(file.read())
|
||||||
|
|
||||||
|
__call__ = read
|
||||||
|
|
||||||
|
|
||||||
class JsonWriter(FileWriter, JsonHandler):
|
class LdjsonReader(LdjsonHandler, JsonReader):
|
||||||
|
"""
|
||||||
|
Read a stream of line-delimited JSON objects (one object per line).
|
||||||
|
|
||||||
|
Not to be mistaken with JSON-LD (where LD stands for linked data).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read(self, file, *, fs):
|
||||||
|
yield from map(self.loader, file)
|
||||||
|
|
||||||
|
__call__ = read
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
|
class JsonWriter(JsonHandler, FileWriter):
|
||||||
@ContextProcessor
|
@ContextProcessor
|
||||||
def envelope(self, context, fs, file, lineno):
|
def envelope(self, context, file, *, fs):
|
||||||
file.write(self.prefix)
|
file.write(self.prefix)
|
||||||
yield
|
yield
|
||||||
file.write(self.suffix)
|
file.write(self.suffix)
|
||||||
|
|
||||||
def write(self, fs, file, lineno, arg0=None, **kwargs):
|
def write(self, file, context, *args, fs):
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
row = _getrow(arg0, kwargs)
|
context.setdefault('lineno', 0)
|
||||||
self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row))
|
fields = context.get_input_fields()
|
||||||
lineno += 1
|
|
||||||
|
if fields:
|
||||||
|
prefix = self.eol if context.lineno else ''
|
||||||
|
self._write_line(file, prefix + json.dumps(OrderedDict(zip(fields, args))))
|
||||||
|
context.lineno += 1
|
||||||
|
else:
|
||||||
|
for arg in args:
|
||||||
|
prefix = self.eol if context.lineno else ''
|
||||||
|
self._write_line(file, prefix + json.dumps(arg))
|
||||||
|
context.lineno += 1
|
||||||
|
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
__call__ = write
|
||||||
class LdjsonReader(FileReader):
|
|
||||||
"""Read a stream of JSON objects, one object per line."""
|
|
||||||
loader = staticmethod(json.loads)
|
|
||||||
|
|
||||||
def read(self, fs, file):
|
|
||||||
for line in file:
|
|
||||||
yield self.loader(line)
|
|
||||||
|
|
||||||
|
|
||||||
class LdjsonWriter(FileWriter):
|
@use_context
|
||||||
"""Write a stream of JSON objects, one object per line."""
|
class LdjsonWriter(LdjsonHandler, JsonWriter):
|
||||||
|
"""
|
||||||
|
Write a stream of Line-delimited JSON objects (one object per line).
|
||||||
|
|
||||||
def write(self, fs, file, lineno, arg0=None, **kwargs):
|
Not to be mistaken with JSON-LD (where LD stands for linked data).
|
||||||
row = _getrow(arg0, kwargs)
|
|
||||||
file.write(json.dumps(row) + '\n')
|
|
||||||
lineno += 1 # class-level variable
|
|
||||||
return NOT_MODIFIED
|
|
||||||
|
|
||||||
|
"""
|
||||||
def _getrow(arg0, kwargs):
|
|
||||||
if len(kwargs):
|
|
||||||
assert arg0 is None, 'Got both positional and keyword arguments, I recommend using keyword arguments.'
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
if arg0 is not None:
|
|
||||||
return arg0
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from bonobo.config import Option
|
from bonobo.config import Option, use_context
|
||||||
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
|
from bonobo.nodes.io.base import FileHandler
|
||||||
from bonobo.nodes.io.file import FileReader, FileWriter
|
from bonobo.nodes.io.file import FileReader, FileWriter
|
||||||
from bonobo.util.objects import ValueHolder
|
|
||||||
|
|
||||||
|
|
||||||
class PickleHandler(FileHandler):
|
class PickleHandler(FileHandler):
|
||||||
@ -17,9 +15,10 @@ class PickleHandler(FileHandler):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_names = Option(tuple, required=False)
|
fields = Option(tuple, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
class PickleReader(FileReader, PickleHandler):
|
class PickleReader(FileReader, PickleHandler):
|
||||||
"""
|
"""
|
||||||
Reads a Python pickle object and yields the items in dicts.
|
Reads a Python pickle object and yields the items in dicts.
|
||||||
@ -27,11 +26,7 @@ class PickleReader(FileReader, PickleHandler):
|
|||||||
|
|
||||||
mode = Option(str, default='rb')
|
mode = Option(str, default='rb')
|
||||||
|
|
||||||
@ContextProcessor
|
def read(self, file, context, *, fs):
|
||||||
def pickle_headers(self, context, fs, file):
|
|
||||||
yield ValueHolder(self.item_names)
|
|
||||||
|
|
||||||
def read(self, fs, file, pickle_headers):
|
|
||||||
data = pickle.load(file)
|
data = pickle.load(file)
|
||||||
|
|
||||||
# if the data is not iterable, then wrap the object in a list so it may be iterated
|
# if the data is not iterable, then wrap the object in a list so it may be iterated
|
||||||
@ -45,28 +40,31 @@ class PickleReader(FileReader, PickleHandler):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
iterator = iter([data])
|
iterator = iter([data])
|
||||||
|
|
||||||
if not pickle_headers.get():
|
if not context.output_type:
|
||||||
pickle_headers.set(next(iterator))
|
context.set_output_fields(self.fields or next(iterator))
|
||||||
|
fields = context.get_output_fields()
|
||||||
|
fields_length = len(fields)
|
||||||
|
|
||||||
item_count = len(pickle_headers.value)
|
for row in iterator:
|
||||||
|
if len(row) != fields_length:
|
||||||
|
raise ValueError('Received an object with {} items, expected {}.'.format(len(row), fields_length))
|
||||||
|
|
||||||
for i in iterator:
|
yield tuple(row.values() if is_dict else row)
|
||||||
if len(i) != item_count:
|
|
||||||
raise ValueError('Received an object with %d items, expecting %d.' % (
|
|
||||||
len(i),
|
|
||||||
item_count,
|
|
||||||
))
|
|
||||||
|
|
||||||
yield dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))
|
__call__ = read
|
||||||
|
|
||||||
|
|
||||||
|
@use_context
|
||||||
class PickleWriter(FileWriter, PickleHandler):
|
class PickleWriter(FileWriter, PickleHandler):
|
||||||
mode = Option(str, default='wb')
|
mode = Option(str, default='wb')
|
||||||
|
|
||||||
def write(self, fs, file, lineno, item):
|
def write(self, file, context, item, *, fs):
|
||||||
"""
|
"""
|
||||||
Write a pickled item to the opened file.
|
Write a pickled item to the opened file.
|
||||||
"""
|
"""
|
||||||
|
context.setdefault('lineno', 0)
|
||||||
file.write(pickle.dumps(item))
|
file.write(pickle.dumps(item))
|
||||||
lineno += 1
|
context.lineno += 1
|
||||||
return NOT_MODIFIED
|
return NOT_MODIFIED
|
||||||
|
|
||||||
|
__call__ = write
|
||||||
|
|||||||
@ -47,6 +47,6 @@ class RateLimited(Configurable):
|
|||||||
bucket.stop()
|
bucket.stop()
|
||||||
bucket.join()
|
bucket.join()
|
||||||
|
|
||||||
def call(self, bucket, *args, **kwargs):
|
def __call__(self, bucket, *args, **kwargs):
|
||||||
bucket.wait()
|
bucket.wait()
|
||||||
return self.handler(*args, **kwargs)
|
return self.handler(*args, **kwargs)
|
||||||
|
|||||||
@ -95,8 +95,7 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
|
|
||||||
liveliness_color = alive_color if node.alive else dead_color
|
liveliness_color = alive_color if node.alive else dead_color
|
||||||
liveliness_prefix = ' {}{}{} '.format(liveliness_color, node.status, Style.RESET_ALL)
|
liveliness_prefix = ' {}{}{} '.format(liveliness_color, node.status, Style.RESET_ALL)
|
||||||
_line = ''.join(
|
_line = ''.join((
|
||||||
(
|
|
||||||
liveliness_prefix,
|
liveliness_prefix,
|
||||||
node.name,
|
node.name,
|
||||||
name_suffix,
|
name_suffix,
|
||||||
@ -106,19 +105,16 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
node.get_flags_as_string(),
|
node.get_flags_as_string(),
|
||||||
Style.RESET_ALL,
|
Style.RESET_ALL,
|
||||||
' ',
|
' ',
|
||||||
)
|
))
|
||||||
)
|
|
||||||
print(prefix + _line + CLEAR_EOL, file=self._stderr)
|
print(prefix + _line + CLEAR_EOL, file=self._stderr)
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
# todo handle multiline
|
# todo handle multiline
|
||||||
print(
|
print(
|
||||||
''.join(
|
''.join((
|
||||||
(
|
|
||||||
' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
|
' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
|
||||||
CLEAR_EOL
|
CLEAR_EOL
|
||||||
)
|
)),
|
||||||
),
|
|
||||||
file=self._stderr
|
file=self._stderr
|
||||||
)
|
)
|
||||||
t_cnt += 1
|
t_cnt += 1
|
||||||
@ -132,8 +128,7 @@ class ConsoleOutputPlugin(Plugin):
|
|||||||
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:
|
||||||
self._append_cache = append = (
|
self._append_cache = append = (('Memory', '{0:.2f} Mb'.format(memory_usage())),
|
||||||
('Memory', '{0:.2f} Mb'.format(memory_usage())),
|
|
||||||
# ('Total time', '{0} s'.format(execution_time(harness))),
|
# ('Total time', '{0} s'.format(execution_time(harness))),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
6
bonobo/plugins/sentry.py
Normal file
6
bonobo/plugins/sentry.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from bonobo.plugins import Plugin
|
||||||
|
from raven import Client
|
||||||
|
|
||||||
|
|
||||||
|
class SentryPlugin(Plugin):
|
||||||
|
pass
|
||||||
@ -1,10 +1,5 @@
|
|||||||
from bonobo.structs.bags import Bag, ErrorBag
|
|
||||||
from bonobo.structs.graphs import Graph
|
from bonobo.structs.graphs import Graph
|
||||||
from bonobo.structs.tokens import Token
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Bag',
|
|
||||||
'ErrorBag',
|
|
||||||
'Graph',
|
'Graph',
|
||||||
'Token',
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
import itertools
|
|
||||||
|
|
||||||
from bonobo.constants import INHERIT_INPUT, LOOPBACK
|
|
||||||
from bonobo.structs.tokens import Token
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'Bag',
|
|
||||||
'ErrorBag',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Bag:
|
|
||||||
"""
|
|
||||||
Bags are simple datastructures that holds arguments and keyword arguments together, that may be applied to a
|
|
||||||
callable.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> from bonobo import Bag
|
|
||||||
>>> def myfunc(foo, *, bar):
|
|
||||||
... print(foo, bar)
|
|
||||||
...
|
|
||||||
>>> bag = Bag('foo', bar='baz')
|
|
||||||
>>> bag.apply(myfunc)
|
|
||||||
foo baz
|
|
||||||
|
|
||||||
A bag can inherit another bag, allowing to override only a few arguments without touching the parent.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> bag2 = Bag(bar='notbaz', _parent=bag)
|
|
||||||
>>> bag2.apply(myfunc)
|
|
||||||
foo notbaz
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_flags = ()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_args(*args, **kwargs):
|
|
||||||
return ', '.join(itertools.chain(map(repr, args), ('{}={!r}'.format(k, v) for k, v in kwargs.items())))
|
|
||||||
|
|
||||||
def __new__(cls, *args, _flags=None, _parent=None, **kwargs):
|
|
||||||
# Handle the special case where we call Bag's constructor with only one bag or token as argument.
|
|
||||||
if len(args) == 1 and len(kwargs) == 0:
|
|
||||||
if isinstance(args[0], Bag):
|
|
||||||
raise ValueError('Bag cannot be instanciated with a bag (for now ...).')
|
|
||||||
|
|
||||||
if isinstance(args[0], Token):
|
|
||||||
return args[0]
|
|
||||||
|
|
||||||
# Otherwise, type will handle that for us.
|
|
||||||
return super().__new__(cls)
|
|
||||||
|
|
||||||
def __init__(self, *args, _flags=None, _parent=None, _argnames=None, **kwargs):
|
|
||||||
self._flags = type(self).default_flags + (_flags or ())
|
|
||||||
self._argnames = _argnames
|
|
||||||
self._parent = _parent
|
|
||||||
|
|
||||||
if len(args) == 1 and len(kwargs) == 0:
|
|
||||||
# If we only have one argument, that may be because we're using the shorthand syntax.
|
|
||||||
mixed = args[0]
|
|
||||||
|
|
||||||
if isinstance(mixed, Bag):
|
|
||||||
# Just duplicate the bag.
|
|
||||||
self._args = mixed.args
|
|
||||||
self._kwargs = mixed.kwargs
|
|
||||||
elif isinstance(mixed, tuple):
|
|
||||||
if not len(mixed):
|
|
||||||
# Empty bag.
|
|
||||||
self._args = ()
|
|
||||||
self._kwargs = {}
|
|
||||||
elif isinstance(mixed[-1], dict):
|
|
||||||
# Args + Kwargs
|
|
||||||
self._args = mixed[:-1]
|
|
||||||
self._kwargs = mixed[-1]
|
|
||||||
else:
|
|
||||||
# Args only
|
|
||||||
self._args = mixed
|
|
||||||
self._kwargs = {}
|
|
||||||
elif isinstance(mixed, dict):
|
|
||||||
# Kwargs only
|
|
||||||
self._args = ()
|
|
||||||
self._kwargs = mixed
|
|
||||||
else:
|
|
||||||
self._args = args
|
|
||||||
self._kwargs = {}
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Otherwise, lets get args/kwargs from the constructor.
|
|
||||||
self._args = args
|
|
||||||
self._kwargs = kwargs
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Bag({})'.format(Bag.format_args(*self.args, **self.kwargs))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def args(self):
|
|
||||||
if self._parent is None:
|
|
||||||
return self._args
|
|
||||||
return (
|
|
||||||
*self._parent.args,
|
|
||||||
*self._args,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def kwargs(self):
|
|
||||||
if self._parent is None:
|
|
||||||
return self._kwargs
|
|
||||||
return {
|
|
||||||
**self._parent.kwargs,
|
|
||||||
**self._kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def flags(self):
|
|
||||||
return self._flags
|
|
||||||
|
|
||||||
@property
|
|
||||||
def specials(self):
|
|
||||||
return {k: self.__dict__[k] for k in ('_argnames', ) if k in self.__dict__ and self.__dict__[k]}
|
|
||||||
|
|
||||||
def apply(self, func_or_iter, *args, **kwargs):
|
|
||||||
if callable(func_or_iter):
|
|
||||||
return func_or_iter(*args, *self.args, **kwargs, **self.kwargs, **self.specials)
|
|
||||||
|
|
||||||
if len(args) == 0 and len(kwargs) == 0:
|
|
||||||
try:
|
|
||||||
iter(func_or_iter)
|
|
||||||
|
|
||||||
def generator():
|
|
||||||
yield from func_or_iter
|
|
||||||
|
|
||||||
return generator()
|
|
||||||
except TypeError as exc:
|
|
||||||
raise TypeError('Could not apply bag to {}.'.format(func_or_iter)) from exc
|
|
||||||
|
|
||||||
raise TypeError('Could not apply bag to {}.'.format(func_or_iter))
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get a 2 element tuple of this bag's args and kwargs.
|
|
||||||
|
|
||||||
:return: tuple
|
|
||||||
"""
|
|
||||||
return self.args, self.kwargs
|
|
||||||
|
|
||||||
def extend(self, *args, **kwargs):
|
|
||||||
return type(self)(*args, _parent=self, **kwargs)
|
|
||||||
|
|
||||||
def set_parent(self, parent):
|
|
||||||
self._parent = parent
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def inherit(cls, *args, **kwargs):
|
|
||||||
return cls(*args, _flags=(INHERIT_INPUT, ), **kwargs)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
# XXX there are overlapping cases, but this is very handy for now. Let's think about it later.
|
|
||||||
|
|
||||||
# bag
|
|
||||||
if isinstance(other, Bag) and other.args == self.args and other.kwargs == self.kwargs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# tuple
|
|
||||||
if isinstance(other, tuple):
|
|
||||||
# self == ()
|
|
||||||
if not len(other):
|
|
||||||
return not self.args and not self.kwargs
|
|
||||||
|
|
||||||
if isinstance(other[-1], dict):
|
|
||||||
# self == (*args, {**kwargs}) ?
|
|
||||||
return other[:-1] == self.args and other[-1] == self.kwargs
|
|
||||||
|
|
||||||
# self == (*args) ?
|
|
||||||
return other == self.args and not self.kwargs
|
|
||||||
|
|
||||||
# dict (aka kwargs)
|
|
||||||
if isinstance(other, dict) and not self.args and other == self.kwargs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return len(self.args) == 1 and not self.kwargs and self.args[0] == other
|
|
||||||
|
|
||||||
def args_as_dict(self):
|
|
||||||
return dict(zip(self._argnames, self.args))
|
|
||||||
|
|
||||||
|
|
||||||
class LoopbackBag(Bag):
|
|
||||||
default_flags = (LOOPBACK, )
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorBag(Bag):
|
|
||||||
pass
|
|
||||||
@ -1,12 +1,16 @@
|
|||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
|
from collections import namedtuple
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
|
from graphviz import ExecutableNotFound
|
||||||
from graphviz.dot import Digraph
|
from graphviz.dot import Digraph
|
||||||
|
|
||||||
from bonobo.constants import BEGIN
|
from bonobo.constants import BEGIN
|
||||||
from bonobo.util import get_name
|
from bonobo.util import get_name
|
||||||
|
|
||||||
|
GraphRange = namedtuple('GraphRange', ['graph', 'input', 'output'])
|
||||||
|
|
||||||
|
|
||||||
class Graph:
|
class Graph:
|
||||||
"""
|
"""
|
||||||
@ -51,15 +55,19 @@ class Graph:
|
|||||||
if len(nodes):
|
if len(nodes):
|
||||||
_input = self._resolve_index(_input)
|
_input = self._resolve_index(_input)
|
||||||
_output = self._resolve_index(_output)
|
_output = self._resolve_index(_output)
|
||||||
|
_first = None
|
||||||
|
_last = None
|
||||||
|
|
||||||
for i, node in enumerate(nodes):
|
for i, node in enumerate(nodes):
|
||||||
_next = self.add_node(node)
|
_last = self.add_node(node)
|
||||||
if not i and _name:
|
if not i and _name:
|
||||||
if _name in self.named:
|
if _name in self.named:
|
||||||
raise KeyError('Duplicate name {!r} in graph.'.format(_name))
|
raise KeyError('Duplicate name {!r} in graph.'.format(_name))
|
||||||
self.named[_name] = _next
|
self.named[_name] = _last
|
||||||
self.outputs_of(_input, create=True).add(_next)
|
if not _first:
|
||||||
_input = _next
|
_first = _last
|
||||||
|
self.outputs_of(_input, create=True).add(_last)
|
||||||
|
_input = _last
|
||||||
|
|
||||||
if _output is not None:
|
if _output is not None:
|
||||||
self.outputs_of(_input, create=True).add(_output)
|
self.outputs_of(_input, create=True).add(_output)
|
||||||
@ -67,7 +75,8 @@ class Graph:
|
|||||||
if hasattr(self, '_topologcally_sorted_indexes_cache'):
|
if hasattr(self, '_topologcally_sorted_indexes_cache'):
|
||||||
del self._topologcally_sorted_indexes_cache
|
del self._topologcally_sorted_indexes_cache
|
||||||
|
|
||||||
return self
|
return GraphRange(self, _first, _last)
|
||||||
|
return GraphRange(self, None, None)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
g = Graph()
|
g = Graph()
|
||||||
@ -135,11 +144,11 @@ class Graph:
|
|||||||
def _repr_dot_(self):
|
def _repr_dot_(self):
|
||||||
return str(self.graphviz)
|
return str(self.graphviz)
|
||||||
|
|
||||||
def _repr_svg_(self):
|
|
||||||
return self.graphviz._repr_svg_()
|
|
||||||
|
|
||||||
def _repr_html_(self):
|
def _repr_html_(self):
|
||||||
|
try:
|
||||||
return '<div>{}</div><pre>{}</pre>'.format(self.graphviz._repr_svg_(), html.escape(repr(self)))
|
return '<div>{}</div><pre>{}</pre>'.format(self.graphviz._repr_svg_(), html.escape(repr(self)))
|
||||||
|
except (ExecutableNotFound, FileNotFoundError) as exc:
|
||||||
|
return '<strong>{}</strong>: {}'.format(type(exc).__name__, str(exc))
|
||||||
|
|
||||||
def _resolve_index(self, mixed):
|
def _resolve_index(self, mixed):
|
||||||
""" Find the index based on various strategies for a node, probably an input or output of chain. Supported inputs are indexes, node values or names.
|
""" Find the index based on various strategies for a node, probably an input or output of chain. Supported inputs are indexes, node values or names.
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
class Token:
|
|
||||||
"""Factory for signal oriented queue messages or other token types."""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.__name__ = name
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{}>'.format(self.__name__)
|
|
||||||
@ -2,13 +2,10 @@ from bonobo.util.collections import ensure_tuple, sortedlist, tuplize
|
|||||||
from bonobo.util.compat import deprecated, deprecated_alias
|
from bonobo.util.compat import deprecated, deprecated_alias
|
||||||
from bonobo.util.inspect import (
|
from bonobo.util.inspect import (
|
||||||
inspect_node,
|
inspect_node,
|
||||||
isbag,
|
|
||||||
isconfigurable,
|
isconfigurable,
|
||||||
isconfigurabletype,
|
isconfigurabletype,
|
||||||
iscontextprocessor,
|
iscontextprocessor,
|
||||||
isdict,
|
isdict,
|
||||||
iserrorbag,
|
|
||||||
isloopbackbag,
|
|
||||||
ismethod,
|
ismethod,
|
||||||
isoption,
|
isoption,
|
||||||
istuple,
|
istuple,
|
||||||
@ -25,13 +22,10 @@ __all__ = [
|
|||||||
'get_attribute_or_create',
|
'get_attribute_or_create',
|
||||||
'get_name',
|
'get_name',
|
||||||
'inspect_node',
|
'inspect_node',
|
||||||
'isbag',
|
|
||||||
'isconfigurable',
|
'isconfigurable',
|
||||||
'isconfigurabletype',
|
'isconfigurabletype',
|
||||||
'iscontextprocessor',
|
'iscontextprocessor',
|
||||||
'isdict',
|
'isdict',
|
||||||
'iserrorbag',
|
|
||||||
'isloopbackbag',
|
|
||||||
'ismethod',
|
'ismethod',
|
||||||
'isoption',
|
'isoption',
|
||||||
'istype',
|
'istype',
|
||||||
|
|||||||
187
bonobo/util/bags.py
Normal file
187
bonobo/util/bags.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import functools
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from keyword import iskeyword
|
||||||
|
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
_class_template = '''\
|
||||||
|
from builtins import property as _property, tuple as _tuple
|
||||||
|
from operator import itemgetter as _itemgetter
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
class {typename}(tuple):
|
||||||
|
'{typename}({arg_list})'
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
_attrs = {attrs!r}
|
||||||
|
_fields = {fields!r}
|
||||||
|
|
||||||
|
def __new__(_cls, {arg_list}):
|
||||||
|
"""
|
||||||
|
Create new instance of {typename}({arg_list})
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _tuple.__new__(_cls, ({arg_list}))
|
||||||
|
|
||||||
|
def __getnewargs__(self):
|
||||||
|
"""
|
||||||
|
Return self as a plain tuple.
|
||||||
|
Used by copy and pickle.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return tuple(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""
|
||||||
|
Return a nicely formatted representation string
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__ + '({repr_fmt})' % self
|
||||||
|
|
||||||
|
def get(self, field, default=None):
|
||||||
|
try:
|
||||||
|
index = self._fields.index(field)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
return self[index]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make(cls, iterable, new=tuple.__new__, len=len):
|
||||||
|
'Make a new {typename} object from a sequence or iterable'
|
||||||
|
result = new(cls, iterable)
|
||||||
|
if len(result) != {num_fields:d}:
|
||||||
|
raise TypeError('Expected {num_fields:d} arguments, got %d' % len(result))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _replace(_self, **kwds):
|
||||||
|
'Return a new {typename} object replacing specified fields with new values'
|
||||||
|
result = _self._make(map(kwds.pop, {fields!r}, _self))
|
||||||
|
if kwds:
|
||||||
|
raise ValueError('Got unexpected field names: %r' % list(kwds))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _asdict(self):
|
||||||
|
"""
|
||||||
|
Return a new OrderedDict which maps field names to their values.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return OrderedDict(zip(self._fields, self))
|
||||||
|
|
||||||
|
{field_defs}
|
||||||
|
'''
|
||||||
|
|
||||||
|
_field_template = '''\
|
||||||
|
{name} = _property(_itemgetter({index:d}), doc={doc!r})
|
||||||
|
'''.strip('\n')
|
||||||
|
|
||||||
|
_reserved = frozenset(
|
||||||
|
['_', '_cls', '_attrs', '_fields', 'get', '_asdict', '_replace', '_make', 'self', '_self', 'tuple'] + dir(tuple)
|
||||||
|
)
|
||||||
|
|
||||||
|
_multiple_underscores_pattern = re.compile('__+')
|
||||||
|
_slugify_allowed_chars_pattern = re.compile(r'[^a-z0-9_]+', flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _uniquify(f):
|
||||||
|
seen = set(_reserved)
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def _uniquified(x):
|
||||||
|
nonlocal f, seen
|
||||||
|
x = str(x)
|
||||||
|
v = v0 = _multiple_underscores_pattern.sub('_', f(x))
|
||||||
|
i = 0
|
||||||
|
# if last character is not "allowed", let's start appending indexes right from the first iteration
|
||||||
|
if len(x) and _slugify_allowed_chars_pattern.match(x[-1]):
|
||||||
|
v = '{}{}'.format(v0, i)
|
||||||
|
while v in seen:
|
||||||
|
v = '{}{}'.format(v0, i)
|
||||||
|
i += 1
|
||||||
|
seen.add(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
return _uniquified
|
||||||
|
|
||||||
|
|
||||||
|
def _make_valid_attr_name(x):
|
||||||
|
if iskeyword(x):
|
||||||
|
x = '_' + x
|
||||||
|
if x.isidentifier():
|
||||||
|
return x
|
||||||
|
x = slugify(x, separator='_', regex_pattern=_slugify_allowed_chars_pattern)
|
||||||
|
if x.isidentifier():
|
||||||
|
return x
|
||||||
|
x = '_' + x
|
||||||
|
if x.isidentifier():
|
||||||
|
return x
|
||||||
|
raise ValueError(x)
|
||||||
|
|
||||||
|
|
||||||
|
def BagType(typename, fields, *, verbose=False, module=None):
|
||||||
|
# Validate the field names. At the user's option, either generate an error
|
||||||
|
# message or automatically replace the field name with a valid name.
|
||||||
|
|
||||||
|
attrs = tuple(map(_uniquify(_make_valid_attr_name), fields))
|
||||||
|
if type(fields) is str:
|
||||||
|
raise TypeError('BagType does not support providing fields as a string.')
|
||||||
|
fields = list(map(str, fields))
|
||||||
|
typename = str(typename)
|
||||||
|
|
||||||
|
for i, name in enumerate([typename] + fields):
|
||||||
|
if type(name) is not str:
|
||||||
|
raise TypeError('Type names and field names must be strings, got {name!r}'.format(name=name))
|
||||||
|
if not i:
|
||||||
|
if not name.isidentifier():
|
||||||
|
raise ValueError('Type names must be valid identifiers: {name!r}'.format(name=name))
|
||||||
|
if iskeyword(name):
|
||||||
|
raise ValueError('Type names cannot be a keyword: {name!r}'.format(name=name))
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for name in fields:
|
||||||
|
if name in seen:
|
||||||
|
raise ValueError('Encountered duplicate field name: {name!r}'.format(name=name))
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
# Fill-in the class template
|
||||||
|
class_definition = _class_template.format(
|
||||||
|
typename=typename,
|
||||||
|
fields=tuple(fields),
|
||||||
|
attrs=attrs,
|
||||||
|
num_fields=len(fields),
|
||||||
|
arg_list=repr(attrs).replace("'", "")[1:-1],
|
||||||
|
repr_fmt=', '.join(('%r' if isinstance(fields[index], int) else '{name}=%r').format(name=name)
|
||||||
|
for index, name in enumerate(attrs)),
|
||||||
|
field_defs='\n'.join(
|
||||||
|
_field_template.format(
|
||||||
|
index=index,
|
||||||
|
name=name,
|
||||||
|
doc='Alias for ' +
|
||||||
|
('field #{}'.format(index) if isinstance(fields[index], int) else repr(fields[index]))
|
||||||
|
) for index, name in enumerate(attrs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute the template string in a temporary namespace and support
|
||||||
|
# tracing utilities by setting a value for frame.f_globals['__name__']
|
||||||
|
namespace = dict(__name__='namedtuple_%s' % typename)
|
||||||
|
exec(class_definition, namespace)
|
||||||
|
result = namespace[typename]
|
||||||
|
result._source = class_definition
|
||||||
|
if verbose:
|
||||||
|
print(result._source)
|
||||||
|
|
||||||
|
# For pickling to work, the __module__ variable needs to be set to the frame
|
||||||
|
# where the named tuple is created. Bypass this step in environments where
|
||||||
|
# sys._getframe is not defined (Jython for example) or sys._getframe is not
|
||||||
|
# defined for arguments greater than 0 (IronPython), or where the user has
|
||||||
|
# specified a particular module.
|
||||||
|
if module is None:
|
||||||
|
try:
|
||||||
|
module = sys._getframe(1).f_globals.get('__name__', '__main__')
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
|
if module is not None:
|
||||||
|
result.__module__ = module
|
||||||
|
|
||||||
|
return result
|
||||||
@ -7,7 +7,7 @@ class sortedlist(list):
|
|||||||
bisect.insort(self, x)
|
bisect.insort(self, x)
|
||||||
|
|
||||||
|
|
||||||
def ensure_tuple(tuple_or_mixed):
|
def ensure_tuple(tuple_or_mixed, *, cls=tuple):
|
||||||
"""
|
"""
|
||||||
If it's not a tuple, let's make a tuple of one item.
|
If it's not a tuple, let's make a tuple of one item.
|
||||||
Otherwise, not changed.
|
Otherwise, not changed.
|
||||||
@ -16,11 +16,17 @@ def ensure_tuple(tuple_or_mixed):
|
|||||||
:return: tuple
|
:return: tuple
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if tuple_or_mixed is None:
|
|
||||||
return ()
|
if isinstance(tuple_or_mixed, cls):
|
||||||
if isinstance(tuple_or_mixed, tuple):
|
|
||||||
return tuple_or_mixed
|
return tuple_or_mixed
|
||||||
return (tuple_or_mixed, )
|
|
||||||
|
if tuple_or_mixed is None:
|
||||||
|
return tuple.__new__(cls, ())
|
||||||
|
|
||||||
|
if isinstance(tuple_or_mixed, tuple):
|
||||||
|
return tuple.__new__(cls, tuple_or_mixed)
|
||||||
|
|
||||||
|
return tuple.__new__(cls, (tuple_or_mixed, ))
|
||||||
|
|
||||||
|
|
||||||
def tuplize(generator):
|
def tuplize(generator):
|
||||||
|
|||||||
@ -12,16 +12,20 @@ def isconfigurable(mixed):
|
|||||||
return isinstance(mixed, Configurable)
|
return isinstance(mixed, Configurable)
|
||||||
|
|
||||||
|
|
||||||
def isconfigurabletype(mixed):
|
def isconfigurabletype(mixed, *, strict=False):
|
||||||
"""
|
"""
|
||||||
Check if the given argument is an instance of :class:`bonobo.config.ConfigurableMeta`, meaning it has all the
|
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.
|
plumbery necessary to build :class:`bonobo.config.Configurable`-like instances.
|
||||||
|
|
||||||
:param mixed:
|
:param mixed:
|
||||||
|
:param strict: should we consider partially configured objects?
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
from bonobo.config.configurables import ConfigurableMeta
|
from bonobo.config.configurables import ConfigurableMeta, PartiallyConfigured
|
||||||
return isinstance(mixed, ConfigurableMeta)
|
return isinstance(mixed, (ConfigurableMeta, ) if strict else (
|
||||||
|
ConfigurableMeta,
|
||||||
|
PartiallyConfigured,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def isoption(mixed):
|
def isoption(mixed):
|
||||||
@ -88,39 +92,6 @@ def istuple(mixed):
|
|||||||
return isinstance(mixed, tuple)
|
return isinstance(mixed, tuple)
|
||||||
|
|
||||||
|
|
||||||
def isbag(mixed):
|
|
||||||
"""
|
|
||||||
Check if the given argument is an instance of a :class:`bonobo.Bag`.
|
|
||||||
|
|
||||||
:param mixed:
|
|
||||||
:return: bool
|
|
||||||
"""
|
|
||||||
from bonobo.structs.bags import Bag
|
|
||||||
return isinstance(mixed, Bag)
|
|
||||||
|
|
||||||
|
|
||||||
def iserrorbag(mixed):
|
|
||||||
"""
|
|
||||||
Check if the given argument is an instance of an :class:`bonobo.ErrorBag`.
|
|
||||||
|
|
||||||
:param mixed:
|
|
||||||
:return: bool
|
|
||||||
"""
|
|
||||||
from bonobo.structs.bags import ErrorBag
|
|
||||||
return isinstance(mixed, ErrorBag)
|
|
||||||
|
|
||||||
|
|
||||||
def isloopbackbag(mixed):
|
|
||||||
"""
|
|
||||||
Check if the given argument is an instance of a :class:`bonobo.Bag`, marked for loopback behaviour.
|
|
||||||
|
|
||||||
:param mixed:
|
|
||||||
:return: bool
|
|
||||||
"""
|
|
||||||
from bonobo.constants import LOOPBACK
|
|
||||||
return isbag(mixed) and LOOPBACK in mixed.flags
|
|
||||||
|
|
||||||
|
|
||||||
ConfigurableInspection = namedtuple(
|
ConfigurableInspection = namedtuple(
|
||||||
'ConfigurableInspection', [
|
'ConfigurableInspection', [
|
||||||
'type',
|
'type',
|
||||||
@ -149,7 +120,7 @@ def inspect_node(mixed, *, _partial=None):
|
|||||||
|
|
||||||
:raise: TypeError
|
:raise: TypeError
|
||||||
"""
|
"""
|
||||||
if isconfigurabletype(mixed):
|
if isconfigurabletype(mixed, strict=True):
|
||||||
inst, typ = None, mixed
|
inst, typ = None, mixed
|
||||||
elif isconfigurable(mixed):
|
elif isconfigurable(mixed):
|
||||||
inst, typ = mixed, type(mixed)
|
inst, typ = mixed, type(mixed)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
@ -8,8 +9,9 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import open_fs, Token, __main__, get_examples_path, Bag
|
from bonobo import open_fs, __main__, get_examples_path
|
||||||
from bonobo.commands import entrypoint
|
from bonobo.commands import entrypoint
|
||||||
|
from bonobo.constants import Token
|
||||||
from bonobo.execution.contexts.graph import GraphExecutionContext
|
from bonobo.execution.contexts.graph import GraphExecutionContext
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.execution.contexts.node import NodeExecutionContext
|
||||||
|
|
||||||
@ -24,9 +26,9 @@ def optional_contextmanager(cm, *, ignore=False):
|
|||||||
|
|
||||||
|
|
||||||
class FilesystemTester:
|
class FilesystemTester:
|
||||||
def __init__(self, extension='txt', mode='w'):
|
def __init__(self, extension='txt', mode='w', *, input_data=''):
|
||||||
self.extension = extension
|
self.extension = extension
|
||||||
self.input_data = ''
|
self.input_data = input_data
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
|
||||||
def get_services_for_reader(self, tmpdir):
|
def get_services_for_reader(self, tmpdir):
|
||||||
@ -58,7 +60,7 @@ class BufferingContext:
|
|||||||
return self.buffer
|
return self.buffer
|
||||||
|
|
||||||
def get_buffer_args_as_dicts(self):
|
def get_buffer_args_as_dicts(self):
|
||||||
return list(map(lambda x: x.args_as_dict() if isinstance(x, Bag) else dict(x), self.buffer))
|
return [row._asdict() if hasattr(row, '_asdict') else dict(row) for row in self.buffer]
|
||||||
|
|
||||||
|
|
||||||
class BufferingNodeExecutionContext(BufferingContext, NodeExecutionContext):
|
class BufferingNodeExecutionContext(BufferingContext, NodeExecutionContext):
|
||||||
@ -141,3 +143,121 @@ class EnvironmentTestCase():
|
|||||||
|
|
||||||
assert err == ''
|
assert err == ''
|
||||||
return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n'))))
|
return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n'))))
|
||||||
|
|
||||||
|
|
||||||
|
class StaticNodeTest:
|
||||||
|
node = None
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
NodeExecutionContextType = BufferingNodeExecutionContext
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def execute(self, *args, **kwargs):
|
||||||
|
with self.NodeExecutionContextType(type(self).node, services=self.services) as context:
|
||||||
|
yield context
|
||||||
|
|
||||||
|
def call(self, *args, **kwargs):
|
||||||
|
return type(self).node(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurableNodeTest:
|
||||||
|
NodeType = None
|
||||||
|
NodeExecutionContextType = BufferingNodeExecutionContext
|
||||||
|
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def incontext(*create_args, **create_kwargs):
|
||||||
|
def decorator(method):
|
||||||
|
@functools.wraps(method)
|
||||||
|
def _incontext(self, *args, **kwargs):
|
||||||
|
nonlocal create_args, create_kwargs
|
||||||
|
with self.execute(*create_args, **create_kwargs) as context:
|
||||||
|
return method(self, context, *args, **kwargs)
|
||||||
|
|
||||||
|
return _incontext
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def create(self, *args, **kwargs):
|
||||||
|
return self.NodeType(*self.get_create_args(*args), **self.get_create_kwargs(**kwargs))
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def execute(self, *args, **kwargs):
|
||||||
|
with self.NodeExecutionContextType(self.create(*args, **kwargs), services=self.services) as context:
|
||||||
|
yield context
|
||||||
|
|
||||||
|
def get_create_args(self, *args):
|
||||||
|
return args
|
||||||
|
|
||||||
|
def get_create_kwargs(self, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_filesystem_tester(self):
|
||||||
|
return FilesystemTester(self.extension, input_data=self.input_data)
|
||||||
|
|
||||||
|
|
||||||
|
class ReaderTest(ConfigurableNodeTest):
|
||||||
|
""" Helper class to test reader transformations. """
|
||||||
|
|
||||||
|
ReaderNodeType = None
|
||||||
|
|
||||||
|
extension = 'txt'
|
||||||
|
input_data = ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def NodeType(self):
|
||||||
|
return self.ReaderNodeType
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reader_test_fixture(self, tmpdir):
|
||||||
|
fs_tester = self.get_filesystem_tester()
|
||||||
|
self.fs, self.filename, self.services = fs_tester.get_services_for_reader(tmpdir)
|
||||||
|
self.tmpdir = tmpdir
|
||||||
|
|
||||||
|
def get_create_args(self, *args):
|
||||||
|
return (self.filename, ) + args
|
||||||
|
|
||||||
|
def test_customizable_output_type_transform_not_a_type(self):
|
||||||
|
context = self.NodeExecutionContextType(
|
||||||
|
self.create(*self.get_create_args(), output_type=str.upper, **self.get_create_kwargs()),
|
||||||
|
services=self.services
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
context.start()
|
||||||
|
|
||||||
|
def test_customizable_output_type_transform_not_a_tuple(self):
|
||||||
|
context = self.NodeExecutionContextType(
|
||||||
|
self.create(
|
||||||
|
*self.get_create_args(), output_type=type('UpperString', (str, ), {}), **self.get_create_kwargs()
|
||||||
|
),
|
||||||
|
services=self.services
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
context.start()
|
||||||
|
|
||||||
|
|
||||||
|
class WriterTest(ConfigurableNodeTest):
|
||||||
|
""" Helper class to test writer transformations. """
|
||||||
|
|
||||||
|
WriterNodeType = None
|
||||||
|
|
||||||
|
extension = 'txt'
|
||||||
|
input_data = ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def NodeType(self):
|
||||||
|
return self.WriterNodeType
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _writer_test_fixture(self, tmpdir):
|
||||||
|
fs_tester = self.get_filesystem_tester()
|
||||||
|
self.fs, self.filename, self.services = fs_tester.get_services_for_writer(tmpdir)
|
||||||
|
self.tmpdir = tmpdir
|
||||||
|
|
||||||
|
def get_create_args(self, *args):
|
||||||
|
return (self.filename, ) + args
|
||||||
|
|
||||||
|
def readlines(self):
|
||||||
|
with self.fs.open(self.filename) as fp:
|
||||||
|
return tuple(map(str.strip, fp.readlines()))
|
||||||
|
|||||||
@ -41,7 +41,7 @@ instances.
|
|||||||
class JoinDatabaseCategories(Configurable):
|
class JoinDatabaseCategories(Configurable):
|
||||||
database = Service('orders_database')
|
database = Service('orders_database')
|
||||||
|
|
||||||
def call(self, database, row):
|
def __call__(self, database, row):
|
||||||
return {
|
return {
|
||||||
**row,
|
**row,
|
||||||
'category': database.get_category_name_for_sku(row['sku'])
|
'category': database.get_category_name_for_sku(row['sku'])
|
||||||
|
|||||||
@ -206,7 +206,7 @@ can be used as a graph node, then use camelcase names:
|
|||||||
# configurable
|
# configurable
|
||||||
class ChangeCase(Configurable):
|
class ChangeCase(Configurable):
|
||||||
modifier = Option(default='upper')
|
modifier = Option(default='upper')
|
||||||
def call(self, s: str) -> str:
|
def __call__(self, s: str) -> str:
|
||||||
return getattr(s, self.modifier)()
|
return getattr(s, self.modifier)()
|
||||||
|
|
||||||
# transformation factory
|
# transformation factory
|
||||||
|
|||||||
@ -30,7 +30,7 @@ Configurables allows to use the following features:
|
|||||||
class PrefixIt(Configurable):
|
class PrefixIt(Configurable):
|
||||||
prefix = Option(str, positional=True, default='>>>')
|
prefix = Option(str, positional=True, default='>>>')
|
||||||
|
|
||||||
def call(self, row):
|
def __call__(self, row):
|
||||||
return self.prefix + ' ' + row
|
return self.prefix + ' ' + row
|
||||||
|
|
||||||
prefixer = PrefixIt('$')
|
prefixer = PrefixIt('$')
|
||||||
@ -48,7 +48,7 @@ Configurables allows to use the following features:
|
|||||||
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
||||||
http = Service('http.client')
|
http = Service('http.client')
|
||||||
|
|
||||||
def call(self, http):
|
def __call__(self, http):
|
||||||
resp = http.get(self.url)
|
resp = http.get(self.url)
|
||||||
|
|
||||||
for row in resp.json():
|
for row in resp.json():
|
||||||
@ -68,7 +68,7 @@ Configurables allows to use the following features:
|
|||||||
class Applier(Configurable):
|
class Applier(Configurable):
|
||||||
apply = Method()
|
apply = Method()
|
||||||
|
|
||||||
def call(self, row):
|
def __call__(self, row):
|
||||||
return self.apply(row)
|
return self.apply(row)
|
||||||
|
|
||||||
@Applier
|
@Applier
|
||||||
@ -114,7 +114,7 @@ Let's see how to use it, starting from the previous service example:
|
|||||||
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
||||||
http = Service('http.client')
|
http = Service('http.client')
|
||||||
|
|
||||||
def call(self, http):
|
def __call__(self, http):
|
||||||
resp = http.get(self.url)
|
resp = http.get(self.url)
|
||||||
|
|
||||||
for row in resp.json():
|
for row in resp.json():
|
||||||
|
|||||||
@ -30,7 +30,7 @@ Configurables allows to use the following features:
|
|||||||
class PrefixIt(Configurable):
|
class PrefixIt(Configurable):
|
||||||
prefix = Option(str, positional=True, default='>>>')
|
prefix = Option(str, positional=True, default='>>>')
|
||||||
|
|
||||||
def call(self, row):
|
def __call__(self, row):
|
||||||
return self.prefix + ' ' + row
|
return self.prefix + ' ' + row
|
||||||
|
|
||||||
prefixer = PrefixIt('$')
|
prefixer = PrefixIt('$')
|
||||||
@ -48,7 +48,7 @@ Configurables allows to use the following features:
|
|||||||
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
||||||
http = Service('http.client')
|
http = Service('http.client')
|
||||||
|
|
||||||
def call(self, http):
|
def __call__(self, http):
|
||||||
resp = http.get(self.url)
|
resp = http.get(self.url)
|
||||||
|
|
||||||
for row in resp.json():
|
for row in resp.json():
|
||||||
@ -68,7 +68,7 @@ Configurables allows to use the following features:
|
|||||||
class Applier(Configurable):
|
class Applier(Configurable):
|
||||||
apply = Method()
|
apply = Method()
|
||||||
|
|
||||||
def call(self, row):
|
def __call__(self, row):
|
||||||
return self.apply(row)
|
return self.apply(row)
|
||||||
|
|
||||||
@Applier
|
@Applier
|
||||||
@ -114,7 +114,7 @@ Let's see how to use it, starting from the previous service example:
|
|||||||
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
url = Option(default='https://jsonplaceholder.typicode.com/users')
|
||||||
http = Service('http.client')
|
http = Service('http.client')
|
||||||
|
|
||||||
def call(self, http):
|
def __call__(self, http):
|
||||||
resp = http.get(self.url)
|
resp = http.get(self.url)
|
||||||
|
|
||||||
for row in resp.json():
|
for row in resp.json():
|
||||||
|
|||||||
@ -9,12 +9,12 @@ idna==2.6
|
|||||||
imagesize==0.7.1
|
imagesize==0.7.1
|
||||||
jinja2==2.10
|
jinja2==2.10
|
||||||
markupsafe==1.0
|
markupsafe==1.0
|
||||||
py==1.4.34
|
py==1.5.2
|
||||||
pygments==2.2.0
|
pygments==2.2.0
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
pytest-sugar==0.9.0
|
pytest-sugar==0.9.0
|
||||||
pytest-timeout==1.2.0
|
pytest-timeout==1.2.0
|
||||||
pytest==3.2.3
|
pytest==3.2.5
|
||||||
pytz==2017.3
|
pytz==2017.3
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
@ -23,4 +23,4 @@ sphinx==1.6.5
|
|||||||
sphinxcontrib-websupport==1.0.1
|
sphinxcontrib-websupport==1.0.1
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
yapf==0.19.0
|
yapf==0.20.0
|
||||||
|
|||||||
@ -6,7 +6,7 @@ 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
|
||||||
fs==2.0.16
|
fs==2.0.17
|
||||||
idna==2.6
|
idna==2.6
|
||||||
packaging==16.8
|
packaging==16.8
|
||||||
pbr==3.1.1
|
pbr==3.1.1
|
||||||
|
|||||||
@ -32,7 +32,7 @@ pyzmq==17.0.0b3
|
|||||||
qtconsole==4.3.1
|
qtconsole==4.3.1
|
||||||
simplegeneric==0.8.1
|
simplegeneric==0.8.1
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
terminado==0.6
|
terminado==0.7
|
||||||
testpath==0.3.1
|
testpath==0.3.1
|
||||||
tornado==4.5.2
|
tornado==4.5.2
|
||||||
traitlets==4.3.2
|
traitlets==4.3.2
|
||||||
|
|||||||
@ -4,7 +4,7 @@ bonobo-sqlalchemy==0.5.1
|
|||||||
certifi==2017.11.5
|
certifi==2017.11.5
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
fs==2.0.16
|
fs==2.0.17
|
||||||
idna==2.6
|
idna==2.6
|
||||||
packaging==16.8
|
packaging==16.8
|
||||||
pbr==3.1.1
|
pbr==3.1.1
|
||||||
|
|||||||
@ -3,19 +3,21 @@ appdirs==1.4.3
|
|||||||
certifi==2017.11.5
|
certifi==2017.11.5
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
fs==2.0.16
|
fs==2.0.17
|
||||||
graphviz==0.8.1
|
graphviz==0.8.1
|
||||||
idna==2.6
|
idna==2.6
|
||||||
jinja2==2.10
|
jinja2==2.10
|
||||||
markupsafe==1.0
|
markupsafe==1.0
|
||||||
mondrian==0.4.0
|
mondrian==0.5.1
|
||||||
packaging==16.8
|
packaging==16.8
|
||||||
pbr==3.1.1
|
pbr==3.1.1
|
||||||
psutil==5.4.1
|
psutil==5.4.1
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
|
python-slugify==1.2.4
|
||||||
pytz==2017.3
|
pytz==2017.3
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
stevedore==1.27.1
|
stevedore==1.27.1
|
||||||
|
unidecode==0.4.21
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
whistle==1.0.0
|
whistle==1.0.0
|
||||||
|
|||||||
10
setup.py
10
setup.py
@ -43,14 +43,12 @@ else:
|
|||||||
setup(
|
setup(
|
||||||
author='Romain Dorgueil',
|
author='Romain Dorgueil',
|
||||||
author_email='romain@dorgueil.net',
|
author_email='romain@dorgueil.net',
|
||||||
data_files=[
|
data_files=[(
|
||||||
(
|
|
||||||
'share/jupyter/nbextensions/bonobo-jupyter', [
|
'share/jupyter/nbextensions/bonobo-jupyter', [
|
||||||
'bonobo/contrib/jupyter/static/extension.js', 'bonobo/contrib/jupyter/static/index.js',
|
'bonobo/contrib/jupyter/static/extension.js', 'bonobo/contrib/jupyter/static/index.js',
|
||||||
'bonobo/contrib/jupyter/static/index.js.map'
|
'bonobo/contrib/jupyter/static/index.js.map'
|
||||||
]
|
]
|
||||||
)
|
)],
|
||||||
],
|
|
||||||
description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
|
description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
|
||||||
'python 3.5+.'),
|
'python 3.5+.'),
|
||||||
license='Apache License, Version 2.0',
|
license='Apache License, Version 2.0',
|
||||||
@ -62,8 +60,8 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'graphviz (>= 0.8, < 0.9)', 'jinja2 (>= 2.9, < 3)',
|
'colorama (>= 0.3)', 'fs (>= 2.0, < 2.1)', 'graphviz (>= 0.8, < 0.9)', 'jinja2 (>= 2.9, < 3)',
|
||||||
'mondrian (>= 0.4, < 0.5)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6)', 'requests (>= 2, < 3)',
|
'mondrian (>= 0.5, < 0.6)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6)', 'python-slugify (>= 1.2, < 1.3)',
|
||||||
'stevedore (>= 1.27, < 1.28)', 'whistle (>= 1.0, < 1.1)'
|
'requests (>= 2, < 3)', 'stevedore (>= 1.27, < 1.28)', 'whistle (>= 1.0, < 1.1)'
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev': [
|
'dev': [
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import pprint
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo.config.configurables import Configurable
|
from bonobo.config.configurables import Configurable
|
||||||
|
|||||||
@ -7,7 +7,7 @@ class MethodBasedConfigurable(Configurable):
|
|||||||
foo = Option(positional=True)
|
foo = Option(positional=True)
|
||||||
bar = Option()
|
bar = Option()
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
self.handler(*args, **kwargs)
|
self.handler(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class Bobby(Configurable):
|
|||||||
def think(self, context):
|
def think(self, context):
|
||||||
yield 'different'
|
yield 'different'
|
||||||
|
|
||||||
def call(self, think, *args, **kwargs):
|
def __call__(self, think, *args, **kwargs):
|
||||||
self.handler('1', *args, **kwargs)
|
self.handler('1', *args, **kwargs)
|
||||||
self.handler2('2', *args, **kwargs)
|
self.handler2('2', *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from bonobo.config import Configurable
|
from bonobo.config import Configurable
|
||||||
from bonobo.config.processors import ContextProcessor, resolve_processors, ContextCurrifier
|
from bonobo.config.processors import ContextProcessor, resolve_processors, ContextCurrifier, use_context_processor
|
||||||
|
|
||||||
|
|
||||||
class CP1(Configurable):
|
class CP1(Configurable):
|
||||||
@ -59,5 +59,16 @@ def test_setup_teardown():
|
|||||||
o = CP1()
|
o = CP1()
|
||||||
stack = ContextCurrifier(o)
|
stack = ContextCurrifier(o)
|
||||||
stack.setup()
|
stack.setup()
|
||||||
assert o(*stack.context) == ('this is A', 'THIS IS b')
|
assert o(*stack.args) == ('this is A', 'THIS IS b')
|
||||||
stack.teardown()
|
stack.teardown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_processors_on_func():
|
||||||
|
def cp(context):
|
||||||
|
yield context
|
||||||
|
|
||||||
|
@use_context_processor(cp)
|
||||||
|
def node(context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert get_all_processors_names(node) == ['cp']
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import time
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo.util import get_name
|
from bonobo.config import Configurable, Container, Exclusive, Service, use
|
||||||
from bonobo.config import Configurable, Container, Exclusive, Service, requires
|
|
||||||
from bonobo.config.services import validate_service_name, create_container
|
from bonobo.config.services import validate_service_name, create_container
|
||||||
|
from bonobo.util import get_name
|
||||||
|
|
||||||
|
|
||||||
class PrinterInterface():
|
class PrinterInterface():
|
||||||
@ -30,7 +30,7 @@ SERVICES = Container(
|
|||||||
class MyServiceDependantConfigurable(Configurable):
|
class MyServiceDependantConfigurable(Configurable):
|
||||||
printer = Service(PrinterInterface, )
|
printer = Service(PrinterInterface, )
|
||||||
|
|
||||||
def __call__(self, printer: PrinterInterface, *args):
|
def __call__(self, *args, printer: PrinterInterface):
|
||||||
return printer.print(*args)
|
return printer.print(*args)
|
||||||
|
|
||||||
|
|
||||||
@ -51,15 +51,15 @@ def test_service_name_validator():
|
|||||||
def test_service_dependency():
|
def test_service_dependency():
|
||||||
o = MyServiceDependantConfigurable(printer='printer0')
|
o = MyServiceDependantConfigurable(printer='printer0')
|
||||||
|
|
||||||
assert o(SERVICES.get('printer0'), 'foo', 'bar') == '0;foo;bar'
|
assert o('foo', 'bar', printer=SERVICES.get('printer0')) == '0;foo;bar'
|
||||||
assert o(SERVICES.get('printer1'), 'bar', 'baz') == '1;bar;baz'
|
assert o('bar', 'baz', printer=SERVICES.get('printer1')) == '1;bar;baz'
|
||||||
assert o(*SERVICES.args_for(o), 'foo', 'bar') == '0;foo;bar'
|
assert o('foo', 'bar', **SERVICES.kwargs_for(o)) == '0;foo;bar'
|
||||||
|
|
||||||
|
|
||||||
def test_service_dependency_unavailable():
|
def test_service_dependency_unavailable():
|
||||||
o = MyServiceDependantConfigurable(printer='printer2')
|
o = MyServiceDependantConfigurable(printer='printer2')
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
SERVICES.args_for(o)
|
SERVICES.kwargs_for(o)
|
||||||
|
|
||||||
|
|
||||||
class VCR:
|
class VCR:
|
||||||
@ -100,13 +100,13 @@ def test_requires():
|
|||||||
|
|
||||||
services = Container(output=vcr.append)
|
services = Container(output=vcr.append)
|
||||||
|
|
||||||
@requires('output')
|
@use('output')
|
||||||
def append(out, x):
|
def append(out, x):
|
||||||
out(x)
|
out(x)
|
||||||
|
|
||||||
svcargs = services.args_for(append)
|
svcargs = services.kwargs_for(append)
|
||||||
assert len(svcargs) == 1
|
assert len(svcargs) == 1
|
||||||
assert svcargs[0] == vcr.append
|
assert svcargs['output'] == vcr.append
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('services', [None, {}])
|
@pytest.mark.parametrize('services', [None, {}])
|
||||||
|
|||||||
@ -2,7 +2,8 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import Bag, Graph
|
from bonobo import Graph
|
||||||
|
from bonobo.constants import EMPTY
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.execution.contexts.node import NodeExecutionContext
|
||||||
from bonobo.execution.strategies import NaiveStrategy
|
from bonobo.execution.strategies import NaiveStrategy
|
||||||
from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext
|
from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext
|
||||||
@ -13,23 +14,23 @@ def test_node_string():
|
|||||||
return 'foo'
|
return 'foo'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(f) as context:
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == 'foo'
|
assert output[0] == ('foo', )
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield 'foo'
|
yield 'foo'
|
||||||
yield 'bar'
|
yield 'bar'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(g) as context:
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == 'foo'
|
assert output[0] == ('foo', )
|
||||||
assert output[1] == 'bar'
|
assert output[1] == ('bar', )
|
||||||
|
|
||||||
|
|
||||||
def test_node_bytes():
|
def test_node_bytes():
|
||||||
@ -37,23 +38,23 @@ def test_node_bytes():
|
|||||||
return b'foo'
|
return b'foo'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(f) as context:
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
|
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == b'foo'
|
assert output[0] == (b'foo', )
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield b'foo'
|
yield b'foo'
|
||||||
yield b'bar'
|
yield b'bar'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(g) as context:
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == b'foo'
|
assert output[0] == (b'foo', )
|
||||||
assert output[1] == b'bar'
|
assert output[1] == (b'bar', )
|
||||||
|
|
||||||
|
|
||||||
def test_node_dict():
|
def test_node_dict():
|
||||||
@ -61,40 +62,38 @@ def test_node_dict():
|
|||||||
return {'id': 1, 'name': 'foo'}
|
return {'id': 1, 'name': 'foo'}
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(f) as context:
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == {'id': 1, 'name': 'foo'}
|
assert output[0] == ({'id': 1, 'name': 'foo'}, )
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield {'id': 1, 'name': 'foo'}
|
yield {'id': 1, 'name': 'foo'}
|
||||||
yield {'id': 2, 'name': 'bar'}
|
yield {'id': 2, 'name': 'bar'}
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(g) as context:
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == {'id': 1, 'name': 'foo'}
|
assert output[0] == ({'id': 1, 'name': 'foo'}, )
|
||||||
assert output[1] == {'id': 2, 'name': 'bar'}
|
assert output[1] == ({'id': 2, 'name': 'bar'}, )
|
||||||
|
|
||||||
|
|
||||||
def test_node_dict_chained():
|
def test_node_dict_chained():
|
||||||
strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext)
|
strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext)
|
||||||
|
|
||||||
def uppercase_name(**kwargs):
|
|
||||||
return {**kwargs, 'name': kwargs['name'].upper()}
|
|
||||||
|
|
||||||
def f():
|
def f():
|
||||||
return {'id': 1, 'name': 'foo'}
|
return {'id': 1, 'name': 'foo'}
|
||||||
|
|
||||||
|
def uppercase_name(values):
|
||||||
|
return {**values, 'name': values['name'].upper()}
|
||||||
|
|
||||||
graph = Graph(f, uppercase_name)
|
graph = Graph(f, uppercase_name)
|
||||||
context = strategy.execute(graph)
|
context = strategy.execute(graph)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
assert output[0] == {'id': 1, 'name': 'FOO'}
|
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
|
||||||
|
|
||||||
def g():
|
def g():
|
||||||
yield {'id': 1, 'name': 'foo'}
|
yield {'id': 1, 'name': 'foo'}
|
||||||
@ -105,8 +104,8 @@ def test_node_dict_chained():
|
|||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == {'id': 1, 'name': 'FOO'}
|
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
|
||||||
assert output[1] == {'id': 2, 'name': 'BAR'}
|
assert output[1] == ({'id': 2, 'name': 'BAR'}, )
|
||||||
|
|
||||||
|
|
||||||
def test_node_tuple():
|
def test_node_tuple():
|
||||||
@ -114,7 +113,7 @@ def test_node_tuple():
|
|||||||
return 'foo', 'bar'
|
return 'foo', 'bar'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(f) as context:
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
@ -125,7 +124,7 @@ def test_node_tuple():
|
|||||||
yield 'foo', 'baz'
|
yield 'foo', 'baz'
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(g) as context:
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
@ -167,7 +166,7 @@ def test_node_tuple_dict():
|
|||||||
return 'foo', 'bar', {'id': 1}
|
return 'foo', 'bar', {'id': 1}
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(f) as context:
|
with BufferingNodeExecutionContext(f) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
@ -178,7 +177,7 @@ def test_node_tuple_dict():
|
|||||||
yield 'foo', 'baz', {'id': 2}
|
yield 'foo', 'baz', {'id': 2}
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(g) as context:
|
with BufferingNodeExecutionContext(g) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
output = context.get_buffer()
|
||||||
|
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
|
|||||||
@ -21,19 +21,9 @@ class ResponseMock:
|
|||||||
|
|
||||||
def test_read_from_opendatasoft_api():
|
def test_read_from_opendatasoft_api():
|
||||||
extract = OpenDataSoftAPI(dataset='test-a-set')
|
extract = OpenDataSoftAPI(dataset='test-a-set')
|
||||||
with patch(
|
with patch('requests.get', return_value=ResponseMock([
|
||||||
'requests.get', return_value=ResponseMock([
|
{'fields': {'foo': 'bar'}},
|
||||||
{
|
{'fields': {'foo': 'zab'}},
|
||||||
'fields': {
|
])):
|
||||||
'foo': 'bar'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'fields': {
|
|
||||||
'foo': 'zab'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
])
|
|
||||||
):
|
|
||||||
for line in extract('http://example.com/', ValueHolder(0)):
|
for line in extract('http://example.com/', ValueHolder(0)):
|
||||||
assert 'foo' in line
|
assert 'foo' in line
|
||||||
|
|||||||
@ -9,13 +9,7 @@ def useless(*args, **kwargs):
|
|||||||
def test_not_modified():
|
def test_not_modified():
|
||||||
input_messages = [
|
input_messages = [
|
||||||
('foo', 'bar'),
|
('foo', 'bar'),
|
||||||
{
|
('foo', 'baz'),
|
||||||
'foo': 'bar'
|
|
||||||
},
|
|
||||||
('foo', {
|
|
||||||
'bar': 'baz'
|
|
||||||
}),
|
|
||||||
(),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(useless) as context:
|
with BufferingNodeExecutionContext(useless) as context:
|
||||||
|
|||||||
@ -1,56 +1,27 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import CsvReader, CsvWriter, settings
|
from bonobo import CsvReader, CsvWriter
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.constants import EMPTY
|
||||||
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext
|
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext, WriterTest, ConfigurableNodeTest, ReaderTest
|
||||||
|
|
||||||
csv_tester = FilesystemTester('csv')
|
csv_tester = FilesystemTester('csv')
|
||||||
csv_tester.input_data = 'a,b,c\na foo,b foo,c foo\na bar,b bar,c bar'
|
csv_tester.input_data = 'a,b,c\na foo,b foo,c foo\na bar,b bar,c bar'
|
||||||
|
|
||||||
|
defaults = {'lineterminator': '\n'}
|
||||||
|
|
||||||
def test_write_csv_ioformat_arg0(tmpdir):
|
incontext = ConfigurableNodeTest.incontext
|
||||||
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
CsvWriter(path=filename, ioformat=settings.IOFORMAT_ARG0)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
CsvReader(path=filename, delimiter=',', ioformat=settings.IOFORMAT_ARG0),
|
|
||||||
|
|
||||||
|
|
||||||
def test_write_csv_to_file_no_headers(tmpdir):
|
|
||||||
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
|
|
||||||
|
|
||||||
with NodeExecutionContext(CsvWriter(filename), services=services) as context:
|
|
||||||
context.write_sync(('bar', ), ('baz', 'boo'))
|
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
|
||||||
assert fp.read() == 'bar\nbaz;boo\n'
|
|
||||||
|
|
||||||
|
|
||||||
def test_write_csv_to_file_with_headers(tmpdir):
|
|
||||||
fs, filename, services = csv_tester.get_services_for_writer(tmpdir)
|
|
||||||
|
|
||||||
with NodeExecutionContext(CsvWriter(filename, headers='foo'), services=services) as context:
|
|
||||||
context.write_sync(('bar', ), ('baz', 'boo'))
|
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
|
||||||
assert fp.read() == 'foo\nbar\nbaz\n'
|
|
||||||
|
|
||||||
with pytest.raises(AttributeError):
|
|
||||||
getattr(context, 'file')
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_csv_from_file_kwargs(tmpdir):
|
def test_read_csv_from_file_kwargs(tmpdir):
|
||||||
fs, filename, services = csv_tester.get_services_for_reader(tmpdir)
|
fs, filename, services = csv_tester.get_services_for_reader(tmpdir)
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(
|
with BufferingNodeExecutionContext(CsvReader(filename, **defaults), services=services) as context:
|
||||||
CsvReader(path=filename, delimiter=','),
|
context.write_sync(EMPTY)
|
||||||
services=services,
|
|
||||||
) as context:
|
|
||||||
context.write_sync(())
|
|
||||||
|
|
||||||
assert context.get_buffer_args_as_dicts() == [
|
assert context.get_buffer_args_as_dicts() == [{
|
||||||
{
|
|
||||||
'a': 'a foo',
|
'a': 'a foo',
|
||||||
'b': 'b foo',
|
'b': 'b foo',
|
||||||
'c': 'c foo',
|
'c': 'c foo',
|
||||||
@ -58,5 +29,124 @@ def test_read_csv_from_file_kwargs(tmpdir):
|
|||||||
'a': 'a bar',
|
'a': 'a bar',
|
||||||
'b': 'b bar',
|
'b': 'b bar',
|
||||||
'c': 'c bar',
|
'c': 'c bar',
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# CSV Readers / Writers
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
class Csv:
|
||||||
|
extension = 'csv'
|
||||||
|
ReaderNodeType = CsvReader
|
||||||
|
WriterNodeType = CsvWriter
|
||||||
|
|
||||||
|
|
||||||
|
L1, L2, L3, L4 = ('a', 'hey'), ('b', 'bee'), ('c', 'see'), ('d', 'dee')
|
||||||
|
LL = ('i', 'have', 'more', 'values')
|
||||||
|
|
||||||
|
|
||||||
|
class CsvReaderTest(Csv, ReaderTest, TestCase):
|
||||||
|
input_data = '\n'.join((
|
||||||
|
'id,name',
|
||||||
|
'1,John Doe',
|
||||||
|
'2,Jane Doe',
|
||||||
|
',DPR',
|
||||||
|
'42,Elon Musk',
|
||||||
|
))
|
||||||
|
|
||||||
|
def check_output(self, context, *, prepend=None):
|
||||||
|
out = context.get_buffer()
|
||||||
|
assert out == (prepend or list()) + [
|
||||||
|
('1', 'John Doe'),
|
||||||
|
('2', 'Jane Doe'),
|
||||||
|
('', 'DPR'),
|
||||||
|
('42', 'Elon Musk'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
self.check_output(context)
|
||||||
|
assert context.get_output_fields() == ('id', 'name')
|
||||||
|
|
||||||
|
@incontext(output_type=tuple)
|
||||||
|
def test_output_type(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
self.check_output(context, prepend=[('id', 'name')])
|
||||||
|
|
||||||
|
@incontext(
|
||||||
|
output_fields=(
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
), skip=1
|
||||||
|
)
|
||||||
|
def test_output_fields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
self.check_output(context)
|
||||||
|
assert context.get_output_fields() == ('x', 'y')
|
||||||
|
|
||||||
|
|
||||||
|
class CsvWriterTest(Csv, WriterTest, TestCase):
|
||||||
|
@incontext()
|
||||||
|
def test_fields(self, context):
|
||||||
|
context.set_input_fields(['foo', 'bar'])
|
||||||
|
context.write_sync(('a', 'b'), ('c', 'd'))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'foo,bar',
|
||||||
|
'a,b',
|
||||||
|
'c,d',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_fields_from_type(self, context):
|
||||||
|
context.set_input_type(namedtuple('Point', 'x y'))
|
||||||
|
context.write_sync((1, 2), (3, 4))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == ('x,y', '1,2', '3,4')
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args(self, context):
|
||||||
|
# multiple args are iterated onto and flattened in output
|
||||||
|
context.write_sync((L1, L2), (L3, L4))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'a,hey',
|
||||||
|
'b,bee',
|
||||||
|
'c,see',
|
||||||
|
'd,dee',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args_length_mismatch(self, context):
|
||||||
|
# if length of input vary, then we get a TypeError (unrecoverable)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
context.write_sync((L1, L2), (L3, ))
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_single_arg(self, context):
|
||||||
|
# single args are just dumped, shapes can vary.
|
||||||
|
context.write_sync((L1, ), (LL, ), (L3, ))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'a,hey',
|
||||||
|
'i,have,more,values',
|
||||||
|
'c,see',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_empty_args(self, context):
|
||||||
|
# empty calls are ignored
|
||||||
|
context.write_sync(EMPTY, EMPTY, EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == ()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import Bag, FileReader, FileWriter
|
from bonobo import FileReader, FileWriter
|
||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import EMPTY
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.execution.contexts.node import NodeExecutionContext
|
||||||
from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester
|
from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester
|
||||||
|
|
||||||
@ -30,9 +30,7 @@ def test_file_writer_in_context(tmpdir, lines, output):
|
|||||||
fs, filename, services = txt_tester.get_services_for_writer(tmpdir)
|
fs, filename, services = txt_tester.get_services_for_writer(tmpdir)
|
||||||
|
|
||||||
with NodeExecutionContext(FileWriter(path=filename), services=services) as context:
|
with NodeExecutionContext(FileWriter(path=filename), services=services) as context:
|
||||||
context.write(BEGIN, *map(Bag, lines), END)
|
context.write_sync(*lines)
|
||||||
for _ in range(len(lines)):
|
|
||||||
context.step()
|
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
with fs.open(filename) as fp:
|
||||||
assert fp.read() == output
|
assert fp.read() == output
|
||||||
@ -42,9 +40,9 @@ def test_file_reader(tmpdir):
|
|||||||
fs, filename, services = txt_tester.get_services_for_reader(tmpdir)
|
fs, filename, services = txt_tester.get_services_for_reader(tmpdir)
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(FileReader(path=filename), services=services) as context:
|
with BufferingNodeExecutionContext(FileReader(path=filename), services=services) as context:
|
||||||
context.write_sync(Bag())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
|
||||||
|
|
||||||
|
output = context.get_buffer()
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
assert output[0] == 'Hello'
|
assert output[0] == ('Hello', )
|
||||||
assert output[1] == 'World'
|
assert output[1] == ('World', )
|
||||||
|
|||||||
@ -1,66 +1,300 @@
|
|||||||
|
import json
|
||||||
|
from collections import OrderedDict, namedtuple
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import JsonReader, JsonWriter, settings
|
from bonobo import JsonReader, JsonWriter
|
||||||
from bonobo import LdjsonReader, LdjsonWriter
|
from bonobo import LdjsonReader, LdjsonWriter
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.constants import EMPTY
|
||||||
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext
|
from bonobo.util.testing import WriterTest, ReaderTest, ConfigurableNodeTest
|
||||||
|
|
||||||
json_tester = FilesystemTester('json')
|
FOOBAR = {'foo': 'bar'}
|
||||||
json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]'''
|
OD_ABC = OrderedDict((('a', 'A'), ('b', 'B'), ('c', 'C')))
|
||||||
|
FOOBAZ = {'foo': 'baz'}
|
||||||
|
|
||||||
|
incontext = ConfigurableNodeTest.incontext
|
||||||
|
|
||||||
|
###
|
||||||
|
# Standard JSON Readers / Writers
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
def test_write_json_ioformat_arg0(tmpdir):
|
class Json:
|
||||||
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
|
extension = 'json'
|
||||||
|
ReaderNodeType = JsonReader
|
||||||
with pytest.raises(ValueError):
|
WriterNodeType = JsonWriter
|
||||||
JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
JsonReader(filename, ioformat=settings.IOFORMAT_ARG0),
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('add_kwargs', (
|
class JsonReaderDictsTest(Json, ReaderTest, TestCase):
|
||||||
{},
|
input_data = '[{"foo": "bar"},\n{"baz": "boz"}]'
|
||||||
{
|
|
||||||
'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:
|
@incontext()
|
||||||
context.write_sync({'foo': 'bar'})
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
with fs.open(filename) as fp:
|
assert context.get_buffer() == [
|
||||||
assert fp.read() == '[{"foo": "bar"}]'
|
({
|
||||||
|
"foo": "bar"
|
||||||
|
}, ),
|
||||||
|
({
|
||||||
|
"baz": "boz"
|
||||||
|
}, ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
stream_json_tester = FilesystemTester('json')
|
class JsonReaderListsTest(Json, ReaderTest, TestCase):
|
||||||
stream_json_tester.input_data = '''{"foo": "bar"}\n{"baz": "boz"}'''
|
input_data = '[[1,2,3],\n[4,5,6]]'
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
([1, 2, 3], ),
|
||||||
|
([4, 5, 6], ),
|
||||||
|
]
|
||||||
|
|
||||||
|
@incontext(output_type=tuple)
|
||||||
|
def test_output_type(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
([1, 2, 3], ),
|
||||||
|
([4, 5, 6], ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_read_stream_json(tmpdir):
|
class JsonReaderStringsTest(Json, ReaderTest, TestCase):
|
||||||
fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir)
|
input_data = '[' + ',\n'.join(map(json.dumps, ('foo', 'bar', 'baz'))) + ']'
|
||||||
with BufferingNodeExecutionContext(LdjsonReader(filename), services=services) as context:
|
|
||||||
context.write_sync(tuple())
|
|
||||||
actual = context.get_buffer()
|
|
||||||
|
|
||||||
expected = [{"foo": "bar"}, {"baz": "boz"}]
|
@incontext()
|
||||||
assert expected == actual
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
('foo', ),
|
||||||
|
('bar', ),
|
||||||
|
('baz', ),
|
||||||
|
]
|
||||||
|
|
||||||
|
@incontext(output_type=tuple)
|
||||||
|
def test_output_type(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
('foo', ),
|
||||||
|
('bar', ),
|
||||||
|
('baz', ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_write_stream_json(tmpdir):
|
class JsonWriterTest(Json, WriterTest, TestCase):
|
||||||
fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir)
|
@incontext()
|
||||||
|
def test_fields(self, context):
|
||||||
|
context.set_input_fields(['foo', 'bar'])
|
||||||
|
context.write_sync(('a', 'b'), ('c', 'd'))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(LdjsonWriter(filename), services=services) as context:
|
assert self.readlines() == (
|
||||||
context.write_sync(
|
'[{"foo": "a", "bar": "b"},',
|
||||||
{
|
'{"foo": "c", "bar": "d"}]',
|
||||||
'foo': 'bar'
|
|
||||||
},
|
|
||||||
{'baz': 'boz'},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = '''{"foo": "bar"}\n{"baz": "boz"}\n'''
|
@incontext()
|
||||||
with fs.open(filename) as fin:
|
def test_fields_from_type(self, context):
|
||||||
actual = fin.read()
|
context.set_input_type(namedtuple('Point', 'x y'))
|
||||||
assert expected == actual
|
context.write_sync((1, 2), (3, 4))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'[{"x": 1, "y": 2},',
|
||||||
|
'{"x": 3, "y": 4}]',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args(self, context):
|
||||||
|
# multiple args are iterated onto and flattened in output
|
||||||
|
context.write_sync((FOOBAR, FOOBAR), (OD_ABC, FOOBAR), (FOOBAZ, FOOBAR))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'[{"foo": "bar"},',
|
||||||
|
'{"foo": "bar"},',
|
||||||
|
'{"a": "A", "b": "B", "c": "C"},',
|
||||||
|
'{"foo": "bar"},',
|
||||||
|
'{"foo": "baz"},',
|
||||||
|
'{"foo": "bar"}]',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args_length_mismatch(self, context):
|
||||||
|
# if length of input vary, then we get a TypeError (unrecoverable)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
context.write_sync((FOOBAR, FOOBAR), (OD_ABC))
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_single_arg(self, context):
|
||||||
|
# single args are just dumped, shapes can vary.
|
||||||
|
context.write_sync(FOOBAR, OD_ABC, FOOBAZ)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'[{"foo": "bar"},',
|
||||||
|
'{"a": "A", "b": "B", "c": "C"},',
|
||||||
|
'{"foo": "baz"}]',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_empty_args(self, context):
|
||||||
|
# empty calls are ignored
|
||||||
|
context.write_sync(EMPTY, EMPTY, EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == ('[]', )
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Line Delimiter JSON Readers / Writers
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
class Ldjson:
|
||||||
|
extension = 'ldjson'
|
||||||
|
ReaderNodeType = LdjsonReader
|
||||||
|
WriterNodeType = LdjsonWriter
|
||||||
|
|
||||||
|
|
||||||
|
class LdjsonReaderDictsTest(Ldjson, ReaderTest, TestCase):
|
||||||
|
input_data = '{"foo": "bar"}\n{"baz": "boz"}'
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
({
|
||||||
|
"foo": "bar"
|
||||||
|
}, ),
|
||||||
|
({
|
||||||
|
"baz": "boz"
|
||||||
|
}, ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LdjsonReaderListsTest(Ldjson, ReaderTest, TestCase):
|
||||||
|
input_data = '[1,2,3]\n[4,5,6]'
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
([1, 2, 3], ),
|
||||||
|
([4, 5, 6], ),
|
||||||
|
]
|
||||||
|
|
||||||
|
@incontext(output_type=tuple)
|
||||||
|
def test_output_type(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
([1, 2, 3], ),
|
||||||
|
([4, 5, 6], ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LdjsonReaderStringsTest(Ldjson, ReaderTest, TestCase):
|
||||||
|
input_data = '\n'.join(map(json.dumps, ('foo', 'bar', 'baz')))
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
('foo', ),
|
||||||
|
('bar', ),
|
||||||
|
('baz', ),
|
||||||
|
]
|
||||||
|
|
||||||
|
@incontext(output_type=tuple)
|
||||||
|
def test_output_type(self, context):
|
||||||
|
context.write_sync(EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert context.get_buffer() == [
|
||||||
|
('foo', ),
|
||||||
|
('bar', ),
|
||||||
|
('baz', ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LdjsonWriterTest(Ldjson, WriterTest, TestCase):
|
||||||
|
@incontext()
|
||||||
|
def test_fields(self, context):
|
||||||
|
context.set_input_fields(['foo', 'bar'])
|
||||||
|
context.write_sync(('a', 'b'), ('c', 'd'))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == ('{"foo": "a", "bar": "b"}', '{"foo": "c", "bar": "d"}')
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_fields_from_type(self, context):
|
||||||
|
context.set_input_type(namedtuple('Point', 'x y'))
|
||||||
|
context.write_sync((1, 2), (3, 4))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'{"x": 1, "y": 2}',
|
||||||
|
'{"x": 3, "y": 4}',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args(self, context):
|
||||||
|
# multiple args are iterated onto and flattened in output
|
||||||
|
context.write_sync((FOOBAR, FOOBAR), (OD_ABC, FOOBAR), (FOOBAZ, FOOBAR))
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'{"foo": "bar"}',
|
||||||
|
'{"foo": "bar"}',
|
||||||
|
'{"a": "A", "b": "B", "c": "C"}',
|
||||||
|
'{"foo": "bar"}',
|
||||||
|
'{"foo": "baz"}',
|
||||||
|
'{"foo": "bar"}',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_multiple_args_length_mismatch(self, context):
|
||||||
|
# if length of input vary, then we get a TypeError (unrecoverable)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
context.write_sync((FOOBAR, FOOBAR), (OD_ABC))
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_single_arg(self, context):
|
||||||
|
# single args are just dumped, shapes can vary.
|
||||||
|
context.write_sync(FOOBAR, OD_ABC, FOOBAZ)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == (
|
||||||
|
'{"foo": "bar"}',
|
||||||
|
'{"a": "A", "b": "B", "c": "C"}',
|
||||||
|
'{"foo": "baz"}',
|
||||||
|
)
|
||||||
|
|
||||||
|
@incontext()
|
||||||
|
def test_nofields_empty_args(self, context):
|
||||||
|
# empty calls are ignored
|
||||||
|
context.write_sync(EMPTY, EMPTY, EMPTY)
|
||||||
|
context.stop()
|
||||||
|
|
||||||
|
assert self.readlines() == ()
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import pickle
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bonobo import Bag, PickleReader, PickleWriter
|
from bonobo import PickleReader, PickleWriter
|
||||||
|
from bonobo.constants import EMPTY
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.execution.contexts.node import NodeExecutionContext
|
||||||
from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester
|
from bonobo.util.testing import BufferingNodeExecutionContext, FilesystemTester
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ def test_write_pickled_dict_to_file(tmpdir):
|
|||||||
fs, filename, services = pickle_tester.get_services_for_writer(tmpdir)
|
fs, filename, services = pickle_tester.get_services_for_writer(tmpdir)
|
||||||
|
|
||||||
with NodeExecutionContext(PickleWriter(filename), services=services) as context:
|
with NodeExecutionContext(PickleWriter(filename), services=services) as context:
|
||||||
context.write_sync(Bag(({'foo': 'bar'}, {})), Bag(({'foo': 'baz', 'ignore': 'this'}, {})))
|
context.write_sync({'foo': 'bar'}, {'foo': 'baz', 'ignore': 'this'})
|
||||||
|
|
||||||
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'}
|
||||||
@ -27,17 +28,11 @@ def test_read_pickled_list_from_file(tmpdir):
|
|||||||
fs, filename, services = pickle_tester.get_services_for_reader(tmpdir)
|
fs, filename, services = pickle_tester.get_services_for_reader(tmpdir)
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context:
|
with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context:
|
||||||
context.write_sync(())
|
context.write_sync(EMPTY)
|
||||||
output = context.get_buffer()
|
|
||||||
|
|
||||||
assert len(output) == 2
|
output = context.get_buffer()
|
||||||
assert output[0] == {
|
assert context.get_output_fields() == ('a', 'b', 'c')
|
||||||
'a': 'a foo',
|
assert output == [
|
||||||
'b': 'b foo',
|
('a foo', 'b foo', 'c foo'),
|
||||||
'c': 'c foo',
|
('a bar', 'b bar', 'c bar'),
|
||||||
}
|
]
|
||||||
assert output[1] == {
|
|
||||||
'a': 'a bar',
|
|
||||||
'b': 'b bar',
|
|
||||||
'c': 'c bar',
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,64 +1,79 @@
|
|||||||
from operator import methodcaller
|
from operator import methodcaller
|
||||||
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.config.processors import ContextCurrifier
|
from bonobo.constants import NOT_MODIFIED, EMPTY
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.util import ensure_tuple, ValueHolder
|
||||||
from bonobo.util.testing import BufferingNodeExecutionContext
|
from bonobo.util.testing import BufferingNodeExecutionContext, StaticNodeTest, ConfigurableNodeTest
|
||||||
|
|
||||||
|
|
||||||
def test_count():
|
class CountTest(StaticNodeTest, TestCase):
|
||||||
|
node = bonobo.count
|
||||||
|
|
||||||
|
def test_counter_required(self):
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
bonobo.count()
|
self.call()
|
||||||
|
|
||||||
context = MagicMock()
|
def test_manual_call(self):
|
||||||
|
counter = ValueHolder(0)
|
||||||
|
for i in range(3):
|
||||||
|
self.call(counter)
|
||||||
|
assert counter == 3
|
||||||
|
|
||||||
with ContextCurrifier(bonobo.count).as_contextmanager(context) as stack:
|
def test_execution(self):
|
||||||
for i in range(42):
|
with self.execute() as context:
|
||||||
stack()
|
context.write_sync(*([EMPTY] * 42))
|
||||||
|
assert context.get_buffer() == [(42, )]
|
||||||
assert len(context.method_calls) == 1
|
|
||||||
bag = context.send.call_args[0][0]
|
|
||||||
assert isinstance(bag, bonobo.Bag)
|
|
||||||
assert 0 == len(bag.kwargs)
|
|
||||||
assert 1 == len(bag.args)
|
|
||||||
assert bag.args[0] == 42
|
|
||||||
|
|
||||||
|
|
||||||
def test_identity():
|
class IdentityTest(StaticNodeTest, TestCase):
|
||||||
assert bonobo.identity(42) == 42
|
node = bonobo.identity
|
||||||
|
|
||||||
|
def test_basic_call(self):
|
||||||
|
assert self.call(42) == 42
|
||||||
|
|
||||||
|
def test_execution(self):
|
||||||
|
object_list = [object() for _ in range(42)]
|
||||||
|
with self.execute() as context:
|
||||||
|
context.write_sync(*object_list)
|
||||||
|
assert context.get_buffer() == list(map(ensure_tuple, object_list))
|
||||||
|
|
||||||
|
|
||||||
def test_limit():
|
class LimitTest(ConfigurableNodeTest, TestCase):
|
||||||
context, results = MagicMock(), []
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.NodeType = bonobo.Limit
|
||||||
|
|
||||||
with ContextCurrifier(bonobo.Limit(2)).as_contextmanager(context) as stack:
|
def test_execution_default(self):
|
||||||
for i in range(42):
|
object_list = [object() for _ in range(42)]
|
||||||
results += list(stack())
|
with self.execute() as context:
|
||||||
|
context.write_sync(*object_list)
|
||||||
|
|
||||||
assert results == [NOT_MODIFIED] * 2
|
assert context.get_buffer() == list(map(ensure_tuple, object_list[:10]))
|
||||||
|
|
||||||
|
def test_execution_custom(self):
|
||||||
|
object_list = [object() for _ in range(42)]
|
||||||
|
with self.execute(21) as context:
|
||||||
|
context.write_sync(*object_list)
|
||||||
|
|
||||||
def test_limit_not_there():
|
assert context.get_buffer() == list(map(ensure_tuple, object_list[:21]))
|
||||||
context, results = MagicMock(), []
|
|
||||||
|
|
||||||
with ContextCurrifier(bonobo.Limit(42)).as_contextmanager(context) as stack:
|
def test_manual(self):
|
||||||
for i in range(10):
|
limit = self.NodeType(5)
|
||||||
results += list(stack())
|
buffer = []
|
||||||
|
for x in range(10):
|
||||||
|
buffer += list(limit(x))
|
||||||
|
assert len(buffer) == 5
|
||||||
|
|
||||||
assert results == [NOT_MODIFIED] * 10
|
def test_underflow(self):
|
||||||
|
limit = self.NodeType(10)
|
||||||
|
buffer = []
|
||||||
def test_limit_default():
|
for x in range(5):
|
||||||
context, results = MagicMock(), []
|
buffer += list(limit(x))
|
||||||
|
assert len(buffer) == 5
|
||||||
with ContextCurrifier(bonobo.Limit()).as_contextmanager(context) as stack:
|
|
||||||
for i in range(20):
|
|
||||||
results += list(stack())
|
|
||||||
|
|
||||||
assert results == [NOT_MODIFIED] * 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_tee():
|
def test_tee():
|
||||||
@ -76,36 +91,28 @@ def test_noop():
|
|||||||
assert bonobo.noop(1, 2, 3, 4, foo='bar') == NOT_MODIFIED
|
assert bonobo.noop(1, 2, 3, 4, foo='bar') == NOT_MODIFIED
|
||||||
|
|
||||||
|
|
||||||
def test_update():
|
|
||||||
with BufferingNodeExecutionContext(bonobo.Update('a', k=True)) as context:
|
|
||||||
context.write_sync('a', ('a', {'b': 1}), ('b', {'k': False}))
|
|
||||||
assert context.get_buffer() == [
|
|
||||||
bonobo.Bag('a', 'a', k=True),
|
|
||||||
bonobo.Bag('a', 'a', b=1, k=True),
|
|
||||||
bonobo.Bag('b', 'a', k=True),
|
|
||||||
]
|
|
||||||
assert context.name == "Update('a', k=True)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_fixedwindow():
|
def test_fixedwindow():
|
||||||
with BufferingNodeExecutionContext(bonobo.FixedWindow(2)) as context:
|
with BufferingNodeExecutionContext(bonobo.FixedWindow(2)) as context:
|
||||||
context.write_sync(*range(10))
|
context.write_sync(*range(10))
|
||||||
assert context.get_buffer() == [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
|
assert context.get_buffer() == [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(bonobo.FixedWindow(2)) as context:
|
with BufferingNodeExecutionContext(bonobo.FixedWindow(2)) as context:
|
||||||
context.write_sync(*range(9))
|
context.write_sync(*range(9))
|
||||||
assert context.get_buffer() == [[0, 1], [2, 3], [4, 5], [6, 7], [8]]
|
assert context.get_buffer() == [(0, 1), (2, 3), (4, 5), (6, 7), (
|
||||||
|
8,
|
||||||
|
None,
|
||||||
|
)]
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(bonobo.FixedWindow(1)) as context:
|
with BufferingNodeExecutionContext(bonobo.FixedWindow(1)) as context:
|
||||||
context.write_sync(*range(3))
|
context.write_sync(*range(3))
|
||||||
assert context.get_buffer() == [[0], [1], [2]]
|
assert context.get_buffer() == [(0, ), (1, ), (2, )]
|
||||||
|
|
||||||
|
|
||||||
def test_methodcaller():
|
def test_methodcaller():
|
||||||
with BufferingNodeExecutionContext(methodcaller('swapcase')) as context:
|
with BufferingNodeExecutionContext(methodcaller('swapcase')) as context:
|
||||||
context.write_sync('aaa', 'bBb', 'CcC')
|
context.write_sync('aaa', 'bBb', 'CcC')
|
||||||
assert context.get_buffer() == ['AAA', 'BbB', 'cCc']
|
assert context.get_buffer() == list(map(ensure_tuple, ['AAA', 'BbB', 'cCc']))
|
||||||
|
|
||||||
with BufferingNodeExecutionContext(methodcaller('zfill', 5)) as context:
|
with BufferingNodeExecutionContext(methodcaller('zfill', 5)) as context:
|
||||||
context.write_sync('a', 'bb', 'ccc')
|
context.write_sync('a', 'bb', 'ccc')
|
||||||
assert context.get_buffer() == ['0000a', '000bb', '00ccc']
|
assert context.get_buffer() == list(map(ensure_tuple, ['0000a', '000bb', '00ccc']))
|
||||||
|
|||||||
@ -1,170 +0,0 @@
|
|||||||
import pickle
|
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bonobo import Bag
|
|
||||||
from bonobo.constants import INHERIT_INPUT, BEGIN
|
|
||||||
from bonobo.structs import Token
|
|
||||||
|
|
||||||
args = (
|
|
||||||
'foo',
|
|
||||||
'bar',
|
|
||||||
)
|
|
||||||
kwargs = dict(acme='corp')
|
|
||||||
|
|
||||||
|
|
||||||
def test_basic():
|
|
||||||
my_callable1 = Mock()
|
|
||||||
my_callable2 = Mock()
|
|
||||||
bag = Bag(*args, **kwargs)
|
|
||||||
|
|
||||||
assert not my_callable1.called
|
|
||||||
result1 = bag.apply(my_callable1)
|
|
||||||
assert my_callable1.called and result1 is my_callable1.return_value
|
|
||||||
|
|
||||||
assert not my_callable2.called
|
|
||||||
result2 = bag.apply(my_callable2)
|
|
||||||
assert my_callable2.called and result2 is my_callable2.return_value
|
|
||||||
|
|
||||||
assert result1 is not result2
|
|
||||||
|
|
||||||
my_callable1.assert_called_once_with(*args, **kwargs)
|
|
||||||
my_callable2.assert_called_once_with(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_empty():
|
|
||||||
a, b = Bag(), Bag()
|
|
||||||
assert a == b
|
|
||||||
assert a.args is ()
|
|
||||||
assert a.kwargs == {}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(('arg_in', 'arg_out'), (
|
|
||||||
((), ()),
|
|
||||||
({}, ()),
|
|
||||||
(('a', 'b', 'c'), None),
|
|
||||||
))
|
|
||||||
def test_constructor_shorthand(arg_in, arg_out):
|
|
||||||
if arg_out is None:
|
|
||||||
arg_out = arg_in
|
|
||||||
assert Bag(arg_in) == arg_out
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_kwargs_only():
|
|
||||||
assert Bag(foo='bar') == {'foo': 'bar'}
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_identity():
|
|
||||||
assert Bag(BEGIN) is BEGIN
|
|
||||||
|
|
||||||
|
|
||||||
def test_inherit():
|
|
||||||
bag = Bag('a', a=1)
|
|
||||||
bag2 = Bag.inherit('b', b=2, _parent=bag)
|
|
||||||
bag3 = bag.extend('c', c=3)
|
|
||||||
bag4 = Bag('d', d=4)
|
|
||||||
|
|
||||||
assert bag.args == ('a', )
|
|
||||||
assert bag.kwargs == {'a': 1}
|
|
||||||
assert bag.flags is ()
|
|
||||||
|
|
||||||
assert bag2.args == (
|
|
||||||
'a',
|
|
||||||
'b',
|
|
||||||
)
|
|
||||||
assert bag2.kwargs == {'a': 1, 'b': 2}
|
|
||||||
assert INHERIT_INPUT in bag2.flags
|
|
||||||
|
|
||||||
assert bag3.args == (
|
|
||||||
'a',
|
|
||||||
'c',
|
|
||||||
)
|
|
||||||
assert bag3.kwargs == {'a': 1, 'c': 3}
|
|
||||||
assert bag3.flags is ()
|
|
||||||
|
|
||||||
assert bag4.args == ('d', )
|
|
||||||
assert bag4.kwargs == {'d': 4}
|
|
||||||
assert bag4.flags is ()
|
|
||||||
|
|
||||||
bag4.set_parent(bag)
|
|
||||||
assert bag4.args == (
|
|
||||||
'a',
|
|
||||||
'd',
|
|
||||||
)
|
|
||||||
assert bag4.kwargs == {'a': 1, 'd': 4}
|
|
||||||
assert bag4.flags is ()
|
|
||||||
|
|
||||||
bag4.set_parent(bag3)
|
|
||||||
assert bag4.args == (
|
|
||||||
'a',
|
|
||||||
'c',
|
|
||||||
'd',
|
|
||||||
)
|
|
||||||
assert bag4.kwargs == {'a': 1, 'c': 3, 'd': 4}
|
|
||||||
assert bag4.flags is ()
|
|
||||||
|
|
||||||
|
|
||||||
def test_pickle():
|
|
||||||
bag1 = Bag('a', a=1)
|
|
||||||
bag2 = Bag.inherit('b', b=2, _parent=bag1)
|
|
||||||
bag3 = bag1.extend('c', c=3)
|
|
||||||
bag4 = Bag('d', d=4)
|
|
||||||
|
|
||||||
# XXX todo this probably won't work with inheriting bags if parent is not there anymore? maybe that's not true
|
|
||||||
# because the parent may be in the serialization output but we need to verify this assertion.
|
|
||||||
|
|
||||||
for bag in bag1, bag2, bag3, bag4:
|
|
||||||
pickled = pickle.dumps(bag)
|
|
||||||
unpickled = pickle.loads(pickled)
|
|
||||||
assert unpickled == bag
|
|
||||||
|
|
||||||
|
|
||||||
def test_eq_operator_bag():
|
|
||||||
assert Bag('foo') == Bag('foo')
|
|
||||||
assert Bag('foo') != Bag('bar')
|
|
||||||
assert Bag('foo') is not Bag('foo')
|
|
||||||
assert Bag('foo') != Token('foo')
|
|
||||||
assert Token('foo') != Bag('foo')
|
|
||||||
|
|
||||||
|
|
||||||
def test_eq_operator_tuple_mixed():
|
|
||||||
assert Bag('foo', bar='baz') == ('foo', {'bar': 'baz'})
|
|
||||||
assert Bag('foo') == ('foo', {})
|
|
||||||
assert Bag() == ({}, )
|
|
||||||
|
|
||||||
|
|
||||||
def test_eq_operator_tuple_not_mixed():
|
|
||||||
assert Bag('foo', 'bar') == ('foo', 'bar')
|
|
||||||
assert Bag('foo') == ('foo', )
|
|
||||||
assert Bag() == ()
|
|
||||||
|
|
||||||
|
|
||||||
def test_eq_operator_dict():
|
|
||||||
assert Bag(foo='bar') == {'foo': 'bar'}
|
|
||||||
assert Bag(
|
|
||||||
foo='bar', corp='acme'
|
|
||||||
) == {
|
|
||||||
'foo': 'bar',
|
|
||||||
'corp': 'acme',
|
|
||||||
}
|
|
||||||
assert Bag(
|
|
||||||
foo='bar', corp='acme'
|
|
||||||
) == {
|
|
||||||
'corp': 'acme',
|
|
||||||
'foo': 'bar',
|
|
||||||
}
|
|
||||||
assert Bag() == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_repr():
|
|
||||||
bag = Bag('a', a=1)
|
|
||||||
assert repr(bag) == "Bag('a', a=1)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_iterator():
|
|
||||||
bag = Bag()
|
|
||||||
assert list(bag.apply([1, 2, 3])) == [1, 2, 3]
|
|
||||||
assert list(bag.apply((1, 2, 3))) == [1, 2, 3]
|
|
||||||
assert list(bag.apply(range(5))) == [0, 1, 2, 3, 4]
|
|
||||||
assert list(bag.apply('azerty')) == ['a', 'z', 'e', 'r', 't', 'y']
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from bonobo.structs import Token
|
from bonobo.constants import Token
|
||||||
|
|
||||||
|
|
||||||
def test_token_repr():
|
def test_token_repr():
|
||||||
|
|||||||
@ -1,29 +1,28 @@
|
|||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import use_context_processor
|
||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import BEGIN, END
|
||||||
from bonobo.execution.contexts.graph import GraphExecutionContext
|
from bonobo.execution.contexts.graph import GraphExecutionContext
|
||||||
from bonobo.execution.strategies import NaiveStrategy
|
from bonobo.execution.strategies import NaiveStrategy
|
||||||
from bonobo.structs import Bag, Graph
|
from bonobo.structs import Graph
|
||||||
|
|
||||||
|
|
||||||
def generate_integers():
|
def generate_integers():
|
||||||
yield from range(10)
|
yield from range(10)
|
||||||
|
|
||||||
|
|
||||||
def square(i: int) -> int:
|
def square(i):
|
||||||
return i**2
|
return i**2
|
||||||
|
|
||||||
|
|
||||||
def push_result(results, i: int):
|
|
||||||
results.append(i)
|
|
||||||
|
|
||||||
|
|
||||||
@ContextProcessor.decorate(push_result)
|
|
||||||
def results(f, context):
|
def results(f, context):
|
||||||
results = []
|
results = yield list()
|
||||||
yield results
|
|
||||||
context.parent.results = results
|
context.parent.results = results
|
||||||
|
|
||||||
|
|
||||||
|
@use_context_processor(results)
|
||||||
|
def push_result(results, i):
|
||||||
|
results.append(i)
|
||||||
|
|
||||||
|
|
||||||
chain = (generate_integers, square, push_result)
|
chain = (generate_integers, square, push_result)
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ def test_simple_execution_context():
|
|||||||
assert not context.started
|
assert not context.started
|
||||||
assert not context.stopped
|
assert not context.stopped
|
||||||
|
|
||||||
context.write(BEGIN, Bag(), END)
|
context.write(BEGIN, (), END)
|
||||||
|
|
||||||
assert not context.alive
|
assert not context.alive
|
||||||
assert not context.started
|
assert not context.started
|
||||||
|
|||||||
278
tests/util/test_bags.py
Normal file
278
tests/util/test_bags.py
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
"""Those tests are mostly a copy paste of cpython unit tests for namedtuple, with a few differences to reflect the
|
||||||
|
implementation details that differs. It ensures that we caught the same edge cases as they did."""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import copy
|
||||||
|
import pickle
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from collections import OrderedDict
|
||||||
|
from random import choice
|
||||||
|
|
||||||
|
from bonobo.util.bags import BagType
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
### Named Tuples
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
TBag = BagType('TBag', ('x', 'y', 'z')) # type used for pickle tests
|
||||||
|
|
||||||
|
|
||||||
|
class TestBagType(unittest.TestCase):
|
||||||
|
def _create(self, *fields, typename='abc'):
|
||||||
|
bt = BagType(typename, fields)
|
||||||
|
assert bt._fields == fields
|
||||||
|
assert len(bt._fields) == len(bt._attrs)
|
||||||
|
return bt
|
||||||
|
|
||||||
|
def test_factory(self):
|
||||||
|
Point = BagType('Point', ('x', 'y'))
|
||||||
|
self.assertEqual(Point.__name__, 'Point')
|
||||||
|
self.assertEqual(Point.__slots__, ())
|
||||||
|
self.assertEqual(Point.__module__, __name__)
|
||||||
|
self.assertEqual(Point.__getitem__, tuple.__getitem__)
|
||||||
|
assert Point._fields == ('x', 'y')
|
||||||
|
assert Point._attrs == ('x', 'y')
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, BagType, 'abc%', ('efg', 'ghi')) # type has non-alpha char
|
||||||
|
self.assertRaises(ValueError, BagType, 'class', ('efg', 'ghi')) # type has keyword
|
||||||
|
self.assertRaises(ValueError, BagType, '9abc', ('efg', 'ghi')) # type starts with digit
|
||||||
|
|
||||||
|
assert self._create('efg', 'g%hi')._attrs == ('efg', 'g_hi')
|
||||||
|
assert self._create('abc', 'class')._attrs == ('abc', '_class')
|
||||||
|
assert self._create('8efg', '9ghi')._attrs == ('_8efg', '_9ghi')
|
||||||
|
assert self._create('_efg', 'ghi')._attrs == ('_efg', 'ghi')
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, BagType, 'abc', ('efg', 'efg', 'ghi')) # duplicate field
|
||||||
|
|
||||||
|
self._create('x1', 'y2', typename='Point0') # Verify that numbers are allowed in names
|
||||||
|
self._create('a', 'b', 'c', typename='_') # Test leading underscores in a typename
|
||||||
|
|
||||||
|
bt = self._create('a!', 'a?')
|
||||||
|
assert bt._attrs == ('a0', 'a1')
|
||||||
|
x = bt('foo', 'bar')
|
||||||
|
assert x.get('a!') == 'foo'
|
||||||
|
assert x.a0 == 'foo'
|
||||||
|
assert x.get('a?') == 'bar'
|
||||||
|
assert x.a1 == 'bar'
|
||||||
|
|
||||||
|
# check unicode output
|
||||||
|
bt = self._create('the', 'quick', 'brown', 'fox')
|
||||||
|
assert "u'" not in repr(bt._fields)
|
||||||
|
|
||||||
|
self.assertRaises(TypeError, Point._make, [11]) # catch too few args
|
||||||
|
self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above")
|
||||||
|
def test_factory_doc_attr(self):
|
||||||
|
Point = BagType('Point', ('x', 'y'))
|
||||||
|
self.assertEqual(Point.__doc__, 'Point(x, y)')
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above")
|
||||||
|
def test_doc_writable(self):
|
||||||
|
Point = BagType('Point', ('x', 'y'))
|
||||||
|
self.assertEqual(Point.x.__doc__, "Alias for 'x'")
|
||||||
|
Point.x.__doc__ = 'docstring for Point.x'
|
||||||
|
self.assertEqual(Point.x.__doc__, 'docstring for Point.x')
|
||||||
|
|
||||||
|
def test_name_fixer(self):
|
||||||
|
for spec, renamed in [
|
||||||
|
[('efg', 'g%hi'), ('efg', 'g_hi')], # field with non-alpha char
|
||||||
|
[('abc', 'class'), ('abc', '_class')], # field has keyword
|
||||||
|
[('8efg', '9ghi'), ('_8efg', '_9ghi')], # field starts with digit
|
||||||
|
[('abc', '_efg'), ('abc', '_efg')], # field with leading underscore
|
||||||
|
[('abc', '', 'x'), ('abc', '_0', 'x')], # fieldname is a space
|
||||||
|
[('&', '¨', '*'), ('_0', '_1', '_2')], # Duplicate attrs, in theory
|
||||||
|
]:
|
||||||
|
assert self._create(*spec)._attrs == renamed
|
||||||
|
|
||||||
|
def test_module_parameter(self):
|
||||||
|
NT = BagType('NT', ['x', 'y'], module=collections)
|
||||||
|
self.assertEqual(NT.__module__, collections)
|
||||||
|
|
||||||
|
def test_instance(self):
|
||||||
|
Point = self._create('x', 'y', typename='Point')
|
||||||
|
p = Point(11, 22)
|
||||||
|
self.assertEqual(p, Point(x=11, y=22))
|
||||||
|
self.assertEqual(p, Point(11, y=22))
|
||||||
|
self.assertEqual(p, Point(y=22, x=11))
|
||||||
|
self.assertEqual(p, Point(*(11, 22)))
|
||||||
|
self.assertEqual(p, Point(**dict(x=11, y=22)))
|
||||||
|
self.assertRaises(TypeError, Point, 1) # too few args
|
||||||
|
self.assertRaises(TypeError, Point, 1, 2, 3) # too many args
|
||||||
|
self.assertRaises(TypeError, eval, 'Point(XXX=1, y=2)', locals()) # wrong keyword argument
|
||||||
|
self.assertRaises(TypeError, eval, 'Point(x=1)', locals()) # missing keyword argument
|
||||||
|
self.assertEqual(repr(p), 'Point(x=11, y=22)')
|
||||||
|
self.assertNotIn('__weakref__', dir(p))
|
||||||
|
self.assertEqual(p, Point._make([11, 22])) # test _make classmethod
|
||||||
|
self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute
|
||||||
|
self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method
|
||||||
|
self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method
|
||||||
|
|
||||||
|
try:
|
||||||
|
p._replace(x=1, error=2)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._fail('Did not detect an incorrect fieldname')
|
||||||
|
|
||||||
|
p = Point(x=11, y=22)
|
||||||
|
self.assertEqual(repr(p), 'Point(x=11, y=22)')
|
||||||
|
|
||||||
|
def test_tupleness(self):
|
||||||
|
Point = BagType('Point', ('x', 'y'))
|
||||||
|
p = Point(11, 22)
|
||||||
|
|
||||||
|
self.assertIsInstance(p, tuple)
|
||||||
|
self.assertEqual(p, (11, 22)) # matches a real tuple
|
||||||
|
self.assertEqual(tuple(p), (11, 22)) # coercable to a real tuple
|
||||||
|
self.assertEqual(list(p), [11, 22]) # coercable to a list
|
||||||
|
self.assertEqual(max(p), 22) # iterable
|
||||||
|
self.assertEqual(max(*p), 22) # star-able
|
||||||
|
x, y = p
|
||||||
|
self.assertEqual(p, (x, y)) # unpacks like a tuple
|
||||||
|
self.assertEqual((p[0], p[1]), (11, 22)) # indexable like a tuple
|
||||||
|
self.assertRaises(IndexError, p.__getitem__, 3)
|
||||||
|
|
||||||
|
self.assertEqual(p.x, x)
|
||||||
|
self.assertEqual(p.y, y)
|
||||||
|
self.assertRaises(AttributeError, eval, 'p.z', locals())
|
||||||
|
|
||||||
|
def test_odd_sizes(self):
|
||||||
|
Zero = BagType('Zero', ())
|
||||||
|
self.assertEqual(Zero(), ())
|
||||||
|
self.assertEqual(Zero._make([]), ())
|
||||||
|
self.assertEqual(repr(Zero()), 'Zero()')
|
||||||
|
self.assertEqual(Zero()._asdict(), {})
|
||||||
|
self.assertEqual(Zero()._fields, ())
|
||||||
|
|
||||||
|
Dot = BagType('Dot', ('d', ))
|
||||||
|
self.assertEqual(Dot(1), (1, ))
|
||||||
|
self.assertEqual(Dot._make([1]), (1, ))
|
||||||
|
self.assertEqual(Dot(1).d, 1)
|
||||||
|
self.assertEqual(repr(Dot(1)), 'Dot(d=1)')
|
||||||
|
self.assertEqual(Dot(1)._asdict(), {'d': 1})
|
||||||
|
self.assertEqual(Dot(1)._replace(d=999), (999, ))
|
||||||
|
self.assertEqual(Dot(1)._fields, ('d', ))
|
||||||
|
|
||||||
|
n = 5000 if sys.version_info >= (3, 7) else 254
|
||||||
|
names = list(set(''.join([choice(string.ascii_letters) for j in range(10)]) for i in range(n)))
|
||||||
|
n = len(names)
|
||||||
|
Big = BagType('Big', names)
|
||||||
|
b = Big(*range(n))
|
||||||
|
self.assertEqual(b, tuple(range(n)))
|
||||||
|
self.assertEqual(Big._make(range(n)), tuple(range(n)))
|
||||||
|
for pos, name in enumerate(names):
|
||||||
|
self.assertEqual(getattr(b, name), pos)
|
||||||
|
repr(b) # make sure repr() doesn't blow-up
|
||||||
|
d = b._asdict()
|
||||||
|
d_expected = dict(zip(names, range(n)))
|
||||||
|
self.assertEqual(d, d_expected)
|
||||||
|
b2 = b._replace(**dict([(names[1], 999), (names[-5], 42)]))
|
||||||
|
b2_expected = list(range(n))
|
||||||
|
b2_expected[1] = 999
|
||||||
|
b2_expected[-5] = 42
|
||||||
|
self.assertEqual(b2, tuple(b2_expected))
|
||||||
|
self.assertEqual(b._fields, tuple(names))
|
||||||
|
|
||||||
|
def test_pickle(self):
|
||||||
|
p = TBag(x=10, y=20, z=30)
|
||||||
|
for module in (pickle, ):
|
||||||
|
loads = getattr(module, 'loads')
|
||||||
|
dumps = getattr(module, 'dumps')
|
||||||
|
for protocol in range(-1, module.HIGHEST_PROTOCOL + 1):
|
||||||
|
q = loads(dumps(p, protocol))
|
||||||
|
self.assertEqual(p, q)
|
||||||
|
self.assertEqual(p._fields, q._fields)
|
||||||
|
self.assertNotIn(b'OrderedDict', dumps(p, protocol))
|
||||||
|
|
||||||
|
def test_copy(self):
|
||||||
|
p = TBag(x=10, y=20, z=30)
|
||||||
|
for copier in copy.copy, copy.deepcopy:
|
||||||
|
q = copier(p)
|
||||||
|
self.assertEqual(p, q)
|
||||||
|
self.assertEqual(p._fields, q._fields)
|
||||||
|
|
||||||
|
def test_name_conflicts(self):
|
||||||
|
# Some names like "self", "cls", "tuple", "itemgetter", and "property"
|
||||||
|
# failed when used as field names. Test to make sure these now work.
|
||||||
|
T = BagType('T', ('itemgetter', 'property', 'self', 'cls', 'tuple'))
|
||||||
|
t = T(1, 2, 3, 4, 5)
|
||||||
|
self.assertEqual(t, (1, 2, 3, 4, 5))
|
||||||
|
newt = t._replace(itemgetter=10, property=20, self=30, cls=40, tuple=50)
|
||||||
|
self.assertEqual(newt, (10, 20, 30, 40, 50))
|
||||||
|
|
||||||
|
# Broader test of all interesting names taken from the code, old
|
||||||
|
# template, and an example
|
||||||
|
words = {
|
||||||
|
'Alias', 'At', 'AttributeError', 'Build', 'Bypass', 'Create', 'Encountered', 'Expected', 'Field', 'For',
|
||||||
|
'Got', 'Helper', 'IronPython', 'Jython', 'KeyError', 'Make', 'Modify', 'Note', 'OrderedDict', 'Point',
|
||||||
|
'Return', 'Returns', 'Type', 'TypeError', 'Used', 'Validate', 'ValueError', 'Variables', 'a', 'accessible',
|
||||||
|
'add', 'added', 'all', 'also', 'an', 'arg_list', 'args', 'arguments', 'automatically', 'be', 'build',
|
||||||
|
'builtins', 'but', 'by', 'cannot', 'class_namespace', 'classmethod', 'cls', 'collections', 'convert',
|
||||||
|
'copy', 'created', 'creation', 'd', 'debugging', 'defined', 'dict', 'dictionary', 'doc', 'docstring',
|
||||||
|
'docstrings', 'duplicate', 'effect', 'either', 'enumerate', 'environments', 'error', 'example', 'exec', 'f',
|
||||||
|
'f_globals', 'field', 'field_names', 'fields', 'formatted', 'frame', 'function', 'functions', 'generate',
|
||||||
|
'getter', 'got', 'greater', 'has', 'help', 'identifiers', 'indexable', 'instance', 'instantiate',
|
||||||
|
'interning', 'introspection', 'isidentifier', 'isinstance', 'itemgetter', 'iterable', 'join', 'keyword',
|
||||||
|
'keywords', 'kwds', 'len', 'like', 'list', 'map', 'maps', 'message', 'metadata', 'method', 'methods',
|
||||||
|
'module', 'module_name', 'must', 'name', 'named', 'namedtuple', 'namedtuple_', 'names', 'namespace',
|
||||||
|
'needs', 'new', 'nicely', 'num_fields', 'number', 'object', 'of', 'operator', 'option', 'p', 'particular',
|
||||||
|
'pickle', 'pickling', 'plain', 'pop', 'positional', 'property', 'r', 'regular', 'rename', 'replace',
|
||||||
|
'replacing', 'repr', 'repr_fmt', 'representation', 'result', 'reuse_itemgetter', 's', 'seen', 'sequence',
|
||||||
|
'set', 'side', 'specified', 'split', 'start', 'startswith', 'step', 'str', 'string', 'strings', 'subclass',
|
||||||
|
'sys', 'targets', 'than', 'the', 'their', 'this', 'to', 'tuple_new', 'type', 'typename', 'underscore',
|
||||||
|
'unexpected', 'unpack', 'up', 'use', 'used', 'user', 'valid', 'values', 'variable', 'verbose', 'where',
|
||||||
|
'which', 'work', 'x', 'y', 'z', 'zip'
|
||||||
|
}
|
||||||
|
sorted_words = tuple(sorted(words))
|
||||||
|
T = BagType('T', sorted_words)
|
||||||
|
# test __new__
|
||||||
|
values = tuple(range(len(words)))
|
||||||
|
t = T(*values)
|
||||||
|
self.assertEqual(t, values)
|
||||||
|
t = T(**dict(zip(T._attrs, values)))
|
||||||
|
self.assertEqual(t, values)
|
||||||
|
# test _make
|
||||||
|
t = T._make(values)
|
||||||
|
self.assertEqual(t, values)
|
||||||
|
# exercise __repr__
|
||||||
|
repr(t)
|
||||||
|
# test _asdict
|
||||||
|
self.assertEqual(t._asdict(), dict(zip(T._fields, values)))
|
||||||
|
# test _replace
|
||||||
|
t = T._make(values)
|
||||||
|
newvalues = tuple(v * 10 for v in values)
|
||||||
|
newt = t._replace(**dict(zip(T._fields, newvalues)))
|
||||||
|
self.assertEqual(newt, newvalues)
|
||||||
|
# test _fields
|
||||||
|
self.assertEqual(T._attrs, sorted_words)
|
||||||
|
# test __getnewargs__
|
||||||
|
self.assertEqual(t.__getnewargs__(), values)
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
A = BagType('A', ('x', ))
|
||||||
|
self.assertEqual(repr(A(1)), 'A(x=1)')
|
||||||
|
|
||||||
|
# repr should show the name of the subclass
|
||||||
|
class B(A):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertEqual(repr(B(1)), 'B(x=1)')
|
||||||
|
|
||||||
|
def test_namedtuple_subclass_issue_24931(self):
|
||||||
|
class Point(BagType('_Point', ['x', 'y'])):
|
||||||
|
pass
|
||||||
|
|
||||||
|
a = Point(3, 4)
|
||||||
|
self.assertEqual(a._asdict(), OrderedDict([('x', 3), ('y', 4)]))
|
||||||
|
|
||||||
|
a.w = 5
|
||||||
|
self.assertEqual(a.__dict__, {'w': 5})
|
||||||
|
|
||||||
|
def test_annoying_attribute_names(self):
|
||||||
|
self._create(
|
||||||
|
'__slots__', '__getattr__', '_attrs', '_fields', '__new__', '__getnewargs__', '__repr__', '_make', 'get',
|
||||||
|
'_replace', '_asdict', '_cls', 'self', 'tuple'
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user