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]
based_on_style = pep8
column_limit = 120
allow_multiline_lambdas = false
allow_multiline_dictionary_keys = false
coalesce_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.
PACKAGE ?= bonobo

View File

@ -43,9 +43,10 @@ python.add_requirements(
'fs >=2.0,<2.1',
'graphviz >=0.8,<0.9',
'jinja2 >=2.9,<3',
'mondrian >=0.4,<0.5',
'mondrian >=0.5,<0.6',
'packaging >=16,<17',
'psutil >=5.4,<6',
'python-slugify >=1.2,<1.3',
'requests >=2,<3',
'stevedore >=1.27,<1.28',
'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,
PrettyPrinter,
RateLimited,
SetFields,
Tee,
Update,
arg0_to_kwargs,
count,
identity,
kwargs_to_arg0,
noop,
)
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.environ import parse_args, get_argument_parser
@ -133,7 +131,7 @@ def inspect(graph, *, plugins=None, services=None, strategy=None, format):
# data structures
register_api_group(Bag, ErrorBag, Graph, Token)
register_api_group(Graph)
# execution strategies
register_api(create_strategy)
@ -181,12 +179,10 @@ register_api_group(
PickleWriter,
PrettyPrinter,
RateLimited,
SetFields,
Tee,
Update,
arg0_to_kwargs,
count,
identity,
kwargs_to_arg0,
noop,
)

View File

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

View File

@ -49,15 +49,15 @@ class ConvertCommand(BaseCommand):
)
def handle(
self,
input_filename,
output_filename,
reader=None,
reader_option=None,
writer=None,
writer_option=None,
option=None,
transformation=None
self,
input_filename,
output_filename,
reader=None,
reader_option=None,
writer=None,
writer_option=None,
option=None,
transformation=None
):
reader_factory = default_registry.get_reader_factory_for(input_filename, format=reader)
reader_options = _resolve_options((option or []) + (reader_option or []))

View File

@ -1,23 +1,29 @@
from bonobo.commands import BaseCommand
def get_versions(*, all=False, quiet=None):
import bonobo
from bonobo.util.pkgs import bonobo_packages
yield _format_version(bonobo, quiet=quiet)
if all:
for name in sorted(bonobo_packages):
if name != 'bonobo':
try:
mod = __import__(name.replace('-', '_'))
try:
yield _format_version(mod, name=name, quiet=quiet)
except Exception as exc:
yield '{} ({})'.format(name, exc)
except ImportError as exc:
yield '{} is not importable ({}).'.format(name, exc)
class VersionCommand(BaseCommand):
def handle(self, *, all=False, quiet=False):
import bonobo
from bonobo.util.pkgs import bonobo_packages
print(_format_version(bonobo, quiet=quiet))
if all:
for name in sorted(bonobo_packages):
if name != 'bonobo':
try:
mod = __import__(name.replace('-', '_'))
try:
print(_format_version(mod, name=name, quiet=quiet))
except Exception as exc:
print('{} ({})'.format(name, exc))
except ImportError as exc:
print('{} is not importable ({}).'.format(name, exc))
for line in get_versions(all=all, quiet=quiet):
print(line)
def add_arguments(self, parser):
parser.add_argument('--all', '-a', action='store_true')

View File

@ -1,9 +1,11 @@
from bonobo.config.configurables import Configurable
from bonobo.config.functools import transformation_factory
from bonobo.config.options import Method, Option
from bonobo.config.processors import ContextProcessor
from bonobo.config.services import Container, Exclusive, Service, requires, create_container
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, use, create_container
from bonobo.util import deprecated_alias
use = requires
requires = deprecated_alias('requires', use)
# Bonobo's Config API
__all__ = [
@ -16,5 +18,10 @@ __all__ = [
'Service',
'create_container',
'requires',
'transformation_factory',
'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.util import isoption, iscontextprocessor, sortedlist
__all__ = [
'Configurable',
@ -18,6 +18,7 @@ class ConfigurableMeta(type):
super().__init__(what, bases, dict)
cls.__processors = sortedlist()
cls.__processors_cache = None
cls.__methods = sortedlist()
cls.__options = sortedlist()
cls.__names = set()
@ -47,7 +48,9 @@ class ConfigurableMeta(type):
@property
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):
return ' '.join((
@ -65,7 +68,7 @@ except:
else:
class PartiallyConfigured(_functools.partial):
@property # TODO XXX cache this shit
@property # TODO XXX cache this
def _options_values(self):
""" Simulate option values for partially configured objects. """
try:
@ -142,8 +145,8 @@ class Configurable(metaclass=ConfigurableMeta):
if len(extraneous):
raise TypeError(
'{}() got {} unexpected option{}: {}.'.format(
cls.__name__,
len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
cls.__name__, len(extraneous), 's'
if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
)
)
@ -153,8 +156,8 @@ class Configurable(metaclass=ConfigurableMeta):
if _final:
raise TypeError(
'{}() missing {} required option{}: {}.'.format(
cls.__name__,
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
cls.__name__, len(missing), 's'
if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
)
)
return PartiallyConfigured(cls, *args, **kwargs)
@ -189,9 +192,11 @@ class Configurable(metaclass=ConfigurableMeta):
position += 1
def __call__(self, *args, **kwargs):
""" You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override.
"""
return self.call(*args, **kwargs)
raise AbstractError(
'You must implement the __call__ method in your configurable class {} to actually use it.'.format(
type(self).__name__
)
)
@property
def __options__(self):
@ -200,6 +205,3 @@ class Configurable(metaclass=ConfigurableMeta):
@property
def __processors__(self):
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)
keyword = Option(str, default='foo')
def call(self, s):
def __call__(self, s):
return self.title + ': ' + s + ' (' + self.keyword + ')'
example = Example('hello', keyword='bar')
@ -116,6 +116,22 @@ class RemovedOption(Option):
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):
"""
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__)
# 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_):
x = super(Method, self).__get__(inst, type_)
if inst:
@ -164,10 +186,12 @@ class Method(Option):
return x
def __set__(self, inst, value):
if not hasattr(value, '__call__'):
if not callable(value):
raise TypeError(
'Option of type {!r} is expecting a callable value, got {!r} object (which is not).'.format(
type(self).__name__, type(value).__name__
'Option {!r} ({}) is expecting a callable value, got {!r} object: {!r}.'.format(
self.name,
type(self).__name__,
type(value).__name__, 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 contextlib import contextmanager
from functools import partial
from inspect import signature
from bonobo.config import Option
from bonobo.errors import UnrecoverableTypeError
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):
@ -51,18 +58,11 @@ class ContextProcessor(Option):
def __call__(self, *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):
cls_or_func.__processors__.append(cls(processor))
return cls_or_func
return decorator
class bound(partial):
@property
def kwargs(self):
return self.keywords
class ContextCurrifier:
@ -70,18 +70,47 @@ class ContextCurrifier:
This is a helper to resolve processors.
"""
def __init__(self, wrapped, *initial_context):
def __init__(self, wrapped, *args, **kwargs):
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
def __iter__(self):
yield from self.wrapped
def __call__(self, *args, **kwargs):
if not callable(self.wrapped) and isinstance(self.wrapped, Iterable):
return self.__iter__()
return self.wrapped(*self.context, *args, **kwargs)
def _bind(self, _input):
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__()
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):
if self._stack is not None:
@ -89,14 +118,11 @@ class ContextCurrifier:
self._stack, self._stack_values = list(), list()
for processor in resolve_processors(self.wrapped):
_processed = processor(self.wrapped, *context, *self.context)
try:
_append_to_context = next(_processed)
except TypeError as exc:
raise TypeError('Context processor should be generators (using yield).') from exc
_processed = processor(self.wrapped, *context, *self.args, **self.kwargs)
_append_to_context = next(_processed)
self._stack_values.append(_append_to_context)
if _append_to_context is not None:
self.context += ensure_tuple(_append_to_context)
self.args += ensure_tuple(_append_to_context)
self._stack.append(_processed)
def teardown(self):
@ -139,3 +165,42 @@ def resolve_processors(mixed):
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 threading
import types
@ -73,13 +75,13 @@ class Container(dict):
return cls
return super().__new__(cls, *args, **kwargs)
def args_for(self, mixed):
def kwargs_for(self, mixed):
try:
options = dict(mixed.__options__)
except AttributeError:
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):
if not name in self:
@ -156,7 +158,7 @@ class Exclusive(ContextDecorator):
self.get_lock().release()
def requires(*service_names):
def use(*service_names):
def decorate(mixed):
try:
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')
END = Token('End')
INHERIT_INPUT = Token('InheritInput')
LOOPBACK = Token('Loopback')
NOT_MODIFIED = Token('NotModified')
DEFAULT_SERVICES_FILENAME = '_services.py'
DEFAULT_SERVICES_ATTR = 'get_services'
EMPTY = tuple()
TICK_PERIOD = 0.2
ARGNAMES = '_argnames'

View File

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

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

View File

@ -3,9 +3,11 @@ import sys
from contextlib import contextmanager
from logging import ERROR
from bonobo.util.objects import Wrapper, get_name
from mondrian import term
from bonobo.util import deprecated
from bonobo.util.objects import Wrapper, get_name
@contextmanager
def recoverable(error_handler):
@ -107,6 +109,13 @@ class Lifecycle:
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):
logging.getLogger(__name__).log(level, repr(self), exc_info=exc_info)
self._defunct = True

View File

@ -41,8 +41,8 @@ class GraphExecutionContext:
outputs = self.graph.outputs_of(i)
if len(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_end = partial(node_context.send, END, _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_finalize = partial(node_context.stop)
def __getitem__(self, item):

View File

@ -1,25 +1,36 @@
import logging
import sys
from collections import namedtuple
from queue import Empty
from time import sleep
from types import GeneratorType
from bonobo.config import create_container
from bonobo.config.processors import ContextCurrifier
from bonobo.constants import NOT_MODIFIED, BEGIN, END, TICK_PERIOD
from bonobo.errors import InactiveReadableError, UnrecoverableError
from bonobo.constants import NOT_MODIFIED, BEGIN, END, TICK_PERIOD, Token
from bonobo.errors import InactiveReadableError, UnrecoverableError, UnrecoverableTypeError
from bonobo.execution.contexts.base import BaseContext
from bonobo.structs.bags import Bag
from bonobo.structs.inputs import Input
from bonobo.structs.tokens import Token
from bonobo.util import get_name, iserrorbag, isloopbackbag, isbag, istuple, isconfigurabletype
from bonobo.util import get_name, istuple, isconfigurabletype, ensure_tuple
from bonobo.util.bags import BagType
from bonobo.util.statistics import WithStatistics
logger = logging.getLogger(__name__)
UnboundArguments = namedtuple('UnboundArguments', ['args', 'kwargs'])
class NodeExecutionContext(BaseContext, WithStatistics):
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)
WithStatistics.__init__(self, 'in', 'out', 'err', 'warn')
@ -37,6 +48,10 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self.input = _input or Input()
self.outputs = _outputs or []
# Types
self._input_type, self._input_length = None, None
self._output_type = None
# Stack: context decorators for the execution
self._stack = None
@ -48,22 +63,40 @@ class NodeExecutionContext(BaseContext, WithStatistics):
return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' '))
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()
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):
# 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.
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)
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):
"""
The actual infinite loop for this transformation.
"""
logger.debug('Node loop starts for {!r}.'.format(self))
while self.should_loop:
try:
self.step()
@ -72,29 +105,31 @@ class NodeExecutionContext(BaseContext, WithStatistics):
except Empty:
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
continue
except UnrecoverableError:
self.handle_error(*sys.exc_info())
self.input.shutdown()
break
except (
NotImplementedError,
UnrecoverableError,
):
self.fatal(sys.exc_info()) # exit loop
except Exception: # pylint: disable=broad-except
self.handle_error(*sys.exc_info())
self.error(sys.exc_info()) # does not exit loop
except BaseException:
self.handle_error(*sys.exc_info())
break
self.fatal(sys.exc_info()) # exit loop
logger.debug('Node loop ends for {!r}.'.format(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
input_bag = self.get()
Basically gets an input bag, send it to the node, interpret the results.
"""
# Pull and check data
input_bag = self._get()
# Sent through the stack
try:
results = input_bag.apply(self._stack)
except Exception:
return self.handle_error(*sys.exc_info())
results = self._stack(input_bag)
# self._exec_time += timer.duration
# Put data onto output channels
@ -109,32 +144,85 @@ class NodeExecutionContext(BaseContext, WithStatistics):
except StopIteration:
# That's not an error, we're just done.
break
except Exception:
# Let's kill this loop, won't be able to generate next.
self.handle_error(*sys.exc_info())
break
else:
self.send(_resolve(input_bag, result))
# Push data (in case of an iterator)
self._send(self._cast(input_bag, result))
elif results:
self.send(_resolve(input_bag, results))
# Push data (returned value)
self._send(self._cast(input_bag, results))
else:
# case with no result, an execution went through anyway, use for stats.
# self._exec_count += 1
pass
def stop(self):
"""
Cleanup the context, after the loop ended.
"""
if self._stack:
self._stack.teardown()
try:
self._stack.teardown()
except:
self.fatal(sys.exc_info())
super().stop()
def handle_error(self, exctype, exc, tb, *, level=logging.ERROR):
self.increment('err')
logging.getLogger(__name__).log(level, repr(self), exc_info=(exctype, exc, tb))
def send(self, *_output, _input=None):
return self._send(self._cast(_input, _output))
def fatal(self, exc_info, *, level=logging.CRITICAL):
super().fatal(exc_info, level=level)
self.input.shutdown()
### Input type and fields
@property
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):
"""
@ -143,14 +231,83 @@ class NodeExecutionContext(BaseContext, WithStatistics):
:param mixed value: message
"""
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):
self.write(BEGIN, *messages, END)
for _ in messages:
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.
@ -161,29 +318,15 @@ class NodeExecutionContext(BaseContext, WithStatistics):
if not _control:
self.increment('out')
if iserrorbag(value):
value.apply(self.handle_error)
elif isloopbackbag(value):
self.input.put(value)
else:
for output in self.outputs:
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
for output in self.outputs:
output.put(value)
def _get_initial_context(self):
if self.parent:
return self.parent.services.args_for(self.wrapped)
return UnboundArguments((), self.parent.services.kwargs_for(self.wrapped))
if self.services:
return self.services.args_for(self.wrapped)
return ()
return UnboundArguments((), self.services.kwargs_for(self.wrapped))
return UnboundArguments((), {})
def isflag(param):
@ -210,23 +353,3 @@ def split_tokens(output):
i += 1
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
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
from bonobo.structs.bags import Bag
from bonobo.constants import BEGIN, END
from bonobo.execution.strategies.base import Strategy
from bonobo.util import get_name
logger = logging.getLogger(__name__)
@ -24,7 +22,7 @@ class ExecutorStrategy(Strategy):
def execute(self, graph, **kwargs):
context = self.create_graph_execution_context(graph, **kwargs)
context.write(BEGIN, Bag(), END)
context.write(BEGIN, (), END)
futures = []

View File

@ -1,6 +1,5 @@
from bonobo.constants import BEGIN, END
from bonobo.execution.strategies.base import Strategy
from bonobo.structs.bags import Bag
class NaiveStrategy(Strategy):
@ -8,7 +7,7 @@ class NaiveStrategy(Strategy):
def execute(self, graph, **kwargs):
context = self.create_graph_execution_context(graph, **kwargs)
context.write(BEGIN, Bag(), END)
context.write(BEGIN, (), END)
# start
context.start()

View File

@ -1,11 +1,17 @@
import functools
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.config import Configurable, Option
from bonobo.config.processors import ContextProcessor
from bonobo.constants import NOT_MODIFIED, ARGNAMES
from bonobo.structs.bags import Bag
from bonobo.config import Configurable, Option, Method, use_raw_input, use_context, use_no_input
from bonobo.config.functools import transformation_factory
from bonobo.config.processors import ContextProcessor, use_context_processor
from bonobo.constants import NOT_MODIFIED
from bonobo.util.objects import ValueHolder
from bonobo.util.term import CLEAR_EOL
@ -14,11 +20,9 @@ __all__ = [
'Limit',
'PrettyPrinter',
'Tee',
'Update',
'arg0_to_kwargs',
'SetFields',
'count',
'identity',
'kwargs_to_arg0',
'noop',
]
@ -35,6 +39,8 @@ class Limit(Configurable):
Number of rows to let go through.
TODO: simplify into a closure building factory?
"""
limit = Option(positional=True, default=10)
@ -42,7 +48,7 @@ class Limit(Configurable):
def counter(self, context):
yield ValueHolder(0)
def call(self, counter, *args, **kwargs):
def __call__(self, counter, *args, **kwargs):
counter += 1
if counter <= self.limit:
yield NOT_MODIFIED
@ -60,17 +66,6 @@ def Tee(f):
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):
if w and len(s) > w:
s = s[0:w - 3] + '...'
@ -80,87 +75,73 @@ def _shorten(s, w):
class PrettyPrinter(Configurable):
max_width = Option(
int,
default=term.get_size()[0],
required=False,
__doc__='''
If set, truncates the output values longer than this to this width.
'''
)
def call(self, *args, **kwargs):
formater = self._format_quiet if settings.QUIET.get() else self._format_console
argnames = kwargs.get(ARGNAMES, None)
filter = Method(
default=
(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.
Default is to ignore any key starting with an underscore and none values.
'''
)
for i, (item, value) in enumerate(
itertools.chain(enumerate(args), filter(lambda x: not x[0].startswith('_'), kwargs.items()))
):
print(formater(i, item, value, argnames=argnames))
@ContextProcessor
def context(self, context):
yield context
def _format_quiet(self, i, item, value, *, argnames=None):
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))
return NOT_MODIFIED
def _format_quiet(self, index, key, value, *, fields=None):
# XXX should we implement argnames here ?
return ' '.join(((' ' if i else '-'), str(item), ':', str(value).strip()))
return ' '.join(((' ' if index else '-'), str(key), ':', 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])
def _format_console(self, index, key, value, *, fields=None):
fields = fields or []
if not isinstance(key, str):
if len(fields) >= key and str(key) != str(fields[key]):
key = '{}{}'.format(fields[key], term.lightblack('[{}]'.format(key)))
else:
item = str(i)
key = str(index)
return ' '.join(
(
(' ' if i else ''), item, '=', _shorten(str(value).strip(),
self.max_width).replace('\n', '\n' + CLEAR_EOL), CLEAR_EOL
)
)
prefix = '\u2503 {} = '.format(key)
prefix_length = len(prefix)
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 noop(*args, **kwargs): # pylint: disable=unused-argument
from bonobo.constants import NOT_MODIFIED
@use_no_input
def noop(*args, **kwargs):
return NOT_MODIFIED
def arg0_to_kwargs(row):
"""
Transform items in a stream from "arg0" format (each call only has one positional argument, which is a dict-like
object) to "kwargs" format (each call only has keyword arguments that represent a row).
:param row:
:return: bonobo.Bag
"""
return Bag(**row)
def kwargs_to_arg0(**row):
"""
Transform items in a stream from "kwargs" format (each call only has keyword arguments that represent a row) to
"arg0" format (each call only has one positional argument, which is a dict-like object) .
:param **row:
:return: bonobo.Bag
"""
return Bag(row)
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):
"""
Transformation factory to create fixed windows of inputs, as lists.
@ -176,10 +157,112 @@ class FixedWindow(Configurable):
def buffer(self, context):
buffer = yield ValueHolder([])
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):
buffer.append(x)
@use_raw_input
def __call__(self, buffer, bag):
buffer.append(bag)
if len(buffer) >= self.length:
yield buffer.get()
yield tuple(buffer.get())
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()
def call(self, *args, **kwargs):
def __call__(self, *args, **kwargs):
if self.filter(*args, **kwargs):
return NOT_MODIFIED

View File

@ -5,10 +5,11 @@ class FileHandler(Configurable):
"""Abstract component factory for file-related components.
Args:
fs (str): service name to use for filesystem.
path (str): which path to use within the provided filesystem.
eol (str): which character to use to separate lines.
mode (str): which mode to use when opening the file.
fs (str): service name to use for filesystem.
encoding (str): which encoding to use when opening the file.
"""
path = Option(str, required=True, positional=True) # type: str
@ -19,7 +20,7 @@ class FileHandler(Configurable):
fs = Service('fs') # type: str
@ContextProcessor
def file(self, context, fs):
def file(self, context, *, fs):
with self.open(fs) as file:
yield file
@ -28,22 +29,8 @@ class FileHandler(Configurable):
class Reader:
"""Abstract component factory for readers.
"""
def __call__(self, *args, **kwargs):
yield from self.read(*args, **kwargs)
def read(self, *args, **kwargs):
raise NotImplementedError('Abstract.')
pass
class Writer:
"""Abstract component factory for writers.
"""
def __call__(self, *args, **kwargs):
return self.write(*args, **kwargs)
def write(self, *args, **kwargs):
raise NotImplementedError('Abstract.')
pass

View File

@ -1,13 +1,12 @@
import csv
import warnings
from bonobo.config import Option, ContextProcessor
from bonobo.config.options import RemovedOption, Method
from bonobo.constants import NOT_MODIFIED, ARGNAMES
from bonobo.config import Option, use_raw_input, use_context
from bonobo.config.options import Method, RenamedOption
from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.structs.bags import Bag
from bonobo.util import ensure_tuple
from bonobo.util.bags import BagType
class CsvHandler(FileHandler):
@ -21,17 +20,38 @@ class CsvHandler(FileHandler):
The CSV quote character.
.. attribute:: headers
.. attribute:: fields
The list of column names, if the CSV does not contain it as its first line.
"""
delimiter = Option(str, default=';')
quotechar = Option(str, default='"')
headers = Option(ensure_tuple, required=False)
ioformat = RemovedOption(positional=False, value='kwargs')
# Dialect related options
delimiter = Option(str, default=csv.excel.delimiter, required=False)
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):
"""
Reads a CSV and yield the values as dicts.
@ -45,6 +65,7 @@ class CsvReader(FileReader, CsvHandler):
skip = Option(int, default=0)
@Method(
positional=False,
__doc__='''
Builds the CSV reader, a.k.a an object we can iterate, each iteration giving one line of fields, as an
iterable.
@ -53,20 +74,37 @@ class CsvReader(FileReader, CsvHandler):
'''
)
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)
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:
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):
@ContextProcessor
def context(self, context, *args):
yield context
@Method(
__doc__='''
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):
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):
try:
writer = context.writer
except AttributeError:
def write(self, file, context, *values, fs):
context.setdefault('lineno', 0)
fields = context.get_input_fields()
if not context.lineno:
context.writer = self.writer_factory(file)
writer = context.writer
context.headers = self.headers or _argnames
if context.headers and not lineno:
writer(context.headers)
if fields:
context.writer(fields)
context.lineno += 1
lineno += 1
if context.headers:
try:
row = [args[i] for i, header in enumerate(context.headers) if header]
except IndexError:
warnings.warn(
'At line #{}, expected {} fields but only got {}. Padding with empty strings.'.format(
lineno, len(context.headers), len(args)
if fields:
if len(values) != len(fields):
raise ValueError(
'Values length differs from input fields length. Expected: {}. Got: {}. Values: {!r}.'.format(
len(fields), len(values), values
)
)
row = [(args[i] if i < len(args) else '') for i, header in enumerate(context.headers) if header]
context.writer(values)
else:
row = args
writer(row)
for arg in values:
context.writer(ensure_tuple(arg))
return NOT_MODIFIED
__call__ = write

View File

@ -1,8 +1,8 @@
from bonobo.config import Option
from bonobo.config.processors import ContextProcessor
from bonobo.config import Option, ContextProcessor, use_context
from bonobo.constants import NOT_MODIFIED
from bonobo.errors import UnrecoverableError
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):
@ -14,7 +14,44 @@ class FileReader(Reader, FileHandler):
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.
Prefix is used for newlines.
@ -22,7 +59,10 @@ class FileReader(Reader, FileHandler):
for line in file:
yield line.rstrip(self.eol)
__call__ = read
@use_context
class FileWriter(Writer, FileHandler):
"""Component factory for file or file-like writers.
@ -32,18 +72,16 @@ class FileWriter(Writer, FileHandler):
mode = Option(str, default='w+')
@ContextProcessor
def lineno(self, context, fs, file):
lineno = ValueHolder(0)
yield lineno
def write(self, fs, file, lineno, line):
def write(self, file, context, line, *, fs):
"""
Write a row on the next line of opened file in context.
"""
self._write_line(file, (self.eol if lineno.value else '') + line)
lineno += 1
context.setdefault('lineno', 0)
self._write_line(file, (self.eol if context.lineno else '') + line)
context.lineno += 1
return NOT_MODIFIED
def _write_line(self, file, line):
return file.write(line)
__call__ = write

View File

@ -1,78 +1,86 @@
import json
from collections import OrderedDict
from bonobo.config.options import RemovedOption
from bonobo.config.processors import ContextProcessor
from bonobo.config import Method
from bonobo.config.processors import ContextProcessor, use_context
from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.structs.bags import Bag
class JsonHandler(FileHandler):
eol = ',\n'
prefix, suffix = '[', ']'
ioformat = RemovedOption(positional=False, value='kwargs')
class JsonReader(FileReader, JsonHandler):
loader = staticmethod(json.load)
def read(self, fs, file):
for line in self.loader(file):
yield line
class LdjsonHandler(FileHandler):
eol = '\n'
prefix, suffix = '', ''
class JsonDictItemsReader(JsonReader):
def read(self, fs, file):
for line in self.loader(file).items():
yield Bag(*line)
class JsonReader(JsonHandler, FileReader):
@Method(positional=False)
def loader(self, file):
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
def envelope(self, context, fs, file, lineno):
def envelope(self, context, file, *, fs):
file.write(self.prefix)
yield
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.
:param ctx:
:param row:
"""
row = _getrow(arg0, kwargs)
self._write_line(file, (self.eol if lineno.value else '') + json.dumps(row))
lineno += 1
context.setdefault('lineno', 0)
fields = context.get_input_fields()
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
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)
__call__ = write
class LdjsonWriter(FileWriter):
"""Write a stream of JSON objects, one object per line."""
@use_context
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):
row = _getrow(arg0, kwargs)
file.write(json.dumps(row) + '\n')
lineno += 1 # class-level variable
return NOT_MODIFIED
Not to be mistaken with JSON-LD (where LD stands for linked data).
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
from bonobo.config import Option
from bonobo.config.processors import ContextProcessor
from bonobo.config import Option, use_context
from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.util.objects import ValueHolder
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):
"""
Reads a Python pickle object and yields the items in dicts.
@ -27,11 +26,7 @@ class PickleReader(FileReader, PickleHandler):
mode = Option(str, default='rb')
@ContextProcessor
def pickle_headers(self, context, fs, file):
yield ValueHolder(self.item_names)
def read(self, fs, file, pickle_headers):
def read(self, file, context, *, fs):
data = pickle.load(file)
# 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:
iterator = iter([data])
if not pickle_headers.get():
pickle_headers.set(next(iterator))
if not context.output_type:
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:
if len(i) != item_count:
raise ValueError('Received an object with %d items, expecting %d.' % (
len(i),
item_count,
))
yield tuple(row.values() if is_dict else row)
yield dict(zip(i)) if is_dict else dict(zip(pickle_headers.value, i))
__call__ = read
@use_context
class PickleWriter(FileWriter, PickleHandler):
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.
"""
context.setdefault('lineno', 0)
file.write(pickle.dumps(item))
lineno += 1
context.lineno += 1
return NOT_MODIFIED
__call__ = write

View File

View File

@ -47,6 +47,6 @@ class RateLimited(Configurable):
bucket.stop()
bucket.join()
def call(self, bucket, *args, **kwargs):
def __call__(self, bucket, *args, **kwargs):
bucket.wait()
return self.handler(*args, **kwargs)

View File

@ -95,30 +95,26 @@ class ConsoleOutputPlugin(Plugin):
liveliness_color = alive_color if node.alive else dead_color
liveliness_prefix = ' {}{}{} '.format(liveliness_color, node.status, Style.RESET_ALL)
_line = ''.join(
(
liveliness_prefix,
node.name,
name_suffix,
' ',
node.get_statistics_as_string(),
' ',
node.get_flags_as_string(),
Style.RESET_ALL,
' ',
)
)
_line = ''.join((
liveliness_prefix,
node.name,
name_suffix,
' ',
node.get_statistics_as_string(),
' ',
node.get_flags_as_string(),
Style.RESET_ALL,
' ',
))
print(prefix + _line + CLEAR_EOL, file=self._stderr)
if append:
# todo handle multiline
print(
''.join(
(
' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
CLEAR_EOL
)
),
''.join((
' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
CLEAR_EOL
)),
file=self._stderr
)
t_cnt += 1
@ -132,10 +128,9 @@ class ConsoleOutputPlugin(Plugin):
if self.counter % 10 and self._append_cache:
append = self._append_cache
else:
self._append_cache = append = (
('Memory', '{0:.2f} Mb'.format(memory_usage())),
# ('Total time', '{0} s'.format(execution_time(harness))),
)
self._append_cache = append = (('Memory', '{0:.2f} Mb'.format(memory_usage())),
# ('Total time', '{0} s'.format(execution_time(harness))),
)
else:
append = ()
self.write(context, prefix=self.prefix, append=append, rewind=rewind)

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.tokens import Token
__all__ = [
'Bag',
'ErrorBag',
'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 json
from collections import namedtuple
from copy import copy
from graphviz import ExecutableNotFound
from graphviz.dot import Digraph
from bonobo.constants import BEGIN
from bonobo.util import get_name
GraphRange = namedtuple('GraphRange', ['graph', 'input', 'output'])
class Graph:
"""
@ -51,15 +55,19 @@ class Graph:
if len(nodes):
_input = self._resolve_index(_input)
_output = self._resolve_index(_output)
_first = None
_last = None
for i, node in enumerate(nodes):
_next = self.add_node(node)
_last = self.add_node(node)
if not i and _name:
if _name in self.named:
raise KeyError('Duplicate name {!r} in graph.'.format(_name))
self.named[_name] = _next
self.outputs_of(_input, create=True).add(_next)
_input = _next
self.named[_name] = _last
if not _first:
_first = _last
self.outputs_of(_input, create=True).add(_last)
_input = _last
if _output is not None:
self.outputs_of(_input, create=True).add(_output)
@ -67,7 +75,8 @@ class Graph:
if hasattr(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):
g = Graph()
@ -135,11 +144,11 @@ class Graph:
def _repr_dot_(self):
return str(self.graphviz)
def _repr_svg_(self):
return self.graphviz._repr_svg_()
def _repr_html_(self):
return '<div>{}</div><pre>{}</pre>'.format(self.graphviz._repr_svg_(), html.escape(repr(self)))
try:
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):
""" 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.inspect import (
inspect_node,
isbag,
isconfigurable,
isconfigurabletype,
iscontextprocessor,
isdict,
iserrorbag,
isloopbackbag,
ismethod,
isoption,
istuple,
@ -25,13 +22,10 @@ __all__ = [
'get_attribute_or_create',
'get_name',
'inspect_node',
'isbag',
'isconfigurable',
'isconfigurabletype',
'iscontextprocessor',
'isdict',
'iserrorbag',
'isloopbackbag',
'ismethod',
'isoption',
'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)
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.
Otherwise, not changed.
@ -16,11 +16,17 @@ def ensure_tuple(tuple_or_mixed):
:return: tuple
"""
if tuple_or_mixed is None:
return ()
if isinstance(tuple_or_mixed, tuple):
if isinstance(tuple_or_mixed, cls):
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):

View File

@ -12,16 +12,20 @@ def isconfigurable(mixed):
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
plumbery necessary to build :class:`bonobo.config.Configurable`-like instances.
:param mixed:
:param strict: should we consider partially configured objects?
:return: bool
"""
from bonobo.config.configurables import ConfigurableMeta
return isinstance(mixed, ConfigurableMeta)
from bonobo.config.configurables import ConfigurableMeta, PartiallyConfigured
return isinstance(mixed, (ConfigurableMeta, ) if strict else (
ConfigurableMeta,
PartiallyConfigured,
))
def isoption(mixed):
@ -88,39 +92,6 @@ def istuple(mixed):
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', [
'type',
@ -149,7 +120,7 @@ def inspect_node(mixed, *, _partial=None):
:raise: TypeError
"""
if isconfigurabletype(mixed):
if isconfigurabletype(mixed, strict=True):
inst, typ = None, mixed
elif isconfigurable(mixed):
inst, typ = mixed, type(mixed)

View File

@ -1,3 +1,4 @@
import contextlib
import functools
import io
import os
@ -8,8 +9,9 @@ from unittest.mock import patch
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.constants import Token
from bonobo.execution.contexts.graph import GraphExecutionContext
from bonobo.execution.contexts.node import NodeExecutionContext
@ -24,9 +26,9 @@ def optional_contextmanager(cm, *, ignore=False):
class FilesystemTester:
def __init__(self, extension='txt', mode='w'):
def __init__(self, extension='txt', mode='w', *, input_data=''):
self.extension = extension
self.input_data = ''
self.input_data = input_data
self.mode = mode
def get_services_for_reader(self, tmpdir):
@ -58,7 +60,7 @@ class BufferingContext:
return self.buffer
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):
@ -141,3 +143,121 @@ class EnvironmentTestCase():
assert err == ''
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):
database = Service('orders_database')
def call(self, database, row):
def __call__(self, database, row):
return {
**row,
'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
class ChangeCase(Configurable):
modifier = Option(default='upper')
def call(self, s: str) -> str:
def __call__(self, s: str) -> str:
return getattr(s, self.modifier)()
# transformation factory

View File

@ -30,7 +30,7 @@ Configurables allows to use the following features:
class PrefixIt(Configurable):
prefix = Option(str, positional=True, default='>>>')
def call(self, row):
def __call__(self, row):
return self.prefix + ' ' + row
prefixer = PrefixIt('$')
@ -48,7 +48,7 @@ Configurables allows to use the following features:
url = Option(default='https://jsonplaceholder.typicode.com/users')
http = Service('http.client')
def call(self, http):
def __call__(self, http):
resp = http.get(self.url)
for row in resp.json():
@ -68,7 +68,7 @@ Configurables allows to use the following features:
class Applier(Configurable):
apply = Method()
def call(self, row):
def __call__(self, row):
return self.apply(row)
@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')
http = Service('http.client')
def call(self, http):
def __call__(self, http):
resp = http.get(self.url)
for row in resp.json():

View File

@ -30,7 +30,7 @@ Configurables allows to use the following features:
class PrefixIt(Configurable):
prefix = Option(str, positional=True, default='>>>')
def call(self, row):
def __call__(self, row):
return self.prefix + ' ' + row
prefixer = PrefixIt('$')
@ -48,7 +48,7 @@ Configurables allows to use the following features:
url = Option(default='https://jsonplaceholder.typicode.com/users')
http = Service('http.client')
def call(self, http):
def __call__(self, http):
resp = http.get(self.url)
for row in resp.json():
@ -68,7 +68,7 @@ Configurables allows to use the following features:
class Applier(Configurable):
apply = Method()
def call(self, row):
def __call__(self, row):
return self.apply(row)
@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')
http = Service('http.client')
def call(self, http):
def __call__(self, http):
resp = http.get(self.url)
for row in resp.json():

View File

@ -9,12 +9,12 @@ idna==2.6
imagesize==0.7.1
jinja2==2.10
markupsafe==1.0
py==1.4.34
py==1.5.2
pygments==2.2.0
pytest-cov==2.5.1
pytest-sugar==0.9.0
pytest-timeout==1.2.0
pytest==3.2.3
pytest==3.2.5
pytz==2017.3
requests==2.18.4
six==1.11.0
@ -23,4 +23,4 @@ sphinx==1.6.5
sphinxcontrib-websupport==1.0.1
termcolor==1.1.0
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
docker-pycreds==0.2.1
docker==2.3.0
fs==2.0.16
fs==2.0.17
idna==2.6
packaging==16.8
pbr==3.1.1

View File

@ -32,7 +32,7 @@ pyzmq==17.0.0b3
qtconsole==4.3.1
simplegeneric==0.8.1
six==1.11.0
terminado==0.6
terminado==0.7
testpath==0.3.1
tornado==4.5.2
traitlets==4.3.2

View File

@ -4,7 +4,7 @@ bonobo-sqlalchemy==0.5.1
certifi==2017.11.5
chardet==3.0.4
colorama==0.3.9
fs==2.0.16
fs==2.0.17
idna==2.6
packaging==16.8
pbr==3.1.1

View File

@ -3,19 +3,21 @@ appdirs==1.4.3
certifi==2017.11.5
chardet==3.0.4
colorama==0.3.9
fs==2.0.16
fs==2.0.17
graphviz==0.8.1
idna==2.6
jinja2==2.10
markupsafe==1.0
mondrian==0.4.0
mondrian==0.5.1
packaging==16.8
pbr==3.1.1
psutil==5.4.1
pyparsing==2.2.0
python-slugify==1.2.4
pytz==2017.3
requests==2.18.4
six==1.11.0
stevedore==1.27.1
unidecode==0.4.21
urllib3==1.22
whistle==1.0.0

View File

@ -43,14 +43,12 @@ else:
setup(
author='Romain Dorgueil',
author_email='romain@dorgueil.net',
data_files=[
(
'share/jupyter/nbextensions/bonobo-jupyter', [
'bonobo/contrib/jupyter/static/extension.js', 'bonobo/contrib/jupyter/static/index.js',
'bonobo/contrib/jupyter/static/index.js.map'
]
)
],
data_files=[(
'share/jupyter/nbextensions/bonobo-jupyter', [
'bonobo/contrib/jupyter/static/extension.js', 'bonobo/contrib/jupyter/static/index.js',
'bonobo/contrib/jupyter/static/index.js.map'
]
)],
description=('Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
'python 3.5+.'),
license='Apache License, Version 2.0',
@ -62,8 +60,8 @@ setup(
include_package_data=True,
install_requires=[
'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)',
'stevedore (>= 1.27, < 1.28)', 'whistle (>= 1.0, < 1.1)'
'mondrian (>= 0.5, < 0.6)', 'packaging (>= 16, < 17)', 'psutil (>= 5.4, < 6)', 'python-slugify (>= 1.2, < 1.3)',
'requests (>= 2, < 3)', 'stevedore (>= 1.27, < 1.28)', 'whistle (>= 1.0, < 1.1)'
],
extras_require={
'dev': [

View File

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

View File

@ -7,7 +7,7 @@ class MethodBasedConfigurable(Configurable):
foo = Option(positional=True)
bar = Option()
def call(self, *args, **kwargs):
def __call__(self, *args, **kwargs):
self.handler(*args, **kwargs)

View File

@ -14,7 +14,7 @@ class Bobby(Configurable):
def think(self, context):
yield 'different'
def call(self, think, *args, **kwargs):
def __call__(self, think, *args, **kwargs):
self.handler('1', *args, **kwargs)
self.handler2('2', *args, **kwargs)

View File

@ -1,7 +1,7 @@
from operator import attrgetter
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):
@ -59,5 +59,16 @@ def test_setup_teardown():
o = CP1()
stack = ContextCurrifier(o)
stack.setup()
assert o(*stack.context) == ('this is A', 'THIS IS b')
assert o(*stack.args) == ('this is A', 'THIS IS b')
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
from bonobo.util import get_name
from bonobo.config import Configurable, Container, Exclusive, Service, requires
from bonobo.config import Configurable, Container, Exclusive, Service, use
from bonobo.config.services import validate_service_name, create_container
from bonobo.util import get_name
class PrinterInterface():
@ -30,7 +30,7 @@ SERVICES = Container(
class MyServiceDependantConfigurable(Configurable):
printer = Service(PrinterInterface, )
def __call__(self, printer: PrinterInterface, *args):
def __call__(self, *args, printer: PrinterInterface):
return printer.print(*args)
@ -51,15 +51,15 @@ def test_service_name_validator():
def test_service_dependency():
o = MyServiceDependantConfigurable(printer='printer0')
assert o(SERVICES.get('printer0'), 'foo', 'bar') == '0;foo;bar'
assert o(SERVICES.get('printer1'), 'bar', 'baz') == '1;bar;baz'
assert o(*SERVICES.args_for(o), 'foo', 'bar') == '0;foo;bar'
assert o('foo', 'bar', printer=SERVICES.get('printer0')) == '0;foo;bar'
assert o('bar', 'baz', printer=SERVICES.get('printer1')) == '1;bar;baz'
assert o('foo', 'bar', **SERVICES.kwargs_for(o)) == '0;foo;bar'
def test_service_dependency_unavailable():
o = MyServiceDependantConfigurable(printer='printer2')
with pytest.raises(KeyError):
SERVICES.args_for(o)
SERVICES.kwargs_for(o)
class VCR:
@ -100,13 +100,13 @@ def test_requires():
services = Container(output=vcr.append)
@requires('output')
@use('output')
def append(out, x):
out(x)
svcargs = services.args_for(append)
svcargs = services.kwargs_for(append)
assert len(svcargs) == 1
assert svcargs[0] == vcr.append
assert svcargs['output'] == vcr.append
@pytest.mark.parametrize('services', [None, {}])

View File

@ -2,7 +2,8 @@ from unittest.mock import MagicMock
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.strategies import NaiveStrategy
from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext
@ -13,23 +14,23 @@ def test_node_string():
return 'foo'
with BufferingNodeExecutionContext(f) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == 'foo'
assert output[0] == ('foo', )
def g():
yield 'foo'
yield 'bar'
with BufferingNodeExecutionContext(g) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == 'foo'
assert output[1] == 'bar'
assert output[0] == ('foo', )
assert output[1] == ('bar', )
def test_node_bytes():
@ -37,23 +38,23 @@ def test_node_bytes():
return b'foo'
with BufferingNodeExecutionContext(f) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == b'foo'
assert output[0] == (b'foo', )
def g():
yield b'foo'
yield b'bar'
with BufferingNodeExecutionContext(g) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == b'foo'
assert output[1] == b'bar'
assert output[0] == (b'foo', )
assert output[1] == (b'bar', )
def test_node_dict():
@ -61,40 +62,38 @@ def test_node_dict():
return {'id': 1, 'name': 'foo'}
with BufferingNodeExecutionContext(f) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == {'id': 1, 'name': 'foo'}
assert output[0] == ({'id': 1, 'name': 'foo'}, )
def g():
yield {'id': 1, 'name': 'foo'}
yield {'id': 2, 'name': 'bar'}
with BufferingNodeExecutionContext(g) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == {'id': 1, 'name': 'foo'}
assert output[1] == {'id': 2, 'name': 'bar'}
assert output[0] == ({'id': 1, 'name': 'foo'}, )
assert output[1] == ({'id': 2, 'name': 'bar'}, )
def test_node_dict_chained():
strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext)
def uppercase_name(**kwargs):
return {**kwargs, 'name': kwargs['name'].upper()}
def f():
return {'id': 1, 'name': 'foo'}
def uppercase_name(values):
return {**values, 'name': values['name'].upper()}
graph = Graph(f, uppercase_name)
context = strategy.execute(graph)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == {'id': 1, 'name': 'FOO'}
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
def g():
yield {'id': 1, 'name': 'foo'}
@ -105,8 +104,8 @@ def test_node_dict_chained():
output = context.get_buffer()
assert len(output) == 2
assert output[0] == {'id': 1, 'name': 'FOO'}
assert output[1] == {'id': 2, 'name': 'BAR'}
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
assert output[1] == ({'id': 2, 'name': 'BAR'}, )
def test_node_tuple():
@ -114,7 +113,7 @@ def test_node_tuple():
return 'foo', 'bar'
with BufferingNodeExecutionContext(f) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
@ -125,7 +124,7 @@ def test_node_tuple():
yield 'foo', 'baz'
with BufferingNodeExecutionContext(g) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
@ -167,7 +166,7 @@ def test_node_tuple_dict():
return 'foo', 'bar', {'id': 1}
with BufferingNodeExecutionContext(f) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
@ -178,7 +177,7 @@ def test_node_tuple_dict():
yield 'foo', 'baz', {'id': 2}
with BufferingNodeExecutionContext(g) as context:
context.write_sync(Bag())
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2

View File

@ -21,19 +21,9 @@ class ResponseMock:
def test_read_from_opendatasoft_api():
extract = OpenDataSoftAPI(dataset='test-a-set')
with patch(
'requests.get', return_value=ResponseMock([
{
'fields': {
'foo': 'bar'
}
},
{
'fields': {
'foo': 'zab'
}
},
])
):
with patch('requests.get', return_value=ResponseMock([
{'fields': {'foo': 'bar'}},
{'fields': {'foo': 'zab'}},
])):
for line in extract('http://example.com/', ValueHolder(0)):
assert 'foo' in line

View File

@ -9,13 +9,7 @@ def useless(*args, **kwargs):
def test_not_modified():
input_messages = [
('foo', 'bar'),
{
'foo': 'bar'
},
('foo', {
'bar': 'baz'
}),
(),
('foo', 'baz'),
]
with BufferingNodeExecutionContext(useless) as context:

View File

@ -1,62 +1,152 @@
from collections import namedtuple
from unittest import TestCase
import pytest
from bonobo import CsvReader, CsvWriter, settings
from bonobo.execution.contexts.node import NodeExecutionContext
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext
from bonobo import CsvReader, CsvWriter
from bonobo.constants import EMPTY
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext, WriterTest, ConfigurableNodeTest, ReaderTest
csv_tester = FilesystemTester('csv')
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):
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')
incontext = ConfigurableNodeTest.incontext
def test_read_csv_from_file_kwargs(tmpdir):
fs, filename, services = csv_tester.get_services_for_reader(tmpdir)
with BufferingNodeExecutionContext(
CsvReader(path=filename, delimiter=','),
services=services,
) as context:
context.write_sync(())
with BufferingNodeExecutionContext(CsvReader(filename, **defaults), services=services) as context:
context.write_sync(EMPTY)
assert context.get_buffer_args_as_dicts() == [
{
'a': 'a foo',
'b': 'b foo',
'c': 'c foo',
}, {
'a': 'a bar',
'b': 'b bar',
'c': 'c bar',
}
]
assert context.get_buffer_args_as_dicts() == [{
'a': 'a foo',
'b': 'b foo',
'c': 'c foo',
}, {
'a': 'a bar',
'b': 'b 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
from bonobo import Bag, FileReader, FileWriter
from bonobo.constants import BEGIN, END
from bonobo import FileReader, FileWriter
from bonobo.constants import EMPTY
from bonobo.execution.contexts.node import NodeExecutionContext
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)
with NodeExecutionContext(FileWriter(path=filename), services=services) as context:
context.write(BEGIN, *map(Bag, lines), END)
for _ in range(len(lines)):
context.step()
context.write_sync(*lines)
with fs.open(filename) as fp:
assert fp.read() == output
@ -42,9 +40,9 @@ def test_file_reader(tmpdir):
fs, filename, services = txt_tester.get_services_for_reader(tmpdir)
with BufferingNodeExecutionContext(FileReader(path=filename), services=services) as context:
context.write_sync(Bag())
output = context.get_buffer()
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == 'Hello'
assert output[1] == 'World'
assert output[0] == ('Hello', )
assert output[1] == ('World', )

View File

@ -1,66 +1,300 @@
import json
from collections import OrderedDict, namedtuple
from unittest import TestCase
import pytest
from bonobo import JsonReader, JsonWriter, settings
from bonobo import JsonReader, JsonWriter
from bonobo import LdjsonReader, LdjsonWriter
from bonobo.execution.contexts.node import NodeExecutionContext
from bonobo.util.testing import FilesystemTester, BufferingNodeExecutionContext
from bonobo.constants import EMPTY
from bonobo.util.testing import WriterTest, ReaderTest, ConfigurableNodeTest
json_tester = FilesystemTester('json')
json_tester.input_data = '''[{"x": "foo"},{"x": "bar"}]'''
FOOBAR = {'foo': '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):
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
with pytest.raises(ValueError):
JsonWriter(filename, ioformat=settings.IOFORMAT_ARG0)
with pytest.raises(ValueError):
JsonReader(filename, ioformat=settings.IOFORMAT_ARG0),
class Json:
extension = 'json'
ReaderNodeType = JsonReader
WriterNodeType = JsonWriter
@pytest.mark.parametrize('add_kwargs', (
{},
{
'ioformat': settings.IOFORMAT_KWARGS,
},
))
def test_write_json_kwargs(tmpdir, add_kwargs):
fs, filename, services = json_tester.get_services_for_writer(tmpdir)
class JsonReaderDictsTest(Json, ReaderTest, TestCase):
input_data = '[{"foo": "bar"},\n{"baz": "boz"}]'
with NodeExecutionContext(JsonWriter(filename, **add_kwargs), services=services) as context:
context.write_sync({'foo': 'bar'})
@incontext()
def test_nofields(self, context):
context.write_sync(EMPTY)
context.stop()
with fs.open(filename) as fp:
assert fp.read() == '[{"foo": "bar"}]'
assert context.get_buffer() == [
({
"foo": "bar"
}, ),
({
"baz": "boz"
}, ),
]
stream_json_tester = FilesystemTester('json')
stream_json_tester.input_data = '''{"foo": "bar"}\n{"baz": "boz"}'''
class JsonReaderListsTest(Json, 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], ),
]
def test_read_stream_json(tmpdir):
fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir)
with BufferingNodeExecutionContext(LdjsonReader(filename), services=services) as context:
context.write_sync(tuple())
actual = context.get_buffer()
class JsonReaderStringsTest(Json, ReaderTest, TestCase):
input_data = '[' + ',\n'.join(map(json.dumps, ('foo', 'bar', 'baz'))) + ']'
expected = [{"foo": "bar"}, {"baz": "boz"}]
assert expected == actual
@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', ),
]
def test_write_stream_json(tmpdir):
fs, filename, services = stream_json_tester.get_services_for_reader(tmpdir)
class JsonWriterTest(Json, WriterTest, TestCase):
@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:
context.write_sync(
{
'foo': 'bar'
},
{'baz': 'boz'},
assert self.readlines() == (
'[{"foo": "a", "bar": "b"},',
'{"foo": "c", "bar": "d"}]',
)
expected = '''{"foo": "bar"}\n{"baz": "boz"}\n'''
with fs.open(filename) as fin:
actual = fin.read()
assert expected == actual
@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() == ('[]', )
###
# 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
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.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)
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:
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)
with BufferingNodeExecutionContext(PickleReader(filename), services=services) as context:
context.write_sync(())
output = context.get_buffer()
context.write_sync(EMPTY)
assert len(output) == 2
assert output[0] == {
'a': 'a foo',
'b': 'b foo',
'c': 'c foo',
}
assert output[1] == {
'a': 'a bar',
'b': 'b bar',
'c': 'c bar',
}
output = context.get_buffer()
assert context.get_output_fields() == ('a', 'b', 'c')
assert output == [
('a foo', 'b foo', 'c foo'),
('a bar', 'b bar', 'c bar'),
]

View File

@ -1,64 +1,79 @@
from operator import methodcaller
from unittest import TestCase
from unittest.mock import MagicMock
import pytest
import bonobo
from bonobo.config.processors import ContextCurrifier
from bonobo.constants import NOT_MODIFIED
from bonobo.util.testing import BufferingNodeExecutionContext
from bonobo.constants import NOT_MODIFIED, EMPTY
from bonobo.util import ensure_tuple, ValueHolder
from bonobo.util.testing import BufferingNodeExecutionContext, StaticNodeTest, ConfigurableNodeTest
def test_count():
with pytest.raises(TypeError):
bonobo.count()
class CountTest(StaticNodeTest, TestCase):
node = bonobo.count
context = MagicMock()
def test_counter_required(self):
with pytest.raises(TypeError):
self.call()
with ContextCurrifier(bonobo.count).as_contextmanager(context) as stack:
for i in range(42):
stack()
def test_manual_call(self):
counter = ValueHolder(0)
for i in range(3):
self.call(counter)
assert counter == 3
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_execution(self):
with self.execute() as context:
context.write_sync(*([EMPTY] * 42))
assert context.get_buffer() == [(42, )]
def test_identity():
assert bonobo.identity(42) == 42
class IdentityTest(StaticNodeTest, TestCase):
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():
context, results = MagicMock(), []
class LimitTest(ConfigurableNodeTest, TestCase):
@classmethod
def setUpClass(cls):
cls.NodeType = bonobo.Limit
with ContextCurrifier(bonobo.Limit(2)).as_contextmanager(context) as stack:
for i in range(42):
results += list(stack())
def test_execution_default(self):
object_list = [object() for _ in range(42)]
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():
context, results = MagicMock(), []
assert context.get_buffer() == list(map(ensure_tuple, object_list[:21]))
with ContextCurrifier(bonobo.Limit(42)).as_contextmanager(context) as stack:
for i in range(10):
results += list(stack())
def test_manual(self):
limit = self.NodeType(5)
buffer = []
for x in range(10):
buffer += list(limit(x))
assert len(buffer) == 5
assert results == [NOT_MODIFIED] * 10
def test_limit_default():
context, results = MagicMock(), []
with ContextCurrifier(bonobo.Limit()).as_contextmanager(context) as stack:
for i in range(20):
results += list(stack())
assert results == [NOT_MODIFIED] * 10
def test_underflow(self):
limit = self.NodeType(10)
buffer = []
for x in range(5):
buffer += list(limit(x))
assert len(buffer) == 5
def test_tee():
@ -76,36 +91,28 @@ def test_noop():
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():
with BufferingNodeExecutionContext(bonobo.FixedWindow(2)) as context:
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:
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:
context.write_sync(*range(3))
assert context.get_buffer() == [[0], [1], [2]]
assert context.get_buffer() == [(0, ), (1, ), (2, )]
def test_methodcaller():
with BufferingNodeExecutionContext(methodcaller('swapcase')) as context:
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:
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():

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.execution.contexts.graph import GraphExecutionContext
from bonobo.execution.strategies import NaiveStrategy
from bonobo.structs import Bag, Graph
from bonobo.structs import Graph
def generate_integers():
yield from range(10)
def square(i: int) -> int:
def square(i):
return i**2
def push_result(results, i: int):
results.append(i)
@ContextProcessor.decorate(push_result)
def results(f, context):
results = []
yield results
results = yield list()
context.parent.results = results
@use_context_processor(results)
def push_result(results, i):
results.append(i)
chain = (generate_integers, square, push_result)
@ -62,7 +61,7 @@ def test_simple_execution_context():
assert not context.started
assert not context.stopped
context.write(BEGIN, Bag(), END)
context.write(BEGIN, (), END)
assert not context.alive
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'
)