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:
Romain Dorgueil
2017-11-27 00:04:51 +01:00
parent 52ea29afcb
commit 5e0b6567cd
96 changed files with 2958 additions and 1870 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View File

@ -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,
) )

View File

@ -1 +1 @@
__version__ = '0.6.dev0' __version__ = '0.6.0a0'

View File

@ -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')

View File

@ -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',
] ]

View File

@ -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.')

View 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

View File

@ -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

View File

@ -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)

View File

@ -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__

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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 (),
}

View 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())

View File

@ -1,7 +0,0 @@
from os.path import dirname
import bonobo
def get_services():
return {'fs': bonobo.open_fs(dirname(__file__))}

View File

@ -0,0 +1 @@
"['name', 'address', 'zipcode', 'city']"
1 ['name', 'address', 'zipcode', 'city']

View File

@ -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"}

View 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"}

View File

@ -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__))

View 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 dAcclimatation, 75016 Paris, France La chaumière gourmande,"Route de la Muette à Neuilly
Le Brio, 216, rue Marcadet, 75018 Paris, France Club hippique du Jardin dAcclimatation",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

View File

@ -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(),
}

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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()
)

View File

@ -1,5 +0,0 @@
from bonobo import get_examples_path, open_fs
def get_services():
return {'fs': open_fs(get_examples_path())}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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]
) )

View File

@ -1,7 +0,0 @@
from . import bags, dicts, strings
__all__ = [
'bags',
'dicts',
'strings',
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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 = []

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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.')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,6 @@
from bonobo.plugins import Plugin
from raven import Client
class SentryPlugin(Plugin):
pass

View File

@ -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',
] ]

View File

@ -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

View File

@ -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.

View File

@ -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__)

View File

@ -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
View 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

View File

@ -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):

View File

@ -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)

View File

@ -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()))

View File

@ -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'])

View File

@ -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

View File

@ -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():

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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': [

View File

@ -1,3 +1,5 @@
import pprint
import pytest import pytest
from bonobo.config.configurables import Configurable from bonobo.config.configurables import Configurable

View File

@ -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)

View File

@ -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)

View File

@ -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']

View File

@ -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, {}])

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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() == ()

View File

@ -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', )

View File

@ -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() == ()

View File

@ -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',
}

View File

@ -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']))

View File

@ -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']

View File

@ -1,4 +1,4 @@
from bonobo.structs import Token from bonobo.constants import Token
def test_token_repr(): def test_token_repr():

View File

@ -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
View 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'
)